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