Index: chrome/browser/sync/notifier/communicator/mailbox.cc |
=================================================================== |
--- chrome/browser/sync/notifier/communicator/mailbox.cc (revision 0) |
+++ chrome/browser/sync/notifier/communicator/mailbox.cc (revision 0) |
@@ -0,0 +1,682 @@ |
+// Copyright (c) 2009 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#include "chrome/browser/sync/notifier/communicator/mailbox.h" |
+ |
+#include <assert.h> |
+#include <stdlib.h> |
+ |
+#include <stack> |
+#include <vector> |
+ |
+#include "chrome/browser/sync/notifier/base/string.h" |
+#include "chrome/browser/sync/notifier/base/utils.h" |
+#include "chrome/browser/sync/notifier/communicator/xml_parse_helpers.h" |
+#include "talk/base/basictypes.h" |
+#include "talk/base/common.h" |
+#include "talk/base/stringutils.h" |
+#include "talk/xmllite/xmlelement.h" |
+#include "talk/xmpp/constants.h" |
+ |
+namespace notifier { |
+ |
+// Labels are a list of strings seperated by a '|' character. |
+// The '|' character is escaped with a backslash ('\\') and the |
+// backslash is also escaped with a backslash. |
+static void ParseLabelSet(const std::string& text, |
+ MessageThread::StringSet* labels) { |
+ const char* input_cur = text.c_str(); |
+ const char* input_end = input_cur + text.size(); |
+ char* result = new char[text.size() + 1]; |
+ char* next_write = result; |
+ |
+ while(input_cur < input_end) { |
+ if (*input_cur == '|') { |
+ if (next_write != result) { |
+ *next_write = '\0'; |
+ labels->insert(std::string(result)); |
+ next_write = result; |
+ } |
+ input_cur++; |
+ continue; |
+ } |
+ |
+ if (*input_cur == '\\') { |
+ // skip a character in the input and break if we are at the end |
+ input_cur++; |
+ if (input_cur >= input_end) |
+ break; |
+ } |
+ *next_write = *input_cur; |
+ next_write++; |
+ input_cur++; |
+ } |
+ |
+ if (next_write != result) { |
+ *next_write = '\0'; |
+ labels->insert(std::string(result)); |
+ } |
+ |
+ delete [] result; |
+} |
+ |
+// ----------------------------------------------------------------------------- |
+ |
+std::string MailAddress::safe_name() const { |
+ if (!name().empty()) { |
+ return name(); |
+ } |
+ |
+ if (!address().empty()) { |
+ size_t at = address().find('@'); |
+ if (at == std::string::npos) { |
+ return address(); |
+ } |
+ |
+ if (at != 0) { |
+ return address().substr(0, at); |
+ } |
+ } |
+ |
+ return std::string("(unknown)"); |
+} |
+ |
+// ----------------------------------------------------------------------------- |
+MessageThread::~MessageThread() { |
+ Clear(); |
+} |
+ |
+void MessageThread::Clear() { |
+ delete labels_; |
+ labels_ = NULL; |
+ |
+ delete senders_; |
+ senders_ = NULL; |
+} |
+ |
+MessageThread& MessageThread::operator=(const MessageThread& r) { |
+ if (&r != this) { |
+ Clear(); |
+ // Copy everything |
+ r.AssertValid(); |
+ thread_id_ = r.thread_id_; |
+ date64_ = r.date64_; |
+ message_count_ = r.message_count_; |
+ personal_level_ = r.personal_level_; |
+ subject_ = r.subject_; |
+ snippet_ = r.snippet_; |
+ |
+ if (r.labels_) |
+ labels_ = new StringSet(*r.labels_); |
+ else |
+ labels_ = new StringSet; |
+ if (r.senders_) |
+ senders_ = new MailSenderList(*r.senders_); |
+ else |
+ senders_ = new MailSenderList; |
+ } |
+ AssertValid(); |
+ return *this; |
+} |
+ |
+MessageThread* MessageThread::CreateFromXML( |
+ const buzz::XmlElement* src) { |
+ MessageThread* info = new MessageThread(); |
+ if (!info || !info->InitFromXml(src)) { |
+ delete info; |
+ return NULL; |
+ } |
+ return info; |
+} |
+ |
+// Init from a chunk of XML |
+bool MessageThread::InitFromXml(const buzz::XmlElement* src) { |
+ labels_ = new StringSet; |
+ senders_ = new MailSenderList; |
+ |
+ if (src->Name() != buzz::kQnMailThreadInfo) |
+ return false; |
+ |
+ if (!ParseInt64Attr(src, buzz::kQnMailTid, &thread_id_)) |
+ return false; |
+ if (!ParseInt64Attr(src, buzz::kQnMailDate, &date64_)) |
+ return false; |
+ if (!ParseIntAttr(src, buzz::kQnMailMessages, &message_count_)) |
+ return false; |
+ if (!ParseIntAttr(src, buzz::kQnMailParticipation, &personal_level_)) |
+ return false; |
+ |
+ const buzz::XmlElement* senders = src->FirstNamed(buzz::kQnMailSenders); |
+ if (!senders) |
+ return false; |
+ for (const buzz::XmlElement* child = senders->FirstElement(); |
+ child != NULL; |
+ child = child->NextElement()) { |
+ if (child->Name() != buzz::kQnMailSender) |
+ continue; |
+ std::string address; |
+ if (!ParseStringAttr(child, buzz::kQnMailAddress, &address)) |
+ continue; |
+ std::string name; |
+ ParseStringAttr(child, buzz::kQnMailName, &name); |
+ bool originator = false; |
+ ParseBoolAttr(child, buzz::kQnMailOriginator, &originator); |
+ bool unread = false; |
+ ParseBoolAttr(child, buzz::kQnMailUnread, &unread); |
+ |
+ senders_->push_back(MailSender(name, address, unread, originator)); |
+ } |
+ |
+ const buzz::XmlElement* labels = src->FirstNamed(buzz::kQnMailLabels); |
+ if (!labels) |
+ return false; |
+ ParseLabelSet(labels->BodyText(), labels_); |
+ |
+ const buzz::XmlElement* subject = src->FirstNamed(buzz::kQnMailSubject); |
+ if (!subject) |
+ return false; |
+ subject_ = subject->BodyText(); |
+ |
+ const buzz::XmlElement* snippet = src->FirstNamed(buzz::kQnMailSnippet); |
+ if (!snippet) |
+ return false; |
+ snippet_ = snippet->BodyText(); |
+ |
+ AssertValid(); |
+ return true; |
+} |
+ |
+bool MessageThread::starred() const { |
+ return (labels_->find("^t") != labels_->end()); |
+} |
+ |
+bool MessageThread::unread() const { |
+ return (labels_->find("^u") != labels_->end()); |
+} |
+ |
+#ifdef _DEBUG |
+// non-debug version is inline and empty |
+void MessageThread::AssertValid() const { |
+ assert(thread_id_ != 0); |
+ assert(senders_ != NULL); |
+ // In some (odd) cases, gmail may send email with no sender. |
+ // assert(!senders_->empty()); |
+ assert(message_count_ > 0); |
+ assert(labels_ != NULL); |
+} |
+#endif |
+ |
+ |
+ |
+MailBox* MailBox::CreateFromXML(const buzz::XmlElement* src) { |
+ MailBox* mail_box = new MailBox(); |
+ if (!mail_box || !mail_box->InitFromXml(src)) { |
+ delete mail_box; |
+ return NULL; |
+ } |
+ return mail_box; |
+} |
+ |
+// Init from a chunk of XML |
+bool MailBox::InitFromXml(const buzz::XmlElement* src) |
+{ |
+ if (src->Name() != buzz::kQnMailBox) |
+ return false; |
+ |
+ if (!ParseIntAttr(src, buzz::kQnMailTotalMatched, &mailbox_size_)) |
+ return false; |
+ |
+ estimate_ = false; |
+ ParseBoolAttr(src, buzz::kQnMailTotalEstimate, &estimate_); |
+ |
+ first_index_ = 0; |
+ ParseIntAttr(src, buzz::kQnMailFirstIndex, &first_index_); |
+ |
+ result_time_ = 0; |
+ ParseInt64Attr(src, buzz::kQnMailResultTime, &result_time_); |
+ |
+ highest_thread_id_ = 0; |
+ |
+ const buzz::XmlElement* thread_element = src->FirstNamed(buzz::kQnMailThreadInfo); |
+ while (thread_element) { |
+ MessageThread* thread = MessageThread::CreateFromXML(thread_element); |
+ if (thread) { |
+ if (thread->thread_id() > highest_thread_id_) |
+ highest_thread_id_ = thread->thread_id(); |
+ threads_.push_back(MessageThreadPointer(thread)); |
+ } |
+ thread_element = thread_element->NextNamed(buzz::kQnMailThreadInfo); |
+ } |
+ return true; |
+} |
+ |
+const size_t kMaxShortnameLength = 12; |
+ |
+// Tip: If you extend this list of chars, do not include '-' |
+const char name_delim[] = " ,.:;\'\"()[]{}<>*@"; |
+ |
+class SenderFormatter { |
+ public: |
+ // sender should not be deleted while this class is in use. |
+ SenderFormatter(const MailSender& sender, |
+ const std::string& me_address) |
+ : sender_(sender), |
+ visible_(false), |
+ short_format_(true), |
+ space_(kMaxShortnameLength) { |
+ me_ = talk_base::ascicmp(me_address.c_str(), |
+ sender.address().c_str()) == 0; |
+ } |
+ |
+ bool visible() const { |
+ return visible_; |
+ } |
+ |
+ bool is_unread() const { |
+ return sender_.unread(); |
+ } |
+ |
+ const std::string name() const { |
+ return name_; |
+ } |
+ |
+ void set_short_format(bool short_format) { |
+ short_format_ = short_format; |
+ UpdateName(); |
+ } |
+ |
+ void set_visible(bool visible) { |
+ visible_ = visible; |
+ UpdateName(); |
+ } |
+ |
+ void set_space(size_t space) { |
+ space_ = space; |
+ UpdateName(); |
+ } |
+ |
+ private: |
+ // Attempt to shorten to the first word in a person's name |
+ // We could revisit and do better at international punctuation, |
+ // but this is what cricket did, and it should be removed |
+ // soon when gmail does the notification instead of us |
+ // forming it on the client. |
+ static void ShortenName(std::string* name) { |
+ size_t start = name->find_first_not_of(name_delim); |
+ if (start != std::string::npos && start > 0) { |
+ name->erase(0, start); |
+ } |
+ start = name->find_first_of(name_delim); |
+ if (start != std::string::npos) { |
+ name->erase(start); |
+ } |
+ } |
+ |
+ void UpdateName() { |
+ // Update the name if is going to be used. |
+ if (!visible_) { |
+ return; |
+ } |
+ |
+ if (me_) { |
+ name_ = "me"; |
+ return; |
+ } |
+ |
+ if (sender_.name().empty() && sender_.address().empty()) { |
+ name_ = ""; |
+ return; |
+ } |
+ |
+ name_ = sender_.name(); |
+ // Handle the case of no name or a name looks like an email address. |
+ // When mail is sent to "Quality@example.com" <quality-team@example.com>, |
+ // we shouldn't show "Quality@example.com" as the name. |
+ // Instead use the email address (without the @...) |
+ if (name_.empty() || name_.find_first_of("@") != std::string::npos) { |
+ name_ = sender_.address(); |
+ size_t at_index = name_.find_first_of("@"); |
+ if (at_index != std::string::npos) { |
+ name_.erase(at_index); |
+ } |
+ } else if (short_format_) { |
+ ShortenName(&name_); |
+ } |
+ |
+ if (name_.empty()) { |
+ name_ = "(unknown)"; |
+ } |
+ |
+ // Abbreviate if too long. |
+ if (name_.size() > space_) { |
+ name_.replace(space_ - 1, std::string::npos, "."); |
+ } |
+ } |
+ |
+ const MailSender& sender_; |
+ std::string name_; |
+ bool visible_; |
+ bool short_format_; |
+ size_t space_; |
+ bool me_; |
+ DISALLOW_COPY_AND_ASSIGN(SenderFormatter); |
+}; |
+ |
+const char kNormalSeparator[] = ", "; |
+const char kEllidedSeparator[] = " .."; |
+ |
+std::string FormatName(const std::string& name, bool bold) { |
+ std::string formatted_name; |
+ if (bold) { |
+ formatted_name.append("<b>"); |
+ } |
+ formatted_name.append(HtmlEncode(name)); |
+ if (bold) { |
+ formatted_name.append("</b>"); |
+ } |
+ return formatted_name; |
+} |
+ |
+class SenderFormatterList { |
+ public: |
+ // sender_list must not be deleted while this class is being used. |
+ SenderFormatterList(const MailSenderList& sender_list, |
+ const std::string& me_address) |
+ : state_(INITIAL_STATE), |
+ are_any_read_(false), |
+ index_(-1), |
+ first_unread_index_(-1) { |
+ // Add all read messages. |
+ const MailSender* originator = NULL; |
+ bool any_unread = false; |
+ for (size_t i = 0; i < sender_list.size(); ++i) { |
+ if (sender_list[i].originator()) { |
+ originator = &sender_list[i]; |
+ } |
+ if (sender_list[i].unread()) { |
+ any_unread = true; |
+ continue; |
+ } |
+ are_any_read_ = true; |
+ if (!sender_list[i].originator()) { |
+ senders_.push_back(new SenderFormatter(sender_list[i], |
+ me_address)); |
+ } |
+ } |
+ |
+ // There may not be an orignator (if there no senders). |
+ if (originator) { |
+ senders_.insert(senders_.begin(), new SenderFormatter(*originator, |
+ me_address)); |
+ } |
+ |
+ // Add all unread messages. |
+ if (any_unread) { |
+ // It should be rare, but there may be cases when all of the |
+ // senders appear to have read the message. |
+ first_unread_index_ = is_first_unread() ? 0 : senders_.size(); |
+ for (size_t i = 0; i < sender_list.size(); ++i) { |
+ if (!sender_list[i].unread()) { |
+ continue; |
+ } |
+ // Don't add the originator if it is already at the |
+ // start of the "unread" list. |
+ if (sender_list[i].originator() && is_first_unread()) { |
+ continue; |
+ } |
+ senders_.push_back(new SenderFormatter(sender_list[i], |
+ me_address)); |
+ } |
+ } |
+ } |
+ |
+ ~SenderFormatterList() { |
+ CleanupSequence(&senders_); |
+ } |
+ |
+ std::string GetHtml(int space) { |
+ index_ = -1; |
+ state_ = INITIAL_STATE; |
+ while (!added_.empty()) { |
+ added_.pop(); |
+ } |
+ |
+ // If there is only one sender, let it take up all of the space. |
+ if (senders_.size() == 1) { |
+ senders_[0]->set_space(space); |
+ senders_[0]->set_short_format(false); |
+ } |
+ |
+ int length = 1; |
+ // Add as many senders as we can in the given space. |
+ // Computes the visible length at each iteration, |
+ // but does not construct the actual html. |
+ while (length < space && AddNextSender()) { |
+ int new_length = ConstructHtml(is_first_unread(), NULL); |
+ // Remove names to avoid truncating |
+ // * if there will be at least 2 left or |
+ // * if the spacing <= 2 characters per sender and there |
+ // is at least one left. |
+ if ((new_length > space && |
+ (visible_count() > 2 || |
+ (ComputeSpacePerSender(space) <= 2 && visible_count() > 1)))) { |
+ RemoveLastAddedSender(); |
+ break; |
+ } |
+ length = new_length; |
+ } |
+ |
+ if (length > space) { |
+ int max = ComputeSpacePerSender(space); |
+ for (size_t i = 0; i < senders_.size(); ++i) { |
+ if (senders_[i]->visible()) { |
+ senders_[i]->set_space(max); |
+ } |
+ } |
+ } |
+ |
+ // Now construct the actual html |
+ std::string html_list; |
+ length = ConstructHtml(is_first_unread(), &html_list); |
+ if (length > space) { |
+ LOG(LS_WARNING) << "LENGTH: " << length << " exceeds " |
+ << space << " " << html_list; |
+ } |
+ return html_list; |
+ } |
+ |
+ private: |
+ int ComputeSpacePerSender(int space) const { |
+ // Why the "- 2"? To allow for the " .. " which may occur |
+ // after the names, and no matter what always allow at least |
+ // 2 characters per sender. |
+ return talk_base::_max<int>(space / visible_count() - 2, 2); |
+ } |
+ |
+ // Finds the next sender that should be added to the "from" list |
+ // and sets it to visible. |
+ // |
+ // This method may be called until it returns false or |
+ // until RemoveLastAddedSender is called. |
+ bool AddNextSender() { |
+ // The progression is: |
+ // 1. Add the person who started the thread, which is the first message. |
+ // 2. Add the first unread message (unless it has already been added). |
+ // 3. Add the last message (unless it has already been added). |
+ // 4. Add the message that is just before the last message processed |
+ // (unless it has already been added). |
+ // If there is no message (i.e. at index -1), return false. |
+ // |
+ // Typically, this method is called until it returns false or |
+ // all of the space available is used. |
+ switch (state_) { |
+ case INITIAL_STATE: |
+ state_ = FIRST_MESSAGE; |
+ index_ = 0; |
+ // If the server behaves odd and doesn't send us any senders, |
+ // do something graceful. |
+ if (senders_.size() == 0) { |
+ return false; |
+ } |
+ break; |
+ |
+ case FIRST_MESSAGE: |
+ if (first_unread_index_ >= 0) { |
+ state_ = FIRST_UNREAD_MESSAGE; |
+ index_ = first_unread_index_; |
+ break; |
+ } |
+ // fall through |
+ case FIRST_UNREAD_MESSAGE: |
+ state_ = LAST_MESSAGE; |
+ index_ = senders_.size() - 1; |
+ break; |
+ |
+ case LAST_MESSAGE: |
+ case PREVIOUS_MESSAGE: |
+ state_ = PREVIOUS_MESSAGE; |
+ index_--; |
+ break; |
+ |
+ case REMOVED_MESSAGE: |
+ default: |
+ ASSERT(false); |
+ return false; |
+ } |
+ |
+ if (index_ < 0) { |
+ return false; |
+ } |
+ |
+ if (!senders_[index_]->visible()) { |
+ added_.push(index_); |
+ senders_[index_]->set_visible(true); |
+ } |
+ return true; |
+ } |
+ |
+ // Makes the last added sender not visible. |
+ // |
+ // May be called while visible_count() > 0. |
+ void RemoveLastAddedSender() { |
+ state_ = REMOVED_MESSAGE; |
+ int index = added_.top(); |
+ added_.pop(); |
+ senders_[index]->set_visible(false); |
+ } |
+ |
+ // Constructs the html of the SenderList and returns the length of the |
+ // visible text. |
+ // |
+ // The algorithm simply walks down the list of Senders, appending |
+ // the html for each visible sender, and adding ellipsis or commas |
+ // in between, whichever is appropriate. |
+ // |
+ // html Filled with html. Maybe NULL if the html doesn't |
+ // need to be constructed yet (useful for simply |
+ // determining the length of the visible text). |
+ // |
+ // returns The approximate visible length of the html. |
+ int ConstructHtml(bool first_is_unread, |
+ std::string* html) const { |
+ if (senders_.empty()) { |
+ return 0; |
+ } |
+ |
+ int length = 0; |
+ |
+ // The first is always visible |
+ const SenderFormatter* sender = senders_[0]; |
+ const std::string& originator_name = sender->name(); |
+ length += originator_name.length(); |
+ if (html) { |
+ html->append(FormatName(originator_name, first_is_unread)); |
+ } |
+ |
+ bool elided = false; |
+ const char* between = ""; |
+ for (size_t i = 1; i < senders_.size(); i++) { |
+ sender = senders_[i]; |
+ |
+ if (sender->visible()) { |
+ // Handle the separator |
+ between = elided ? " " : kNormalSeparator; |
+ // Ignore the , for length because it is so narrow, |
+ // so in both cases above the space is the only things |
+ // that counts for spaces. |
+ length++; |
+ |
+ // Handle the name |
+ const std::string name = sender->name(); |
+ length += name.size(); |
+ |
+ // Construct the html |
+ if (html) { |
+ html->append(between); |
+ html->append(FormatName(name, sender->is_unread())); |
+ } |
+ elided = false; |
+ } else if (!elided) { |
+ between = kEllidedSeparator; |
+ length += 2; // ".." is narrow |
+ if (html) { |
+ html->append(between); |
+ } |
+ elided = true; |
+ } |
+ } |
+ return length; |
+ } |
+ |
+ bool is_first_unread() const { |
+ return !are_any_read_; |
+ } |
+ |
+ size_t visible_count() const { |
+ return added_.size(); |
+ } |
+ |
+ enum MessageState { |
+ INITIAL_STATE, |
+ FIRST_MESSAGE, |
+ FIRST_UNREAD_MESSAGE, |
+ LAST_MESSAGE, |
+ PREVIOUS_MESSAGE, |
+ REMOVED_MESSAGE, |
+ }; |
+ |
+ // What state we were in during the last "stateful" function call. |
+ MessageState state_; |
+ bool are_any_read_; |
+ std::vector<SenderFormatter*> senders_; |
+ std::stack<int> added_; |
+ int index_; |
+ int first_unread_index_; |
+ DISALLOW_COPY_AND_ASSIGN(SenderFormatterList); |
+}; |
+ |
+ |
+std::string GetSenderHtml(const MailSenderList& sender_list, |
+ int message_count, |
+ const std::string& me_address, |
+ int space) { |
+ // There has to be at least 9 spaces to show something reasonable. |
+ ASSERT(space >= 10); |
+ std::string count_html; |
+ if (message_count > 1) { |
+ std::string count(IntToString(message_count)); |
+ space -= (count.size()); |
+ count_html.append(" ("); |
+ count_html.append(count); |
+ count_html.append(")"); |
+ // Reduce space due to parenthesis and . |
+ space -= 2; |
+ } |
+ |
+ SenderFormatterList senders(sender_list, me_address); |
+ std::string html_list(senders.GetHtml(space)); |
+ html_list.append(count_html); |
+ return html_list; |
+} |
+} // namespace notifier |
Property changes on: chrome\browser\sync\notifier\communicator\mailbox.cc |
___________________________________________________________________ |
Added: svn:eol-style |
+ LF |