OLD | NEW |
| (Empty) |
1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "chrome/browser/sync/notifier/communicator/mailbox.h" | |
6 | |
7 #include <assert.h> | |
8 #include <stdlib.h> | |
9 | |
10 #include <stack> | |
11 #include <vector> | |
12 | |
13 #include "chrome/browser/sync/notifier/base/string.h" | |
14 #include "chrome/browser/sync/notifier/base/utils.h" | |
15 #include "chrome/browser/sync/notifier/communicator/xml_parse_helpers.h" | |
16 #include "talk/base/basictypes.h" | |
17 #include "talk/base/common.h" | |
18 #include "talk/base/stringutils.h" | |
19 #include "talk/xmllite/xmlelement.h" | |
20 #include "talk/xmpp/constants.h" | |
21 | |
22 namespace notifier { | |
23 | |
24 // Labels are a list of strings seperated by a '|' character. The '|' character | |
25 // is escaped with a backslash ('\\') and the backslash is also escaped with a | |
26 // backslash. | |
27 static void ParseLabelSet(const std::string& text, | |
28 MessageThread::StringSet* labels) { | |
29 const char* input_cur = text.c_str(); | |
30 const char* input_end = input_cur + text.size(); | |
31 char* result = new char[text.size() + 1]; | |
32 char* next_write = result; | |
33 | |
34 while (input_cur < input_end) { | |
35 if (*input_cur == '|') { | |
36 if (next_write != result) { | |
37 *next_write = '\0'; | |
38 labels->insert(std::string(result)); | |
39 next_write = result; | |
40 } | |
41 input_cur++; | |
42 continue; | |
43 } | |
44 | |
45 if (*input_cur == '\\') { | |
46 // Skip a character in the input and break if we are at the end. | |
47 input_cur++; | |
48 if (input_cur >= input_end) | |
49 break; | |
50 } | |
51 *next_write = *input_cur; | |
52 next_write++; | |
53 input_cur++; | |
54 } | |
55 | |
56 if (next_write != result) { | |
57 *next_write = '\0'; | |
58 labels->insert(std::string(result)); | |
59 } | |
60 | |
61 delete [] result; | |
62 } | |
63 | |
64 // ----------------------------------------------------------------------------- | |
65 | |
66 std::string MailAddress::safe_name() const { | |
67 if (!name().empty()) { | |
68 return name(); | |
69 } | |
70 | |
71 if (!address().empty()) { | |
72 size_t at = address().find('@'); | |
73 if (at == std::string::npos) { | |
74 return address(); | |
75 } | |
76 | |
77 if (at != 0) { | |
78 return address().substr(0, at); | |
79 } | |
80 } | |
81 | |
82 return std::string("(unknown)"); | |
83 } | |
84 | |
85 // ----------------------------------------------------------------------------- | |
86 MessageThread::~MessageThread() { | |
87 Clear(); | |
88 } | |
89 | |
90 void MessageThread::Clear() { | |
91 delete labels_; | |
92 labels_ = NULL; | |
93 | |
94 delete senders_; | |
95 senders_ = NULL; | |
96 } | |
97 | |
98 MessageThread& MessageThread::operator=(const MessageThread& r) { | |
99 if (&r != this) { | |
100 Clear(); | |
101 // Copy everything. | |
102 r.AssertValid(); | |
103 thread_id_ = r.thread_id_; | |
104 date64_ = r.date64_; | |
105 message_count_ = r.message_count_; | |
106 personal_level_ = r.personal_level_; | |
107 subject_ = r.subject_; | |
108 snippet_ = r.snippet_; | |
109 | |
110 if (r.labels_) | |
111 labels_ = new StringSet(*r.labels_); | |
112 else | |
113 labels_ = new StringSet; | |
114 if (r.senders_) | |
115 senders_ = new MailSenderList(*r.senders_); | |
116 else | |
117 senders_ = new MailSenderList; | |
118 } | |
119 AssertValid(); | |
120 return *this; | |
121 } | |
122 | |
123 MessageThread* MessageThread::CreateFromXML( | |
124 const buzz::XmlElement* src) { | |
125 MessageThread* info = new MessageThread(); | |
126 if (!info || !info->InitFromXml(src)) { | |
127 delete info; | |
128 return NULL; | |
129 } | |
130 return info; | |
131 } | |
132 | |
133 // Init from a chunk of XML. | |
134 bool MessageThread::InitFromXml(const buzz::XmlElement* src) { | |
135 labels_ = new StringSet; | |
136 senders_ = new MailSenderList; | |
137 | |
138 if (src->Name() != buzz::kQnMailThreadInfo) | |
139 return false; | |
140 | |
141 if (!ParseInt64Attr(src, buzz::kQnMailTid, &thread_id_)) | |
142 return false; | |
143 if (!ParseInt64Attr(src, buzz::kQnMailDate, &date64_)) | |
144 return false; | |
145 if (!ParseIntAttr(src, buzz::kQnMailMessages, &message_count_)) | |
146 return false; | |
147 if (!ParseIntAttr(src, buzz::kQnMailParticipation, &personal_level_)) | |
148 return false; | |
149 | |
150 const buzz::XmlElement* senders = src->FirstNamed(buzz::kQnMailSenders); | |
151 if (!senders) | |
152 return false; | |
153 for (const buzz::XmlElement* child = senders->FirstElement(); | |
154 child != NULL; | |
155 child = child->NextElement()) { | |
156 if (child->Name() != buzz::kQnMailSender) | |
157 continue; | |
158 std::string address; | |
159 if (!ParseStringAttr(child, buzz::kQnMailAddress, &address)) | |
160 continue; | |
161 std::string name; | |
162 ParseStringAttr(child, buzz::kQnMailName, &name); | |
163 bool originator = false; | |
164 ParseBoolAttr(child, buzz::kQnMailOriginator, &originator); | |
165 bool unread = false; | |
166 ParseBoolAttr(child, buzz::kQnMailUnread, &unread); | |
167 | |
168 senders_->push_back(MailSender(name, address, unread, originator)); | |
169 } | |
170 | |
171 const buzz::XmlElement* labels = src->FirstNamed(buzz::kQnMailLabels); | |
172 if (!labels) | |
173 return false; | |
174 ParseLabelSet(labels->BodyText(), labels_); | |
175 | |
176 const buzz::XmlElement* subject = src->FirstNamed(buzz::kQnMailSubject); | |
177 if (!subject) | |
178 return false; | |
179 subject_ = subject->BodyText(); | |
180 | |
181 const buzz::XmlElement* snippet = src->FirstNamed(buzz::kQnMailSnippet); | |
182 if (!snippet) | |
183 return false; | |
184 snippet_ = snippet->BodyText(); | |
185 | |
186 AssertValid(); | |
187 return true; | |
188 } | |
189 | |
190 bool MessageThread::starred() const { | |
191 return (labels_->find("^t") != labels_->end()); | |
192 } | |
193 | |
194 bool MessageThread::unread() const { | |
195 return (labels_->find("^u") != labels_->end()); | |
196 } | |
197 | |
198 #if defined(DEBUG) | |
199 // Non-debug version is inline and empty. | |
200 void MessageThread::AssertValid() const { | |
201 assert(thread_id_ != 0); | |
202 assert(senders_ != NULL); | |
203 // In some (odd) cases, gmail may send email with no sender. | |
204 // assert(!senders_->empty()); | |
205 assert(message_count_ > 0); | |
206 assert(labels_ != NULL); | |
207 } | |
208 #endif | |
209 | |
210 MailBox* MailBox::CreateFromXML(const buzz::XmlElement* src) { | |
211 MailBox* mail_box = new MailBox(); | |
212 if (!mail_box || !mail_box->InitFromXml(src)) { | |
213 delete mail_box; | |
214 return NULL; | |
215 } | |
216 return mail_box; | |
217 } | |
218 | |
219 // Init from a chunk of XML. | |
220 bool MailBox::InitFromXml(const buzz::XmlElement* src) { | |
221 if (src->Name() != buzz::kQnMailBox) | |
222 return false; | |
223 | |
224 if (!ParseIntAttr(src, buzz::kQnMailTotalMatched, &mailbox_size_)) | |
225 return false; | |
226 | |
227 estimate_ = false; | |
228 ParseBoolAttr(src, buzz::kQnMailTotalEstimate, &estimate_); | |
229 | |
230 first_index_ = 0; | |
231 ParseIntAttr(src, buzz::kQnMailFirstIndex, &first_index_); | |
232 | |
233 result_time_ = 0; | |
234 ParseInt64Attr(src, buzz::kQnMailResultTime, &result_time_); | |
235 | |
236 highest_thread_id_ = 0; | |
237 | |
238 const buzz::XmlElement* thread_element = src->FirstNamed(buzz::kQnMailThreadIn
fo); | |
239 while (thread_element) { | |
240 MessageThread* thread = MessageThread::CreateFromXML(thread_element); | |
241 if (thread) { | |
242 if (thread->thread_id() > highest_thread_id_) | |
243 highest_thread_id_ = thread->thread_id(); | |
244 threads_.push_back(MessageThreadPointer(thread)); | |
245 } | |
246 thread_element = thread_element->NextNamed(buzz::kQnMailThreadInfo); | |
247 } | |
248 return true; | |
249 } | |
250 | |
251 const size_t kMaxShortnameLength = 12; | |
252 | |
253 // Tip: If you extend this list of chars, do not include '-'. | |
254 const char name_delim[] = " ,.:;\'\"()[]{}<>*@"; | |
255 | |
256 class SenderFormatter { | |
257 public: | |
258 // sender should not be deleted while this class is in use. | |
259 SenderFormatter(const MailSender& sender, | |
260 const std::string& me_address) | |
261 : sender_(sender), | |
262 visible_(false), | |
263 short_format_(true), | |
264 space_(kMaxShortnameLength) { | |
265 me_ = talk_base::ascicmp(me_address.c_str(), | |
266 sender.address().c_str()) == 0; | |
267 } | |
268 | |
269 bool visible() const { | |
270 return visible_; | |
271 } | |
272 | |
273 bool is_unread() const { | |
274 return sender_.unread(); | |
275 } | |
276 | |
277 const std::string name() const { | |
278 return name_; | |
279 } | |
280 | |
281 void set_short_format(bool short_format) { | |
282 short_format_ = short_format; | |
283 UpdateName(); | |
284 } | |
285 | |
286 void set_visible(bool visible) { | |
287 visible_ = visible; | |
288 UpdateName(); | |
289 } | |
290 | |
291 void set_space(size_t space) { | |
292 space_ = space; | |
293 UpdateName(); | |
294 } | |
295 | |
296 private: | |
297 // Attempt to shorten to the first word in a person's name We could revisit | |
298 // and do better at international punctuation, but this is what cricket did, | |
299 // and it should be removed soon when gmail does the notification instead of | |
300 // us forming it on the client. | |
301 static void ShortenName(std::string* name) { | |
302 size_t start = name->find_first_not_of(name_delim); | |
303 if (start != std::string::npos && start > 0) { | |
304 name->erase(0, start); | |
305 } | |
306 start = name->find_first_of(name_delim); | |
307 if (start != std::string::npos) { | |
308 name->erase(start); | |
309 } | |
310 } | |
311 | |
312 void UpdateName() { | |
313 // Update the name if is going to be used. | |
314 if (!visible_) { | |
315 return; | |
316 } | |
317 | |
318 if (me_) { | |
319 name_ = "me"; | |
320 return; | |
321 } | |
322 | |
323 if (sender_.name().empty() && sender_.address().empty()) { | |
324 name_ = ""; | |
325 return; | |
326 } | |
327 | |
328 name_ = sender_.name(); | |
329 // Handle the case of no name or a name looks like an email address. When | |
330 // mail is sent to "Quality@example.com" <quality-team@example.com>, we | |
331 // shouldn't show "Quality@example.com" as the name. Instead, use the email | |
332 // address (without the @...) | |
333 if (name_.empty() || name_.find_first_of("@") != std::string::npos) { | |
334 name_ = sender_.address(); | |
335 size_t at_index = name_.find_first_of("@"); | |
336 if (at_index != std::string::npos) { | |
337 name_.erase(at_index); | |
338 } | |
339 } else if (short_format_) { | |
340 ShortenName(&name_); | |
341 } | |
342 | |
343 if (name_.empty()) { | |
344 name_ = "(unknown)"; | |
345 } | |
346 | |
347 // Abbreviate if too long. | |
348 if (name_.size() > space_) { | |
349 name_.replace(space_ - 1, std::string::npos, "."); | |
350 } | |
351 } | |
352 | |
353 const MailSender& sender_; | |
354 std::string name_; | |
355 bool visible_; | |
356 bool short_format_; | |
357 size_t space_; | |
358 bool me_; | |
359 DISALLOW_COPY_AND_ASSIGN(SenderFormatter); | |
360 }; | |
361 | |
362 const char kNormalSeparator[] = ", "; | |
363 const char kEllidedSeparator[] = " .."; | |
364 | |
365 std::string FormatName(const std::string& name, bool bold) { | |
366 std::string formatted_name; | |
367 if (bold) { | |
368 formatted_name.append("<b>"); | |
369 } | |
370 formatted_name.append(HtmlEncode(name)); | |
371 if (bold) { | |
372 formatted_name.append("</b>"); | |
373 } | |
374 return formatted_name; | |
375 } | |
376 | |
377 class SenderFormatterList { | |
378 public: | |
379 // sender_list must not be deleted while this class is being used. | |
380 SenderFormatterList(const MailSenderList& sender_list, | |
381 const std::string& me_address) | |
382 : state_(INITIAL_STATE), | |
383 are_any_read_(false), | |
384 index_(-1), | |
385 first_unread_index_(-1) { | |
386 // Add all read messages. | |
387 const MailSender* originator = NULL; | |
388 bool any_unread = false; | |
389 for (size_t i = 0; i < sender_list.size(); ++i) { | |
390 if (sender_list[i].originator()) { | |
391 originator = &sender_list[i]; | |
392 } | |
393 if (sender_list[i].unread()) { | |
394 any_unread = true; | |
395 continue; | |
396 } | |
397 are_any_read_ = true; | |
398 if (!sender_list[i].originator()) { | |
399 senders_.push_back(new SenderFormatter(sender_list[i], | |
400 me_address)); | |
401 } | |
402 } | |
403 | |
404 // There may not be an orignator (if there no senders). | |
405 if (originator) { | |
406 senders_.insert(senders_.begin(), new SenderFormatter(*originator, | |
407 me_address)); | |
408 } | |
409 | |
410 // Add all unread messages. | |
411 if (any_unread) { | |
412 // It should be rare, but there may be cases when all of the senders | |
413 // appear to have read the message. | |
414 first_unread_index_ = is_first_unread() ? 0 : senders_.size(); | |
415 for (size_t i = 0; i < sender_list.size(); ++i) { | |
416 if (!sender_list[i].unread()) { | |
417 continue; | |
418 } | |
419 // Don't add the originator if it is already at the start of the | |
420 // "unread" list. | |
421 if (sender_list[i].originator() && is_first_unread()) { | |
422 continue; | |
423 } | |
424 senders_.push_back(new SenderFormatter(sender_list[i], me_address)); | |
425 } | |
426 } | |
427 } | |
428 | |
429 ~SenderFormatterList() { | |
430 CleanupSequence(&senders_); | |
431 } | |
432 | |
433 std::string GetHtml(int space) { | |
434 index_ = -1; | |
435 state_ = INITIAL_STATE; | |
436 while (!added_.empty()) { | |
437 added_.pop(); | |
438 } | |
439 | |
440 // If there is only one sender, let it take up all of the space. | |
441 if (senders_.size() == 1) { | |
442 senders_[0]->set_space(space); | |
443 senders_[0]->set_short_format(false); | |
444 } | |
445 | |
446 int length = 1; | |
447 // Add as many senders as we can in the given space. Computes the visible | |
448 // length at each iteration, but does not construct the actual html. | |
449 while (length < space && AddNextSender()) { | |
450 int new_length = ConstructHtml(is_first_unread(), NULL); | |
451 // Remove names to avoid truncating | |
452 // * if there will be at least 2 left or | |
453 // * if the spacing <= 2 characters per sender and there | |
454 // is at least one left. | |
455 if ((new_length > space && | |
456 (visible_count() > 2 || | |
457 (ComputeSpacePerSender(space) <= 2 && visible_count() > 1)))) { | |
458 RemoveLastAddedSender(); | |
459 break; | |
460 } | |
461 length = new_length; | |
462 } | |
463 | |
464 if (length > space) { | |
465 int max = ComputeSpacePerSender(space); | |
466 for (size_t i = 0; i < senders_.size(); ++i) { | |
467 if (senders_[i]->visible()) { | |
468 senders_[i]->set_space(max); | |
469 } | |
470 } | |
471 } | |
472 | |
473 // Now construct the actual html. | |
474 std::string html_list; | |
475 length = ConstructHtml(is_first_unread(), &html_list); | |
476 if (length > space) { | |
477 LOG(LS_WARNING) << "LENGTH: " << length << " exceeds " | |
478 << space << " " << html_list; | |
479 } | |
480 return html_list; | |
481 } | |
482 | |
483 private: | |
484 int ComputeSpacePerSender(int space) const { | |
485 // Why the "- 2"? To allow for the " .. " which may occur after the names, | |
486 // and no matter what always allow at least 2 characters per sender. | |
487 return talk_base::_max<int>(space / visible_count() - 2, 2); | |
488 } | |
489 | |
490 // Finds the next sender that should be added to the "from" list and sets it | |
491 // to visible. | |
492 // | |
493 // This method may be called until it returns false or until | |
494 // RemoveLastAddedSender is called. | |
495 bool AddNextSender() { | |
496 // The progression is: | |
497 // 1. Add the person who started the thread, which is the first message. | |
498 // 2. Add the first unread message (unless it has already been added). | |
499 // 3. Add the last message (unless it has already been added). | |
500 // 4. Add the message that is just before the last message processed | |
501 // (unless it has already been added). | |
502 // If there is no message (i.e. at index -1), return false. | |
503 // | |
504 // Typically, this method is called until it returns false or all of the | |
505 // space available is used. | |
506 switch (state_) { | |
507 case INITIAL_STATE: | |
508 state_ = FIRST_MESSAGE; | |
509 index_ = 0; | |
510 // If the server behaves odd and doesn't send us any senders, do | |
511 // something graceful. | |
512 if (senders_.size() == 0) { | |
513 return false; | |
514 } | |
515 break; | |
516 | |
517 case FIRST_MESSAGE: | |
518 if (first_unread_index_ >= 0) { | |
519 state_ = FIRST_UNREAD_MESSAGE; | |
520 index_ = first_unread_index_; | |
521 break; | |
522 } | |
523 // Fall through. | |
524 case FIRST_UNREAD_MESSAGE: | |
525 state_ = LAST_MESSAGE; | |
526 index_ = senders_.size() - 1; | |
527 break; | |
528 | |
529 case LAST_MESSAGE: | |
530 case PREVIOUS_MESSAGE: | |
531 state_ = PREVIOUS_MESSAGE; | |
532 index_--; | |
533 break; | |
534 | |
535 case REMOVED_MESSAGE: | |
536 default: | |
537 ASSERT(false); | |
538 return false; | |
539 } | |
540 | |
541 if (index_ < 0) { | |
542 return false; | |
543 } | |
544 | |
545 if (!senders_[index_]->visible()) { | |
546 added_.push(index_); | |
547 senders_[index_]->set_visible(true); | |
548 } | |
549 return true; | |
550 } | |
551 | |
552 // Makes the last added sender not visible. | |
553 // | |
554 // May be called while visible_count() > 0. | |
555 void RemoveLastAddedSender() { | |
556 state_ = REMOVED_MESSAGE; | |
557 int index = added_.top(); | |
558 added_.pop(); | |
559 senders_[index]->set_visible(false); | |
560 } | |
561 | |
562 // Constructs the html of the SenderList and returns the length of the | |
563 // visible text. | |
564 // | |
565 // The algorithm simply walks down the list of Senders, appending the html | |
566 // for each visible sender, and adding ellipsis or commas in between, | |
567 // whichever is appropriate. | |
568 // | |
569 // html Filled with html. Maybe NULL if the html doesn't need to be | |
570 // constructed yet (useful for simply determining the length of the | |
571 // visible text). | |
572 // | |
573 // returns The approximate visible length of the html. | |
574 int ConstructHtml(bool first_is_unread, | |
575 std::string* html) const { | |
576 if (senders_.empty()) { | |
577 return 0; | |
578 } | |
579 | |
580 int length = 0; | |
581 | |
582 // The first is always visible. | |
583 const SenderFormatter* sender = senders_[0]; | |
584 const std::string& originator_name = sender->name(); | |
585 length += originator_name.length(); | |
586 if (html) { | |
587 html->append(FormatName(originator_name, first_is_unread)); | |
588 } | |
589 | |
590 bool elided = false; | |
591 const char* between = ""; | |
592 for (size_t i = 1; i < senders_.size(); i++) { | |
593 sender = senders_[i]; | |
594 | |
595 if (sender->visible()) { | |
596 // Handle the separator. | |
597 between = elided ? " " : kNormalSeparator; | |
598 // Ignore the , for length because it is so narrow, so in both cases | |
599 // above the space is the only things that counts for spaces. | |
600 length++; | |
601 | |
602 // Handle the name. | |
603 const std::string name = sender->name(); | |
604 length += name.size(); | |
605 | |
606 // Construct the html. | |
607 if (html) { | |
608 html->append(between); | |
609 html->append(FormatName(name, sender->is_unread())); | |
610 } | |
611 elided = false; | |
612 } else if (!elided) { | |
613 between = kEllidedSeparator; | |
614 length += 2; // ".." is narrow. | |
615 if (html) { | |
616 html->append(between); | |
617 } | |
618 elided = true; | |
619 } | |
620 } | |
621 return length; | |
622 } | |
623 | |
624 bool is_first_unread() const { | |
625 return !are_any_read_; | |
626 } | |
627 | |
628 size_t visible_count() const { | |
629 return added_.size(); | |
630 } | |
631 | |
632 enum MessageState { | |
633 INITIAL_STATE, | |
634 FIRST_MESSAGE, | |
635 FIRST_UNREAD_MESSAGE, | |
636 LAST_MESSAGE, | |
637 PREVIOUS_MESSAGE, | |
638 REMOVED_MESSAGE, | |
639 }; | |
640 | |
641 // What state we were in during the last "stateful" function call. | |
642 MessageState state_; | |
643 bool are_any_read_; | |
644 std::vector<SenderFormatter*> senders_; | |
645 std::stack<int> added_; | |
646 int index_; | |
647 int first_unread_index_; | |
648 DISALLOW_COPY_AND_ASSIGN(SenderFormatterList); | |
649 }; | |
650 | |
651 | |
652 std::string GetSenderHtml(const MailSenderList& sender_list, | |
653 int message_count, | |
654 const std::string& me_address, | |
655 int space) { | |
656 // There has to be at least 9 spaces to show something reasonable. | |
657 ASSERT(space >= 10); | |
658 std::string count_html; | |
659 if (message_count > 1) { | |
660 std::string count(IntToString(message_count)); | |
661 space -= (count.size()); | |
662 count_html.append(" ("); | |
663 count_html.append(count); | |
664 count_html.append(")"); | |
665 // Reduce space due to parenthesis and . | |
666 space -= 2; | |
667 } | |
668 | |
669 SenderFormatterList senders(sender_list, me_address); | |
670 std::string html_list(senders.GetHtml(space)); | |
671 html_list.append(count_html); | |
672 return html_list; | |
673 } | |
674 | |
675 } // namespace notifier | |
OLD | NEW |