// 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 #include #include #include #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()); } #if defined(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" , 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(""); } formatted_name.append(HtmlEncode(name)); if (bold) { formatted_name.append(""); } 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(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 senders_; std::stack 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