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