diff options
Diffstat (limited to 'chrome/browser/sync/notifier/communicator/mailbox.cc')
-rw-r--r-- | chrome/browser/sync/notifier/communicator/mailbox.cc | 682 |
1 files changed, 682 insertions, 0 deletions
diff --git a/chrome/browser/sync/notifier/communicator/mailbox.cc b/chrome/browser/sync/notifier/communicator/mailbox.cc new file mode 100644 index 0000000..22b6690 --- /dev/null +++ b/chrome/browser/sync/notifier/communicator/mailbox.cc @@ -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 |