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