xref: /haiku/src/apps/mail/Header.cpp (revision 529cd177b573aaba391c8adc9c9f5ad76a14bf81)
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