1 /* 2 Open Tracker License 3 4 Terms and Conditions 5 6 Copyright (c) 1991-2001, Be Incorporated. All rights reserved. 7 8 Permission is hereby granted, free of charge, to any person obtaining a copy of 9 this software and associated documentation files (the "Software"), to deal in 10 the Software without restriction, including without limitation the rights to 11 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 12 of the Software, and to permit persons to whom the Software is furnished to do 13 so, subject to the following conditions: 14 15 The above copyright notice and this permission notice applies to all licensees 16 and shall be included in all copies or substantial portions of the Software. 17 18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF TITLE, MERCHANTABILITY, 20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 BE INCORPORATED BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 22 AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN 23 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 25 Except as contained in this notice, the name of Be Incorporated shall not be 26 used in advertising or otherwise to promote the sale, use or other dealings in 27 this Software without prior written authorization from Be Incorporated. 28 29 BeMail(TM), Tracker(TM), Be(R), BeOS(R), and BeIA(TM) are trademarks or 30 registered trademarks of Be Incorporated in the United States and other 31 countries. Other brand product names are registered trademarks or trademarks 32 of their respective holders. All rights reserved. 33 */ 34 35 #include "MailApp.h" 36 #include "MailSupport.h" 37 #include "MailWindow.h" 38 #include "Messages.h" 39 #include "Header.h" 40 #include "Utilities.h" 41 #include "QueryMenu.h" 42 #include "FieldMsg.h" 43 #include "Prefs.h" 44 45 #include <MailSettings.h> 46 #include <MailMessage.h> 47 48 #include <CharacterSet.h> 49 #include <CharacterSetRoster.h> 50 #include <E-mail.h> 51 #include <Locale.h> 52 #include <MenuBar.h> 53 #include <MenuField.h> 54 #include <MenuItem.h> 55 #include <PopUpMenu.h> 56 #include <Query.h> 57 #include <String.h> 58 #include <StringView.h> 59 #include <Volume.h> 60 #include <VolumeRoster.h> 61 #include <Window.h> 62 #include <fs_index.h> 63 #include <fs_info.h> 64 65 #include <ctype.h> 66 #include <map> 67 #include <stdio.h> 68 #include <stdlib.h> 69 #include <strings.h> 70 #include <time.h> 71 72 73 #define B_TRANSLATION_CONTEXT "Mail" 74 75 76 using namespace BPrivate; 77 using std::map; 78 79 80 const char* kDateLabel = B_TRANSLATE("Date:"); 81 const uint32 kMsgFrom = 'hFrm'; 82 const uint32 kMsgEncoding = 'encd'; 83 const uint32 kMsgAddressChosen = 'acsn'; 84 85 static const float kTextControlDividerOffset = 0; 86 static const float kMenuFieldDividerOffset = 6; 87 88 89 class QPopupMenu : public QueryMenu { 90 public: 91 QPopupMenu(const char *title); 92 93 private: 94 void AddPersonItem(const entry_ref *ref, ino_t node, BString &name, 95 BString &email, const char *attr, BMenu *groupMenu, 96 BMenuItem *superItem); 97 98 protected: 99 virtual void EntryCreated(const entry_ref &ref, ino_t node); 100 virtual void EntryRemoved(ino_t node); 101 102 int32 fGroups; // Current number of "group" submenus. Includes All People if present. 103 }; 104 105 106 struct CompareBStrings { 107 bool 108 operator()(const BString *s1, const BString *s2) const 109 { 110 return (s1->Compare(*s2) < 0); 111 } 112 }; 113 114 115 const char* 116 mail_to_filter(const char* text, int32& length, const text_run_array*& runs) 117 { 118 if (!strncmp(text, "mailto:", 7)) { 119 text += 7; 120 length -= 7; 121 if (runs != NULL) 122 runs = NULL; 123 } 124 125 return text; 126 } 127 128 129 static const float kPlainFontSizeScale = 0.9; 130 131 132 // #pragma mark - THeaderView 133 134 135 THeaderView::THeaderView(BRect rect, BRect windowRect, bool incoming, 136 bool resending, uint32 defaultCharacterSet, int32 defaultAccount) 137 : 138 BBox(rect, "m_header", B_FOLLOW_LEFT_RIGHT, B_WILL_DRAW, B_NO_BORDER), 139 140 fAccountMenu(NULL), 141 fEncodingMenu(NULL), 142 fAccountID(defaultAccount), 143 fAccountTo(NULL), 144 fAccount(NULL), 145 fBcc(NULL), 146 fCc(NULL), 147 fSubject(NULL), 148 fTo(NULL), 149 fDateLabel(NULL), 150 fDate(NULL), 151 fIncoming(incoming), 152 fCharacterSetUserSees(defaultCharacterSet), 153 fResending(resending), 154 fBccMenu(NULL), 155 fCcMenu(NULL), 156 fToMenu(NULL), 157 fEmailList(NULL) 158 { 159 BMenuField* field; 160 BMessage* msg; 161 162 float x = StringWidth( /* The longest title string in the header area */ 163 B_TRANSLATE("Attachments: ")) + 9; 164 float y = TO_FIELD_V; 165 166 BMenuBar* dummy = new BMenuBar(BRect(0, 0, 100, 15), "Dummy"); 167 AddChild(dummy); 168 float width, menuBarHeight; 169 dummy->GetPreferredSize(&width, &menuBarHeight); 170 dummy->RemoveSelf(); 171 delete dummy; 172 173 float menuFieldHeight = menuBarHeight + 2; 174 float controlHeight = menuBarHeight + floorf(be_plain_font->Size() / 1.15); 175 176 if (!fIncoming) { 177 InitEmailCompletion(); 178 InitGroupCompletion(); 179 } 180 181 // Prepare the character set selection pop-up menu (we tell the user that 182 // it is the Encoding menu, even though it is really the character set). 183 // It may appear in the first line, to the right of the From box if the 184 // user is reading an e-mail. It appears on the second line, to the right 185 // of the e-mail account menu, if the user is composing a message. It lets 186 // the user quickly select a character set different from the application 187 // wide default one, and also shows them which character set is active. If 188 // you are reading a message, you also see an item that says "Automatic" 189 // for automatic decoding character set choice. It can slide around as the 190 // window is resized when viewing a message, but not when composing 191 // (because the adjacent pop-up menu can't resize dynamically due to a BeOS 192 // bug). 193 194 float widestCharacterSet = 0; 195 bool markedCharSet = false; 196 BMenuItem* item; 197 198 fEncodingMenu = new BPopUpMenu(B_EMPTY_STRING); 199 200 BCharacterSetRoster roster; 201 BCharacterSet charset; 202 while (roster.GetNextCharacterSet(&charset) == B_OK) { 203 BString name(charset.GetPrintName()); 204 const char* mime = charset.GetMIMEName(); 205 if (mime) 206 name << " (" << mime << ")"; 207 208 uint32 convertID; 209 if (mime == NULL || strcasecmp(mime, "UTF-8") != 0) 210 convertID = charset.GetConversionID(); 211 else 212 convertID = B_MAIL_UTF8_CONVERSION; 213 214 msg = new BMessage(kMsgEncoding); 215 msg->AddInt32("charset", convertID); 216 fEncodingMenu->AddItem(item = new BMenuItem(name.String(), msg)); 217 if (convertID == fCharacterSetUserSees && !markedCharSet) { 218 item->SetMarked(true); 219 markedCharSet = true; 220 } 221 if (StringWidth(name.String()) > widestCharacterSet) 222 widestCharacterSet = StringWidth(name.String()); 223 } 224 225 msg = new BMessage(kMsgEncoding); 226 msg->AddInt32("charset", B_MAIL_US_ASCII_CONVERSION); 227 fEncodingMenu->AddItem(item = new BMenuItem("US-ASCII", msg)); 228 if (fCharacterSetUserSees == B_MAIL_US_ASCII_CONVERSION && !markedCharSet) { 229 item->SetMarked(true); 230 markedCharSet = true; 231 } 232 233 if (!resending && fIncoming) { 234 // reading a message, display the Automatic item 235 fEncodingMenu->AddSeparatorItem(); 236 msg = new BMessage(kMsgEncoding); 237 msg->AddInt32("charset", B_MAIL_NULL_CONVERSION); 238 fEncodingMenu->AddItem(item = new BMenuItem(B_TRANSLATE("Automatic"), msg)); 239 if (!markedCharSet) 240 item->SetMarked(true); 241 } 242 243 // First line of the header, From for reading e-mails (includes the 244 // character set choice at the right), To when composing (nothing else in 245 // the row). 246 247 BRect r; 248 char string[20]; 249 if (fIncoming && !resending) { 250 // Set up the character set pop-up menu on the right of "To" box. 251 r.Set (windowRect.Width() - widestCharacterSet - 252 StringWidth (B_TRANSLATE("Decoding:")) - 2 * SEPARATOR_MARGIN, 253 y - 2, windowRect.Width() - SEPARATOR_MARGIN, 254 y + menuFieldHeight); 255 field = new BMenuField (r, "decoding", B_TRANSLATE("Decoding:"), 256 fEncodingMenu, true /* fixedSize */, 257 B_FOLLOW_TOP | B_FOLLOW_RIGHT, 258 B_WILL_DRAW | B_NAVIGABLE | B_NAVIGABLE_JUMP); 259 field->SetDivider(field->StringWidth(B_TRANSLATE("Decoding:")) + 5); 260 AddChild(field); 261 r.Set(SEPARATOR_MARGIN, y, 262 field->Frame().left - SEPARATOR_MARGIN, y + menuFieldHeight); 263 sprintf(string, B_TRANSLATE("From:")); 264 } else { 265 r.Set(x - 12, y, windowRect.Width() - SEPARATOR_MARGIN, 266 y + menuFieldHeight); 267 string[0] = 0; 268 } 269 270 y += controlHeight; 271 fTo = new TTextControl(r, string, new BMessage(TO_FIELD), fIncoming, 272 resending, B_FOLLOW_LEFT_RIGHT); 273 fTo->SetFilter(mail_to_filter); 274 275 if (!fIncoming || resending) { 276 fTo->SetChoiceList(&fEmailList); 277 fTo->SetAutoComplete(true); 278 } else { 279 fTo->SetDivider(x - 12 - SEPARATOR_MARGIN); 280 fTo->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT); 281 } 282 283 AddChild(fTo); 284 msg = new BMessage(FIELD_CHANGED); 285 msg->AddInt32("bitmask", FIELD_TO); 286 fTo->SetModificationMessage(msg); 287 288 if (!fIncoming || resending) { 289 r.right = r.left - 5; 290 r.left = r.right - ceilf(be_plain_font->StringWidth( 291 B_TRANSLATE("To:")) + 25); 292 r.top -= 1; 293 fToMenu = new QPopupMenu(B_TRANSLATE("To:")); 294 field = new BMenuField(r, "", "", fToMenu, true, 295 B_FOLLOW_LEFT | B_FOLLOW_TOP, B_WILL_DRAW); 296 field->SetDivider(0.0); 297 field->SetEnabled(true); 298 AddChild(field); 299 } 300 301 // "From:" accounts Menu and Encoding Menu. 302 if (!fIncoming || resending) { 303 // Put the character set box on the right of the From field. 304 r.Set(windowRect.Width() - widestCharacterSet - 305 StringWidth(B_TRANSLATE("Encoding:")) - 2 * SEPARATOR_MARGIN, 306 y - 2, windowRect.Width() - SEPARATOR_MARGIN, y + menuFieldHeight); 307 BMenuField* encodingField = new BMenuField(r, "encoding", 308 B_TRANSLATE("Encoding:"), fEncodingMenu, true /* fixedSize */, 309 B_FOLLOW_TOP | B_FOLLOW_RIGHT, 310 B_WILL_DRAW | B_NAVIGABLE | B_NAVIGABLE_JUMP); 311 encodingField->SetDivider(encodingField->StringWidth( 312 B_TRANSLATE("Encoding:")) + 5); 313 AddChild(encodingField); 314 315 field = encodingField; 316 317 // And now the "from account" pop-up menu, on the left side, taking the 318 // remaining space. 319 320 fAccountMenu = new BPopUpMenu(B_EMPTY_STRING); 321 322 BMailAccounts accounts; 323 bool marked = false; 324 for (int32 i = 0; i < accounts.CountAccounts(); i++) { 325 BMailAccountSettings* account = accounts.AccountAt(i); 326 BString name = account->Name(); 327 name << ": " << account->RealName() << " <" 328 << account->ReturnAddress() << ">"; 329 330 msg = new BMessage(kMsgFrom); 331 BMenuItem *item = new BMenuItem(name, msg); 332 333 msg->AddInt32("id", account->AccountID()); 334 335 if (defaultAccount == account->AccountID()) { 336 item->SetMarked(true); 337 marked = true; 338 } 339 fAccountMenu->AddItem(item); 340 } 341 342 if (!marked) { 343 BMenuItem *item = fAccountMenu->ItemAt(0); 344 if (item != NULL) { 345 item->SetMarked(true); 346 fAccountID = item->Message()->FindInt32("id"); 347 } else { 348 fAccountMenu->AddItem( 349 item = new BMenuItem(B_TRANSLATE("<none>"), NULL)); 350 item->SetEnabled(false); 351 fAccountID = ~(int32)0; 352 } 353 // default account is invalid, set to marked 354 // TODO: do this differently, no casting and knowledge 355 // of TMailApp here.... 356 if (TMailApp* app = dynamic_cast<TMailApp*>(be_app)) 357 app->SetDefaultAccount(fAccountID); 358 } 359 360 r.Set(SEPARATOR_MARGIN, y - 2, 361 field->Frame().left - SEPARATOR_MARGIN, y + menuFieldHeight); 362 field = new BMenuField(r, "account", B_TRANSLATE("From:"), 363 fAccountMenu, true /* fixedSize */, 364 B_FOLLOW_TOP | B_FOLLOW_LEFT_RIGHT, 365 B_WILL_DRAW | B_NAVIGABLE | B_NAVIGABLE_JUMP); 366 AddChild(field, encodingField); 367 field->SetDivider(x - 12 - SEPARATOR_MARGIN + kMenuFieldDividerOffset); 368 field->SetAlignment(B_ALIGN_RIGHT); 369 y += controlHeight; 370 } else { 371 // To: account 372 bool account = BMailAccounts().CountAccounts() > 0; 373 374 r.Set(SEPARATOR_MARGIN, y, 375 windowRect.Width() - SEPARATOR_MARGIN, y + menuFieldHeight); 376 if (account) 377 r.right -= SEPARATOR_MARGIN + ACCOUNT_FIELD_WIDTH; 378 fAccountTo = new TTextControl(r, B_TRANSLATE("To:"), NULL, fIncoming, 379 false, B_FOLLOW_LEFT_RIGHT); 380 fAccountTo->SetEnabled(false); 381 fAccountTo->SetDivider(x - 12 - SEPARATOR_MARGIN); 382 fAccountTo->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT); 383 AddChild(fAccountTo); 384 385 if (account) { 386 r.left = r.right + 6; r.right = windowRect.Width() - SEPARATOR_MARGIN; 387 fAccount = new TTextControl(r, B_TRANSLATE("Account:"), NULL, 388 fIncoming, false, B_FOLLOW_RIGHT | B_FOLLOW_TOP); 389 fAccount->SetEnabled(false); 390 AddChild(fAccount); 391 } 392 y += controlHeight; 393 } 394 395 if (fIncoming) { 396 --y; 397 r.Set(SEPARATOR_MARGIN, y, 398 windowRect.Width() - SEPARATOR_MARGIN, y + menuFieldHeight); 399 y += controlHeight; 400 fCc = new TTextControl(r, B_TRANSLATE("Cc:"), 401 NULL, fIncoming, false, B_FOLLOW_LEFT_RIGHT); 402 fCc->SetEnabled(false); 403 fCc->SetDivider(x - 12 - SEPARATOR_MARGIN); 404 fCc->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT); 405 AddChild(fCc); 406 } 407 408 --y; 409 r.Set(SEPARATOR_MARGIN, y, 410 windowRect.Width() - SEPARATOR_MARGIN, y + menuFieldHeight); 411 y += controlHeight; 412 fSubject = new TTextControl(r, B_TRANSLATE("Subject:"), 413 new BMessage(SUBJECT_FIELD),fIncoming, false, B_FOLLOW_LEFT_RIGHT); 414 AddChild(fSubject); 415 (msg = new BMessage(FIELD_CHANGED))->AddInt32("bitmask", FIELD_SUBJECT); 416 fSubject->SetModificationMessage(msg); 417 fSubject->SetDivider(x - 12 - SEPARATOR_MARGIN); 418 fSubject->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT); 419 if (fResending) 420 fSubject->SetEnabled(false); 421 422 --y; 423 424 if (!fIncoming) { 425 r.Set(x - 12, y, CC_FIELD_H + CC_FIELD_WIDTH, y + menuFieldHeight); 426 fCc = new TTextControl(r, "", new BMessage(CC_FIELD), fIncoming, false); 427 fCc->SetFilter(mail_to_filter); 428 fCc->SetChoiceList(&fEmailList); 429 fCc->SetAutoComplete(true); 430 AddChild(fCc); 431 (msg = new BMessage(FIELD_CHANGED))->AddInt32("bitmask", FIELD_CC); 432 fCc->SetModificationMessage(msg); 433 434 r.right = r.left - 5; 435 r.left = r.right - ceilf(be_plain_font->StringWidth( 436 B_TRANSLATE("Cc:")) + 25); 437 r.top -= 1; 438 fCcMenu = new QPopupMenu(B_TRANSLATE("Cc:")); 439 field = new BMenuField(r, "", "", fCcMenu, true, 440 B_FOLLOW_LEFT | B_FOLLOW_TOP, B_WILL_DRAW); 441 442 field->SetDivider(0.0); 443 field->SetEnabled(true); 444 AddChild(field); 445 446 r.Set(BCC_FIELD_H + be_plain_font->StringWidth(B_TRANSLATE("Bcc:")), y, 447 windowRect.Width() - SEPARATOR_MARGIN, y + menuFieldHeight); 448 y += controlHeight; 449 fBcc = new TTextControl(r, "", new BMessage(BCC_FIELD), 450 fIncoming, false, B_FOLLOW_LEFT_RIGHT); 451 fBcc->SetFilter(mail_to_filter); 452 fBcc->SetChoiceList(&fEmailList); 453 fBcc->SetAutoComplete(true); 454 AddChild(fBcc); 455 (msg = new BMessage(FIELD_CHANGED))->AddInt32("bitmask", FIELD_BCC); 456 fBcc->SetModificationMessage(msg); 457 458 r.right = r.left - 5; 459 r.left = r.right - ceilf(be_plain_font->StringWidth( 460 B_TRANSLATE("Bcc:")) + 25); 461 r.top -= 1; 462 fBccMenu = new QPopupMenu(B_TRANSLATE("Bcc:")); 463 field = new BMenuField(r, "", "", fBccMenu, true, 464 B_FOLLOW_LEFT | B_FOLLOW_TOP, B_WILL_DRAW); 465 field->SetDivider(0.0); 466 field->SetEnabled(true); 467 AddChild(field); 468 } else { 469 y -= SEPARATOR_MARGIN; 470 r.Set(SEPARATOR_MARGIN, y, x - 12 - 1, y + menuFieldHeight); 471 fDateLabel = new BStringView(r, "", kDateLabel); 472 fDateLabel->SetAlignment(B_ALIGN_RIGHT); 473 AddChild(fDateLabel); 474 fDateLabel->SetHighColor(0, 0, 0); 475 476 r.Set(r.right + 9, y, windowRect.Width() - SEPARATOR_MARGIN, 477 y + menuFieldHeight); 478 fDate = new BStringView(r, "", ""); 479 AddChild(fDate); 480 fDate->SetHighColor(0, 0, 0); 481 482 y += controlHeight + 5; 483 } 484 ResizeTo(Bounds().Width(), y); 485 } 486 487 488 void 489 THeaderView::InitEmailCompletion() 490 { 491 // get boot volume 492 BVolume volume; 493 BVolumeRoster().GetBootVolume(&volume); 494 495 BQuery query; 496 query.SetVolume(&volume); 497 query.SetPredicate("META:email=**"); 498 // Due to R5 BFS bugs, you need two stars, META:email=** for the query. 499 // META:email="*" will just return one entry and stop, same with 500 // META:email=* and a few other variations. Grumble. 501 query.Fetch(); 502 entry_ref ref; 503 504 while (query.GetNextRef (&ref) == B_OK) { 505 BNode file; 506 if (file.SetTo(&ref) == B_OK) { 507 // Add the e-mail address as an auto-complete string. 508 BString email; 509 if (file.ReadAttrString("META:email", &email) >= B_OK) 510 fEmailList.AddChoice(email.String()); 511 512 // Also add the quoted full name as an auto-complete string. Can't 513 // do unquoted since auto-complete isn't that smart, so the user 514 // will have to type a quote mark if he wants to select someone by 515 // name. 516 BString fullName; 517 if (file.ReadAttrString("META:name", &fullName) >= B_OK) { 518 if (email.FindFirst('<') < 0) { 519 email.ReplaceAll('>', '_'); 520 email.Prepend("<"); 521 email.Append(">"); 522 } 523 fullName.ReplaceAll('\"', '_'); 524 fullName.Prepend("\""); 525 fullName << "\" " << email; 526 fEmailList.AddChoice(fullName.String()); 527 } 528 529 // support for 3rd-party People apps. Looks like a job for 530 // multiple keyword (so you can have several e-mail addresses in 531 // one attribute, perhaps comma separated) indices! Which aren't 532 // yet in BFS. 533 for (int16 i = 2; i < 6; i++) { 534 char attr[16]; 535 sprintf(attr, "META:email%d", i); 536 if (file.ReadAttrString(attr, &email) >= B_OK) 537 fEmailList.AddChoice(email.String()); 538 } 539 } 540 } 541 } 542 543 544 void 545 THeaderView::InitGroupCompletion() 546 { 547 // get boot volume 548 BVolume volume; 549 BVolumeRoster().GetBootVolume(&volume); 550 551 // Build a list of all unique groups and the addresses they expand to. 552 BQuery query; 553 query.SetVolume(&volume); 554 query.SetPredicate("META:group=**"); 555 query.Fetch(); 556 557 map<BString *, BString *, CompareBStrings> groupMap; 558 entry_ref ref; 559 BNode file; 560 while (query.GetNextRef(&ref) == B_OK) { 561 if (file.SetTo(&ref) != B_OK) 562 continue; 563 564 BString groups; 565 if (file.ReadAttrString("META:group", &groups) < B_OK || groups.Length() == 0) 566 continue; 567 568 BString address; 569 file.ReadAttrString("META:email", &address); 570 571 // avoid adding an empty address 572 if (address.Length() == 0) 573 continue; 574 575 char *group = groups.LockBuffer(groups.Length()); 576 char *next = strchr(group, ','); 577 578 for (;;) { 579 if (next) 580 *next = 0; 581 582 while (*group && *group == ' ') 583 group++; 584 585 BString *groupString = new BString(group); 586 BString *addressListString = NULL; 587 588 // nobody is in this group yet, start it off 589 if (groupMap[groupString] == NULL) { 590 addressListString = new BString(*groupString); 591 addressListString->Append(" "); 592 groupMap[groupString] = addressListString; 593 } else { 594 addressListString = groupMap[groupString]; 595 addressListString->Append(", "); 596 delete groupString; 597 } 598 599 // Append the user's address to the end of the string with the 600 // comma separated list of addresses. If not present, add the 601 // < and > brackets around the address. 602 603 if (address.FindFirst ('<') < 0) { 604 address.ReplaceAll ('>', '_'); 605 address.Prepend ("<"); 606 address.Append(">"); 607 } 608 addressListString->Append(address); 609 610 if (!next) 611 break; 612 613 group = next + 1; 614 next = strchr(group, ','); 615 } 616 } 617 618 map<BString *, BString *, CompareBStrings>::iterator iter; 619 for (iter = groupMap.begin(); iter != groupMap.end();) { 620 BString *group = iter->first; 621 BString *addr = iter->second; 622 fEmailList.AddChoice(addr->String()); 623 ++iter; 624 groupMap.erase(group); 625 delete group; 626 delete addr; 627 } 628 } 629 630 631 void 632 THeaderView::MessageReceived(BMessage *msg) 633 { 634 switch (msg->what) { 635 case B_SIMPLE_DATA: 636 { 637 BTextView *textView = dynamic_cast<BTextView *>(Window()->CurrentFocus()); 638 if (dynamic_cast<TTextControl *>(textView->Parent()) != NULL) 639 textView->Parent()->MessageReceived(msg); 640 else { 641 BMessage message(*msg); 642 message.what = REFS_RECEIVED; 643 Window()->PostMessage(&message, Window()); 644 } 645 break; 646 } 647 648 case kMsgFrom: 649 { 650 BMenuItem *item; 651 if (msg->FindPointer("source", (void **)&item) >= B_OK) 652 item->SetMarked(true); 653 654 int32 account; 655 if (msg->FindInt32("id",(int32 *)&account) >= B_OK) 656 fAccountID = account; 657 658 BMessage message(FIELD_CHANGED); 659 // field doesn't matter; no special processing for this field 660 // it's just to turn on the save button 661 message.AddInt32("bitmask", 0); 662 Window()->PostMessage(&message, Window()); 663 664 break; 665 } 666 667 case kMsgEncoding: 668 { 669 BMessage message(*msg); 670 int32 charSet; 671 672 if (msg->FindInt32("charset", &charSet) == B_OK) 673 fCharacterSetUserSees = charSet; 674 675 message.what = CHARSET_CHOICE_MADE; 676 message.AddInt32 ("charset", fCharacterSetUserSees); 677 Window()->PostMessage (&message, Window()); 678 679 BMessage message2(FIELD_CHANGED); 680 // field doesn't matter; no special processing for this field 681 // it's just to turn on the save button 682 message2.AddInt32("bitmask", 0); 683 Window()->PostMessage(&message2, Window()); 684 685 break; 686 } 687 } 688 } 689 690 691 void 692 THeaderView::AttachedToWindow() 693 { 694 if (fToMenu) { 695 fToMenu->SetTargetForItems(fTo); 696 fToMenu->SetPredicate("META:email=**"); 697 } 698 if (fCcMenu) { 699 fCcMenu->SetTargetForItems(fCc); 700 fCcMenu->SetPredicate("META:email=**"); 701 } 702 if (fBccMenu) { 703 fBccMenu->SetTargetForItems(fBcc); 704 fBccMenu->SetPredicate("META:email=**"); 705 } 706 if (fTo) 707 fTo->SetTarget(Looper()); 708 if (fSubject) 709 fSubject->SetTarget(Looper()); 710 if (fCc) 711 fCc->SetTarget(Looper()); 712 if (fBcc) 713 fBcc->SetTarget(Looper()); 714 if (fAccount) 715 fAccount->SetTarget(Looper()); 716 if (fAccountMenu) 717 fAccountMenu->SetTargetForItems(this); 718 if (fEncodingMenu) 719 fEncodingMenu->SetTargetForItems(this); 720 721 BBox::AttachedToWindow(); 722 } 723 724 725 status_t 726 THeaderView::LoadMessage(BEmailMessage *mail) 727 { 728 // Set the date on this message 729 const char *dateField = mail->Date(); 730 char string[256]; 731 sprintf(string, "%s", dateField != NULL ? dateField : "Unknown"); 732 fDate->SetText(string); 733 734 // Set contents of header fields 735 if (fIncoming && !fResending) { 736 if (fBcc != NULL) 737 fBcc->SetEnabled(false); 738 739 if (fCc != NULL) { 740 fCc->SetEnabled(false); 741 fCc->SetText(mail->CC()); 742 } 743 744 if (fAccount != NULL) 745 fAccount->SetEnabled(false); 746 747 if (fAccountTo != NULL) 748 fAccountTo->SetEnabled(false); 749 750 fSubject->SetEnabled(false); 751 fTo->SetEnabled(false); 752 753 // show/hide CC field 754 bool haveText = false; 755 if (mail->CC() != NULL && strlen(mail->CC()) > 0) { 756 haveText = true; 757 } 758 bool isHidden = fCc->IsHidden(this); // hidden relative to parent 759 if (haveText && isHidden) { 760 float diff = fAccountTo->Frame().top - fTo->Frame().top; 761 fSubject->MoveBy(0, diff); 762 fDate->MoveBy(0, diff); 763 fDateLabel->MoveBy(0, diff); 764 fCc->Show(); 765 this->ResizeBy(0, diff); 766 } 767 else if (!haveText && !isHidden) { 768 float diff = fAccountTo->Frame().top - fTo->Frame().top; 769 fSubject->MoveBy(0, - diff); 770 fDate->MoveBy(0, - diff); 771 fDateLabel->MoveBy(0, - diff); 772 fCc->Hide(); 773 this->ResizeBy(0, - diff); 774 } 775 } 776 777 // Set Subject: & From: fields 778 fSubject->SetText(mail->Subject()); 779 fTo->SetText(mail->From()); 780 781 // Set Account/To Field 782 if (fAccountTo != NULL) 783 fAccountTo->SetText(mail->To()); 784 785 BString accountName; 786 if (fAccount != NULL && mail->GetAccountName(accountName) == B_OK) 787 fAccount->SetText(accountName); 788 789 return B_OK; 790 } 791 792 793 // #pragma mark - TTextControl 794 795 796 TTextControl::TTextControl(BRect rect, const char *label, BMessage *msg, 797 bool incoming, bool resending, int32 resizingMode) 798 : BComboBox(rect, "happy", label, msg, resizingMode), 799 fRefDropMenu(NULL) 800 //:BTextControl(rect, "happy", label, "", msg, resizingMode) 801 { 802 strcpy(fLabel, label); 803 fCommand = msg != NULL ? msg->what : 0UL; 804 fIncoming = incoming; 805 fResending = resending; 806 } 807 808 809 void 810 TTextControl::AttachedToWindow() 811 { 812 SetHighColor(0, 0, 0); 813 // BTextControl::AttachedToWindow(); 814 BComboBox::AttachedToWindow(); 815 816 SetDivider(Divider() + kTextControlDividerOffset); 817 } 818 819 820 void 821 TTextControl::MessageReceived(BMessage *msg) 822 { 823 switch (msg->what) { 824 case B_SIMPLE_DATA: { 825 if (fIncoming && !fResending) 826 return; 827 828 int32 buttons = -1; 829 BPoint point; 830 if (msg->FindInt32("buttons", &buttons) != B_OK) 831 buttons = B_PRIMARY_MOUSE_BUTTON; 832 833 if (buttons != B_PRIMARY_MOUSE_BUTTON 834 && msg->FindPoint("_drop_point_", &point) != B_OK) 835 return; 836 837 BMessage message(REFS_RECEIVED); 838 bool enclosure = false; 839 BString addressList; 840 // Batch up the addresses to be added, since we can only 841 // insert a few times before deadlocking since inserting 842 // sends a notification message to the window BLooper, 843 // which is busy doing this insert. BeOS message queues 844 // are annoyingly limited in their design. 845 846 entry_ref ref; 847 for (int32 index = 0;msg->FindRef("refs", index, &ref) == B_OK; index++) { 848 BFile file(&ref, B_READ_ONLY); 849 if (file.InitCheck() == B_NO_ERROR) { 850 BNodeInfo info(&file); 851 char type[B_FILE_NAME_LENGTH]; 852 info.GetType(type); 853 854 if (fCommand != SUBJECT_FIELD 855 && !strcmp(type,"application/x-person")) { 856 // add person's E-mail address to the To: field 857 858 BString attr = ""; 859 if (buttons == B_PRIMARY_MOUSE_BUTTON) { 860 if (msg->FindString("attr", &attr) < B_OK) 861 attr = "META:email"; // If not META:email3 etc. 862 } else { 863 BNode node(&ref); 864 node.RewindAttrs(); 865 866 char buffer[B_ATTR_NAME_LENGTH]; 867 868 delete fRefDropMenu; 869 fRefDropMenu = new BPopUpMenu("RecipientMenu"); 870 871 while (node.GetNextAttrName(buffer) == B_OK) { 872 if (strstr(buffer, "email") <= 0) 873 continue; 874 875 attr = buffer; 876 877 BString address; 878 node.ReadAttrString(buffer, &address); 879 if (address.Length() <= 0) 880 continue; 881 882 BMessage *itemMsg = new BMessage(kMsgAddressChosen); 883 itemMsg->AddString("address", address.String()); 884 itemMsg->AddRef("ref", &ref); 885 886 BMenuItem *item = new BMenuItem(address.String(), 887 itemMsg); 888 fRefDropMenu->AddItem(item); 889 } 890 891 if (fRefDropMenu->CountItems() > 1) { 892 fRefDropMenu->SetTargetForItems(this); 893 fRefDropMenu->Go(point, true, true, true); 894 return; 895 } else { 896 delete fRefDropMenu; 897 fRefDropMenu = NULL; 898 } 899 } 900 901 BString email; 902 file.ReadAttrString(attr.String(), &email); 903 904 // we got something... 905 if (email.Length() > 0) { 906 // see if we can get a username as well 907 BString name; 908 file.ReadAttrString("META:name", &name); 909 910 BString address; 911 if (name.Length() == 0) { 912 // if we have no Name, just use the email address 913 address = email; 914 } else { 915 // otherwise, pretty-format it 916 address << "\"" << name << "\" <" << email << ">"; 917 } 918 919 if (addressList.Length() > 0) 920 addressList << ", "; 921 addressList << address; 922 } 923 } else { 924 enclosure = true; 925 message.AddRef("refs", &ref); 926 } 927 } 928 } 929 930 if (addressList.Length() > 0) { 931 BTextView *textView = TextView(); 932 int end = textView->TextLength(); 933 if (end != 0) { 934 textView->Select(end, end); 935 textView->Insert(", "); 936 } 937 textView->Insert(addressList.String()); 938 } 939 940 if (enclosure) 941 Window()->PostMessage(&message, Window()); 942 break; 943 } 944 945 case M_SELECT: 946 { 947 BTextView *textView = (BTextView *)ChildAt(0); 948 if (textView != NULL) 949 textView->Select(0, textView->TextLength()); 950 break; 951 } 952 953 case kMsgAddressChosen: { 954 BString display; 955 BString address; 956 entry_ref ref; 957 958 if (msg->FindString("address", &address) != B_OK 959 || msg->FindRef("ref", &ref) != B_OK) 960 return; 961 962 if (address.Length() > 0) { 963 BString name; 964 BNode node(&ref); 965 966 display = address; 967 968 node.ReadAttrString("META:name", &name); 969 if (name.Length() > 0) { 970 display = ""; 971 display << "\"" << name << "\" <" << address << ">"; 972 } 973 974 BTextView *textView = TextView(); 975 int end = textView->TextLength(); 976 if (end != 0) { 977 textView->Select(end, end); 978 textView->Insert(", "); 979 } 980 textView->Insert(display.String()); 981 } 982 break; 983 } 984 985 default: 986 // BTextControl::MessageReceived(msg); 987 BComboBox::MessageReceived(msg); 988 } 989 } 990 991 992 bool 993 TTextControl::HasFocus() 994 { 995 return TextView()->IsFocus(); 996 } 997 998 999 // #pragma mark - QPopupMenu 1000 1001 1002 QPopupMenu::QPopupMenu(const char *title) 1003 : QueryMenu(title, true), 1004 fGroups(0) 1005 { 1006 } 1007 1008 1009 void 1010 QPopupMenu::AddPersonItem(const entry_ref *ref, ino_t node, BString &name, 1011 BString &email, const char *attr, BMenu *groupMenu, BMenuItem *superItem) 1012 { 1013 BString label; 1014 BString sortKey; 1015 // For alphabetical order sorting, usually last name. 1016 1017 // if we have no Name, just use the email address 1018 if (name.Length() == 0) { 1019 label = email; 1020 sortKey = email; 1021 } else { 1022 // otherwise, pretty-format it 1023 label << name << " (" << email << ")"; 1024 1025 // Extract the last name (last word in the name), 1026 // removing trailing and leading spaces. 1027 const char *nameStart = name.String(); 1028 const char *string = nameStart + strlen(nameStart) - 1; 1029 const char *wordEnd; 1030 1031 while (string >= nameStart && isspace(*string)) 1032 string--; 1033 wordEnd = string + 1; // Points to just after last word. 1034 while (string >= nameStart && !isspace(*string)) 1035 string--; 1036 string++; // Point to first letter in the word. 1037 if (wordEnd > string) 1038 sortKey.SetTo(string, wordEnd - string); 1039 else // Blank name, pretend that the last name is after it. 1040 string = nameStart + strlen(nameStart); 1041 1042 // Append the first names to the end, so that people with the same last 1043 // name get sorted by first name. Note no space between the end of the 1044 // last name and the start of the first names, but that shouldn't 1045 // matter for sorting. 1046 sortKey.Append(nameStart, string - nameStart); 1047 } 1048 1049 // The target (a TTextControl) will examine all the People files specified 1050 // and add the emails and names to the string it is displaying (same code 1051 // is used for drag and drop of People files). 1052 BMessage *msg = new BMessage(B_SIMPLE_DATA); 1053 msg->AddRef("refs", ref); 1054 msg->AddInt64("node", node); 1055 if (attr) // For nonstandard e-mail attributes, like META:email3 1056 msg->AddString("attr", attr); 1057 msg->AddString("sortkey", sortKey); 1058 1059 BMenuItem *newItem = new BMenuItem(label.String(), msg); 1060 if (fTargetHandler) 1061 newItem->SetTarget(fTargetHandler); 1062 1063 // If no group, just add it to ourself; else add it to group menu 1064 BMenu *parentMenu = groupMenu ? groupMenu : this; 1065 if (groupMenu) { 1066 // Add ref to group super item. 1067 BMessage *superMsg = superItem->Message(); 1068 superMsg->AddRef("refs", ref); 1069 } 1070 1071 // Add it to the appropriate menu. Use alphabetical order by sortKey to 1072 // insert it in the right spot (a dumb linear search so this will be slow). 1073 // Start searching from the end of the menu, since the main menu includes 1074 // all the groups at the top and we don't want to mix it in with them. 1075 // Thus the search starts at the bottom and ends when we hit a separator 1076 // line or the top of the menu. 1077 1078 int32 index = parentMenu->CountItems(); 1079 while (index-- > 0) { 1080 BMenuItem *item = parentMenu->ItemAt(index); 1081 if (item == NULL || dynamic_cast<BSeparatorItem *>(item) != NULL) 1082 break; 1083 1084 BMessage *message = item->Message(); 1085 BString key; 1086 1087 // Stop when testKey < sortKey. 1088 if (message != NULL 1089 && message->FindString("sortkey", &key) == B_OK 1090 && ICompare(key, sortKey) < 0) 1091 break; 1092 } 1093 1094 if (!parentMenu->AddItem(newItem, index + 1)) { 1095 fprintf (stderr, "QPopupMenu::AddPersonItem: Unable to add menu " 1096 "item \"%s\" at index %" B_PRId32 ".\n", sortKey.String(), index + 1); 1097 delete newItem; 1098 } 1099 } 1100 1101 1102 void 1103 QPopupMenu::EntryCreated(const entry_ref &ref, ino_t node) 1104 { 1105 BNode file; 1106 if (file.SetTo(&ref) < B_OK) 1107 return; 1108 1109 // Make sure the pop-up menu is ready for additions. Need a bunch of 1110 // groups at the top, a divider line, and miscellaneous people added below 1111 // the line. 1112 1113 int32 items = CountItems(); 1114 if (!items) 1115 AddSeparatorItem(); 1116 1117 // Does the file have a group attribute? OK to have none. 1118 BString groups; 1119 const char *kNoGroup = "NoGroup!"; 1120 file.ReadAttrString("META:group", &groups); 1121 if (groups.Length() <= 0) 1122 groups = kNoGroup; 1123 1124 // Add the e-mail address to the all people group. Then add it to all the 1125 // group menus that it exists in (based on the comma separated list of 1126 // groups from the People file), optionally making the group menu if it 1127 // doesn't exist. If it's in the special NoGroup! list, then add it below 1128 // the groups. 1129 1130 bool allPeopleGroupDone = false; 1131 BMenu *groupMenu; 1132 do { 1133 BString group; 1134 1135 if (!allPeopleGroupDone) { 1136 // Create the default group for all people, if it doesn't exist yet. 1137 group = "All People"; 1138 allPeopleGroupDone = true; 1139 } else { 1140 // Break out the next group from the comma separated string. 1141 int32 comma; 1142 if ((comma = groups.FindFirst(',')) > 0) { 1143 groups.MoveInto(group, 0, comma); 1144 groups.Remove(0, 1); 1145 } else 1146 group.Adopt(groups); 1147 } 1148 1149 // trim white spaces 1150 int32 i = 0; 1151 for (i = 0; isspace(group.ByteAt(i)); i++) {} 1152 if (i) 1153 group.Remove(0, i); 1154 for (i = group.Length() - 1; isspace(group.ByteAt(i)); i--) {} 1155 group.Truncate(i + 1); 1156 1157 groupMenu = NULL; 1158 BMenuItem *superItem = NULL; // Corresponding item for group menu. 1159 1160 if (group.Length() > 0 && group != kNoGroup) { 1161 BMenu *sub; 1162 1163 // Look for submenu with label == group name 1164 for (int32 i = 0; i < items; i++) { 1165 if ((sub = SubmenuAt(i)) != NULL) { 1166 superItem = sub->Superitem(); 1167 if (!strcmp(superItem->Label(), group.String())) { 1168 groupMenu = sub; 1169 i++; 1170 break; 1171 } 1172 } 1173 } 1174 1175 // If no submenu, create one 1176 if (!groupMenu) { 1177 // Find where it should go (alphabetical) 1178 int32 mindex = 0; 1179 for (; mindex < fGroups; mindex++) { 1180 if (strcmp(ItemAt(mindex)->Label(), group.String()) > 0) 1181 break; 1182 } 1183 1184 groupMenu = new BMenu(group.String()); 1185 groupMenu->SetFont(be_plain_font); 1186 AddItem(groupMenu, mindex); 1187 1188 superItem = groupMenu->Superitem(); 1189 superItem->SetMessage(new BMessage(B_SIMPLE_DATA)); 1190 if (fTargetHandler) 1191 superItem->SetTarget(fTargetHandler); 1192 1193 fGroups++; 1194 } 1195 } 1196 1197 BString name; 1198 file.ReadAttrString("META:name", &name); 1199 1200 BString email; 1201 file.ReadAttrString("META:email", &email); 1202 1203 if (email.Length() != 0 || name.Length() != 0) 1204 AddPersonItem(&ref, node, name, email, NULL, groupMenu, superItem); 1205 1206 // support for 3rd-party People apps 1207 for (int16 i = 2; i < 6; i++) { 1208 char attr[16]; 1209 sprintf(attr, "META:email%d", i); 1210 if (file.ReadAttrString(attr, &email) >= B_OK && email.Length() > 0) 1211 AddPersonItem(&ref, node, name, email, attr, groupMenu, superItem); 1212 } 1213 } while (groups.Length() > 0); 1214 } 1215 1216 1217 void 1218 QPopupMenu::EntryRemoved(ino_t /*node*/) 1219 { 1220 } 1221 1222