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