xref: /haiku/src/apps/mail/Header.cpp (revision 4fd62caa9acc437534c41bbb7d3fc9d53e915005)
1 /*
2 Open Tracker License
3 
4 Terms and Conditions
5 
6 Copyright (c) 1991-2001, Be Incorporated. All rights reserved.
7 
8 Permission is hereby granted, free of charge, to any person obtaining a copy of
9 this software and associated documentation files (the "Software"), to deal in
10 the Software without restriction, including without limitation the rights to
11 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12 of the Software, and to permit persons to whom the Software is furnished to do
13 so, subject to the following conditions:
14 
15 The above copyright notice and this permission notice applies to all licensees
16 and shall be included in all copies or substantial portions of the Software.
17 
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF TITLE, MERCHANTABILITY,
20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 BE INCORPORATED BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
22 AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN
23 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 
25 Except as contained in this notice, the name of Be Incorporated shall not be
26 used in advertising or otherwise to promote the sale, use or other dealings in
27 this Software without prior written authorization from Be Incorporated.
28 
29 BeMail(TM), Tracker(TM), Be(R), BeOS(R), and BeIA(TM) are trademarks or
30 registered trademarks of Be Incorporated in the United States and other
31 countries. Other brand product names are registered trademarks or trademarks
32 of their respective holders. All rights reserved.
33 */
34 
35 #include "MailApp.h"
36 #include "MailSupport.h"
37 #include "MailWindow.h"
38 #include "Messages.h"
39 #include "Header.h"
40 #include "Utilities.h"
41 #include "QueryMenu.h"
42 #include "FieldMsg.h"
43 #include "Prefs.h"
44 
45 #include <MailSettings.h>
46 #include <MailMessage.h>
47 
48 #include <CharacterSet.h>
49 #include <CharacterSetRoster.h>
50 #include <E-mail.h>
51 #include <Locale.h>
52 #include <MenuBar.h>
53 #include <MenuField.h>
54 #include <MenuItem.h>
55 #include <PopUpMenu.h>
56 #include <Query.h>
57 #include <String.h>
58 #include <StringView.h>
59 #include <Volume.h>
60 #include <VolumeRoster.h>
61 #include <Window.h>
62 #include <fs_index.h>
63 #include <fs_info.h>
64 
65 #include <ctype.h>
66 #include <map>
67 #include <stdio.h>
68 #include <stdlib.h>
69 #include <string.h>
70 #include <time.h>
71 
72 
73 #define B_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 + 6;
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 = ~0UL;
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 	--y;
396 	r.Set(SEPARATOR_MARGIN, y,
397 		windowRect.Width() - SEPARATOR_MARGIN, y + menuFieldHeight);
398 	y += controlHeight;
399 	fSubject = new TTextControl(r, B_TRANSLATE("Subject:"),
400 		new BMessage(SUBJECT_FIELD),fIncoming, false, B_FOLLOW_LEFT_RIGHT);
401 	AddChild(fSubject);
402 	(msg = new BMessage(FIELD_CHANGED))->AddInt32("bitmask", FIELD_SUBJECT);
403 	fSubject->SetModificationMessage(msg);
404 	fSubject->SetDivider(x - 12 - SEPARATOR_MARGIN);
405 	fSubject->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT);
406 	if (fResending)
407 		fSubject->SetEnabled(false);
408 
409 	--y;
410 
411 	if (!fIncoming) {
412 		r.Set(x - 12, y, CC_FIELD_H + CC_FIELD_WIDTH, y + menuFieldHeight);
413 		fCc = new TTextControl(r, "", new BMessage(CC_FIELD), fIncoming, false);
414 		fCc->SetFilter(mail_to_filter);
415 		fCc->SetChoiceList(&fEmailList);
416 		fCc->SetAutoComplete(true);
417 		AddChild(fCc);
418 		(msg = new BMessage(FIELD_CHANGED))->AddInt32("bitmask", FIELD_CC);
419 		fCc->SetModificationMessage(msg);
420 
421 		r.right = r.left - 5;
422 		r.left = r.right - ceilf(be_plain_font->StringWidth(
423 			B_TRANSLATE("Cc:")) + 25);
424 		r.top -= 1;
425 		fCcMenu = new QPopupMenu(B_TRANSLATE("Cc:"));
426 		field = new BMenuField(r, "", "", fCcMenu, true,
427 			B_FOLLOW_LEFT | B_FOLLOW_TOP, B_WILL_DRAW);
428 
429 		field->SetDivider(0.0);
430 		field->SetEnabled(true);
431 		AddChild(field);
432 
433 		r.Set(BCC_FIELD_H + be_plain_font->StringWidth(B_TRANSLATE("Bcc:")), y,
434 			  windowRect.Width() - SEPARATOR_MARGIN, y + menuFieldHeight);
435 		y += controlHeight;
436 		fBcc = new TTextControl(r, "", new BMessage(BCC_FIELD),
437 						fIncoming, false, B_FOLLOW_LEFT_RIGHT);
438 		fBcc->SetFilter(mail_to_filter);
439 		fBcc->SetChoiceList(&fEmailList);
440 		fBcc->SetAutoComplete(true);
441 		AddChild(fBcc);
442 		(msg = new BMessage(FIELD_CHANGED))->AddInt32("bitmask", FIELD_BCC);
443 		fBcc->SetModificationMessage(msg);
444 
445 		r.right = r.left - 5;
446 		r.left = r.right - ceilf(be_plain_font->StringWidth(
447 			B_TRANSLATE("Bcc:")) + 25);
448 		r.top -= 1;
449 		fBccMenu = new QPopupMenu(B_TRANSLATE("Bcc:"));
450 		field = new BMenuField(r, "", "", fBccMenu, true,
451 			B_FOLLOW_LEFT | B_FOLLOW_TOP, B_WILL_DRAW);
452 		field->SetDivider(0.0);
453 		field->SetEnabled(true);
454 		AddChild(field);
455 	} else {
456 		y -= SEPARATOR_MARGIN;
457 		r.Set(SEPARATOR_MARGIN, y, x - 12 - 1, y + menuFieldHeight);
458 		fDateLabel = new BStringView(r, "", kDateLabel);
459 		fDateLabel->SetAlignment(B_ALIGN_RIGHT);
460 		AddChild(fDateLabel);
461 		fDateLabel->SetHighColor(0, 0, 0);
462 
463 		r.Set(r.right + 9, y, windowRect.Width() - SEPARATOR_MARGIN,
464 			y + menuFieldHeight);
465 		fDate = new BStringView(r, "", "");
466 		AddChild(fDate);
467 		fDate->SetHighColor(0, 0, 0);
468 
469 		y += controlHeight + 5;
470 	}
471 	ResizeTo(Bounds().Width(), y);
472 }
473 
474 
475 void
476 THeaderView::InitEmailCompletion()
477 {
478 	// get boot volume
479 	BVolume volume;
480 	BVolumeRoster().GetBootVolume(&volume);
481 
482 	BQuery query;
483 	query.SetVolume(&volume);
484 	query.SetPredicate("META:email=**");
485 		// Due to R5 BFS bugs, you need two stars, META:email=** for the query.
486 		// META:email="*" will just return one entry and stop, same with
487 		// META:email=* and a few other variations.  Grumble.
488 	query.Fetch();
489 	entry_ref ref;
490 
491 	while (query.GetNextRef (&ref) == B_OK) {
492 		BNode file;
493 		if (file.SetTo(&ref) == B_OK) {
494 			// Add the e-mail address as an auto-complete string.
495 			BString email;
496 			if (file.ReadAttrString("META:email", &email) >= B_OK)
497 				fEmailList.AddChoice(email.String());
498 
499 			// Also add the quoted full name as an auto-complete string.  Can't
500 			// do unquoted since auto-complete isn't that smart, so the user
501 			// will have to type a quote mark if he wants to select someone by
502 			// name.
503 			BString fullName;
504 			if (file.ReadAttrString("META:name", &fullName) >= B_OK) {
505 				if (email.FindFirst('<') < 0) {
506 					email.ReplaceAll('>', '_');
507 					email.Prepend("<");
508 					email.Append(">");
509 				}
510 				fullName.ReplaceAll('\"', '_');
511 				fullName.Prepend("\"");
512 				fullName << "\" " << email;
513 				fEmailList.AddChoice(fullName.String());
514 			}
515 
516 			// support for 3rd-party People apps.  Looks like a job for
517 			// multiple keyword (so you can have several e-mail addresses in
518 			// one attribute, perhaps comma separated) indices!  Which aren't
519 			// yet in BFS.
520 			for (int16 i = 2; i < 6; i++) {
521 				char attr[16];
522 				sprintf(attr, "META:email%d", i);
523 				if (file.ReadAttrString(attr, &email) >= B_OK)
524 					fEmailList.AddChoice(email.String());
525 			}
526 		}
527 	}
528 }
529 
530 
531 void
532 THeaderView::InitGroupCompletion()
533 {
534 	// get boot volume
535 	BVolume volume;
536 	BVolumeRoster().GetBootVolume(&volume);
537 
538 	// Build a list of all unique groups and the addresses they expand to.
539 	BQuery query;
540 	query.SetVolume(&volume);
541 	query.SetPredicate("META:group=**");
542 	query.Fetch();
543 
544 	map<BString *, BString *, CompareBStrings> groupMap;
545 	entry_ref ref;
546 	BNode file;
547 	while (query.GetNextRef(&ref) == B_OK) {
548 		if (file.SetTo(&ref) != B_OK)
549 			continue;
550 
551 		BString groups;
552 		if (file.ReadAttrString("META:group", &groups) < B_OK || groups.Length() == 0)
553 			continue;
554 
555 		BString address;
556 		file.ReadAttrString("META:email", &address);
557 
558 		// avoid adding an empty address
559 		if (address.Length() == 0)
560 			continue;
561 
562 		char *group = groups.LockBuffer(groups.Length());
563 		char *next = strchr(group, ',');
564 
565 		for (;;) {
566 			if (next)
567 				*next = 0;
568 
569 			while (*group && *group == ' ')
570 				group++;
571 
572 			BString *groupString = new BString(group);
573 			BString *addressListString = NULL;
574 
575 			// nobody is in this group yet, start it off
576 			if (groupMap[groupString] == NULL) {
577 				addressListString = new BString(*groupString);
578 				addressListString->Append(" ");
579 				groupMap[groupString] = addressListString;
580 			} else {
581 				addressListString = groupMap[groupString];
582 				addressListString->Append(", ");
583 				delete groupString;
584 			}
585 
586 			// Append the user's address to the end of the string with the
587 			// comma separated list of addresses.  If not present, add the
588 			// < and > brackets around the address.
589 
590 			if (address.FindFirst ('<') < 0) {
591 				address.ReplaceAll ('>', '_');
592 				address.Prepend ("<");
593 				address.Append(">");
594 			}
595 			addressListString->Append(address);
596 
597 			if (!next)
598 				break;
599 
600 			group = next + 1;
601 			next = strchr(group, ',');
602 		}
603 	}
604 
605 	map<BString *, BString *, CompareBStrings>::iterator iter;
606 	for (iter = groupMap.begin(); iter != groupMap.end();) {
607 		BString *group = iter->first;
608 		BString *addr = iter->second;
609 		fEmailList.AddChoice(addr->String());
610 		++iter;
611 		groupMap.erase(group);
612 		delete group;
613 		delete addr;
614 	}
615 }
616 
617 
618 void
619 THeaderView::MessageReceived(BMessage *msg)
620 {
621 	switch (msg->what) {
622 		case B_SIMPLE_DATA:
623 		{
624 			BTextView *textView = dynamic_cast<BTextView *>(Window()->CurrentFocus());
625 			if (dynamic_cast<TTextControl *>(textView->Parent()) != NULL)
626 				textView->Parent()->MessageReceived(msg);
627 			else {
628 				BMessage message(*msg);
629 				message.what = REFS_RECEIVED;
630 				Window()->PostMessage(&message, Window());
631 			}
632 			break;
633 		}
634 
635 		case kMsgFrom:
636 		{
637 			BMenuItem *item;
638 			if (msg->FindPointer("source", (void **)&item) >= B_OK)
639 				item->SetMarked(true);
640 
641 			int32 account;
642 			if (msg->FindInt32("id",(int32 *)&account) >= B_OK)
643 				fAccountID = account;
644 
645 			BMessage message(FIELD_CHANGED);
646 			// field doesn't matter; no special processing for this field
647 			// it's just to turn on the save button
648 			message.AddInt32("bitmask", 0);
649 			Window()->PostMessage(&message, Window());
650 
651 			break;
652 		}
653 
654 		case kMsgEncoding:
655 		{
656 			BMessage message(*msg);
657 			int32 charSet;
658 
659 			if (msg->FindInt32("charset", &charSet) == B_OK)
660 				fCharacterSetUserSees = charSet;
661 
662 			message.what = CHARSET_CHOICE_MADE;
663 			message.AddInt32 ("charset", fCharacterSetUserSees);
664 			Window()->PostMessage (&message, Window());
665 
666 			BMessage message2(FIELD_CHANGED);
667 			// field doesn't matter; no special processing for this field
668 			// it's just to turn on the save button
669 			message2.AddInt32("bitmask", 0);
670 			Window()->PostMessage(&message2, Window());
671 
672 			break;
673 		}
674 	}
675 }
676 
677 
678 void
679 THeaderView::AttachedToWindow()
680 {
681 	if (fToMenu) {
682 		fToMenu->SetTargetForItems(fTo);
683 		fToMenu->SetPredicate("META:email=**");
684 	}
685 	if (fCcMenu) {
686 		fCcMenu->SetTargetForItems(fCc);
687 		fCcMenu->SetPredicate("META:email=**");
688 	}
689 	if (fBccMenu) {
690 		fBccMenu->SetTargetForItems(fBcc);
691 		fBccMenu->SetPredicate("META:email=**");
692 	}
693 	if (fTo)
694 		fTo->SetTarget(Looper());
695 	if (fSubject)
696 		fSubject->SetTarget(Looper());
697 	if (fCc)
698 		fCc->SetTarget(Looper());
699 	if (fBcc)
700 		fBcc->SetTarget(Looper());
701 	if (fAccount)
702 		fAccount->SetTarget(Looper());
703 	if (fAccountMenu)
704 		fAccountMenu->SetTargetForItems(this);
705 	if (fEncodingMenu)
706 		fEncodingMenu->SetTargetForItems(this);
707 
708 	BBox::AttachedToWindow();
709 }
710 
711 
712 status_t
713 THeaderView::LoadMessage(BEmailMessage *mail)
714 {
715 	//	Set the date on this message
716 	const char *dateField = mail->Date();
717 	char string[256];
718 	sprintf(string, "%s", dateField != NULL ? dateField : "Unknown");
719 	fDate->SetText(string);
720 
721 	//	Set contents of header fields
722 	if (fIncoming && !fResending) {
723 		if (fBcc != NULL)
724 			fBcc->SetEnabled(false);
725 
726 		if (fCc != NULL)
727 			fCc->SetEnabled(false);
728 
729 		if (fAccount != NULL)
730 			fAccount->SetEnabled(false);
731 
732 		if (fAccountTo != NULL)
733 			fAccountTo->SetEnabled(false);
734 
735 		fSubject->SetEnabled(false);
736 		fTo->SetEnabled(false);
737 	}
738 
739 	//	Set Subject: & From: fields
740 	fSubject->SetText(mail->Subject());
741 	fTo->SetText(mail->From());
742 
743 	//	Set Account/To Field
744 	if (fAccountTo != NULL)
745 		fAccountTo->SetText(mail->To());
746 
747 	BString accountName;
748 	if (fAccount != NULL && mail->GetAccountName(accountName) == B_OK)
749 		fAccount->SetText(accountName);
750 
751 	return B_OK;
752 }
753 
754 
755 //	#pragma mark - TTextControl
756 
757 
758 TTextControl::TTextControl(BRect rect, const char *label, BMessage *msg,
759 		bool incoming, bool resending, int32 resizingMode)
760 	: BComboBox(rect, "happy", label, msg, resizingMode),
761 	fRefDropMenu(NULL)
762 	//:BTextControl(rect, "happy", label, "", msg, resizingMode)
763 {
764 	strcpy(fLabel, label);
765 	fCommand = msg != NULL ? msg->what : 0UL;
766 	fIncoming = incoming;
767 	fResending = resending;
768 }
769 
770 
771 void
772 TTextControl::AttachedToWindow()
773 {
774 	SetHighColor(0, 0, 0);
775 	// BTextControl::AttachedToWindow();
776 	BComboBox::AttachedToWindow();
777 
778 	SetDivider(Divider() + kTextControlDividerOffset);
779 }
780 
781 
782 void
783 TTextControl::MessageReceived(BMessage *msg)
784 {
785 	switch (msg->what) {
786 		case B_SIMPLE_DATA: {
787 			if (fIncoming && !fResending)
788 				return;
789 
790 			int32 buttons = -1;
791 			BPoint point;
792 			if (msg->FindInt32("buttons", &buttons) != B_OK)
793 				buttons = B_PRIMARY_MOUSE_BUTTON;
794 
795 			if (buttons != B_PRIMARY_MOUSE_BUTTON
796 				&& msg->FindPoint("_drop_point_", &point) != B_OK)
797 				return;
798 
799 			BMessage message(REFS_RECEIVED);
800 			bool enclosure = false;
801 			BString addressList;
802 				// Batch up the addresses to be added, since we can only
803 				// insert a few times before deadlocking since inserting
804 				// sends a notification message to the window BLooper,
805 				// which is busy doing this insert.  BeOS message queues
806 				// are annoyingly limited in their design.
807 
808 			entry_ref ref;
809 			for (int32 index = 0;msg->FindRef("refs", index, &ref) == B_OK; index++) {
810 				BFile file(&ref, B_READ_ONLY);
811 				if (file.InitCheck() == B_NO_ERROR) {
812 					BNodeInfo info(&file);
813 					char type[B_FILE_NAME_LENGTH];
814 					info.GetType(type);
815 
816 					if (fCommand != SUBJECT_FIELD
817 						&& !strcmp(type,"application/x-person")) {
818 						// add person's E-mail address to the To: field
819 
820 						BString attr = "";
821 						if (buttons == B_PRIMARY_MOUSE_BUTTON) {
822 							if (msg->FindString("attr", &attr) < B_OK)
823 								attr = "META:email"; // If not META:email3 etc.
824 						} else {
825 							BNode node(&ref);
826 							node.RewindAttrs();
827 
828 							char buffer[B_ATTR_NAME_LENGTH];
829 
830 							delete fRefDropMenu;
831 							fRefDropMenu = new BPopUpMenu("RecipientMenu");
832 
833 							while (node.GetNextAttrName(buffer) == B_OK) {
834 								if (strstr(buffer, "email") <= 0)
835 									continue;
836 
837 								attr = buffer;
838 
839 								BString address;
840 								node.ReadAttrString(buffer, &address);
841 								if (address.Length() <= 0)
842 									continue;
843 
844 								BMessage *itemMsg = new BMessage(kMsgAddressChosen);
845 								itemMsg->AddString("address", address.String());
846 								itemMsg->AddRef("ref", &ref);
847 
848 								BMenuItem *item = new BMenuItem(address.String(),
849 									itemMsg);
850 								fRefDropMenu->AddItem(item);
851 							}
852 
853 							if (fRefDropMenu->CountItems() > 1) {
854 								fRefDropMenu->SetTargetForItems(this);
855 								fRefDropMenu->Go(point, true, true, true);
856 								return;
857 							} else {
858 								delete fRefDropMenu;
859 								fRefDropMenu = NULL;
860 							}
861 						}
862 
863 						BString email;
864 						file.ReadAttrString(attr.String(), &email);
865 
866 						// we got something...
867 						if (email.Length() > 0) {
868 							// see if we can get a username as well
869 							BString name;
870 							file.ReadAttrString("META:name", &name);
871 
872 							BString	address;
873 							if (name.Length() == 0) {
874 								// if we have no Name, just use the email address
875 								address = email;
876 							} else {
877 								// otherwise, pretty-format it
878 								address << "\"" << name << "\" <" << email << ">";
879 							}
880 
881 							if (addressList.Length() > 0)
882 								addressList << ", ";
883 							addressList << address;
884 						}
885 					} else {
886 						enclosure = true;
887 						message.AddRef("refs", &ref);
888 					}
889 				}
890 			}
891 
892 			if (addressList.Length() > 0) {
893 				BTextView *textView = TextView();
894 				int end = textView->TextLength();
895 				if (end != 0) {
896 					textView->Select(end, end);
897 					textView->Insert(", ");
898 				}
899 				textView->Insert(addressList.String());
900 			}
901 
902 			if (enclosure)
903 				Window()->PostMessage(&message, Window());
904 			break;
905 		}
906 
907 		case M_SELECT:
908 		{
909 			BTextView *textView = (BTextView *)ChildAt(0);
910 			if (textView != NULL)
911 				textView->Select(0, textView->TextLength());
912 			break;
913 		}
914 
915 		case kMsgAddressChosen: {
916 			BString display;
917 			BString address;
918 			entry_ref ref;
919 
920 			if (msg->FindString("address", &address) != B_OK
921 				|| msg->FindRef("ref", &ref) != B_OK)
922 				return;
923 
924 			if (address.Length() > 0) {
925 				BString name;
926 				BNode node(&ref);
927 
928 				display = address;
929 
930 				node.ReadAttrString("META:name", &name);
931 				if (name.Length() > 0) {
932 					display = "";
933 					display << "\"" << name << "\" <" << address << ">";
934 				}
935 
936 				BTextView *textView = TextView();
937 				int end = textView->TextLength();
938 				if (end != 0) {
939 					textView->Select(end, end);
940 					textView->Insert(", ");
941 				}
942 				textView->Insert(display.String());
943 			}
944 			break;
945 		}
946 
947 		default:
948 			// BTextControl::MessageReceived(msg);
949 			BComboBox::MessageReceived(msg);
950 	}
951 }
952 
953 
954 bool
955 TTextControl::HasFocus()
956 {
957 	return TextView()->IsFocus();
958 }
959 
960 
961 //	#pragma mark - QPopupMenu
962 
963 
964 QPopupMenu::QPopupMenu(const char *title)
965 	: QueryMenu(title, true),
966 	fGroups(0)
967 {
968 }
969 
970 
971 void
972 QPopupMenu::AddPersonItem(const entry_ref *ref, ino_t node, BString &name,
973 	BString &email, const char *attr, BMenu *groupMenu, BMenuItem *superItem)
974 {
975 	BString	label;
976 	BString	sortKey;
977 		// For alphabetical order sorting, usually last name.
978 
979 	// if we have no Name, just use the email address
980 	if (name.Length() == 0) {
981 		label = email;
982 		sortKey = email;
983 	} else {
984 		// otherwise, pretty-format it
985 		label << name << " (" << email << ")";
986 
987 		// Extract the last name (last word in the name),
988 		// removing trailing and leading spaces.
989 		const char *nameStart = name.String();
990 		const char *string = nameStart + strlen(nameStart) - 1;
991 		const char *wordEnd;
992 
993 		while (string >= nameStart && isspace(*string))
994 			string--;
995 		wordEnd = string + 1; // Points to just after last word.
996 		while (string >= nameStart && !isspace(*string))
997 			string--;
998 		string++; // Point to first letter in the word.
999 		if (wordEnd > string)
1000 			sortKey.SetTo(string, wordEnd - string);
1001 		else // Blank name, pretend that the last name is after it.
1002 			string = nameStart + strlen(nameStart);
1003 
1004 		// Append the first names to the end, so that people with the same last
1005 		// name get sorted by first name.  Note no space between the end of the
1006 		// last name and the start of the first names, but that shouldn't
1007 		// matter for sorting.
1008 		sortKey.Append(nameStart, string - nameStart);
1009 	}
1010 
1011 	// The target (a TTextControl) will examine all the People files specified
1012 	// and add the emails and names to the string it is displaying (same code
1013 	// is used for drag and drop of People files).
1014 	BMessage *msg = new BMessage(B_SIMPLE_DATA);
1015 	msg->AddRef("refs", ref);
1016 	msg->AddInt64("node", node);
1017 	if (attr) // For nonstandard e-mail attributes, like META:email3
1018 		msg->AddString("attr", attr);
1019 	msg->AddString("sortkey", sortKey);
1020 
1021 	BMenuItem *newItem = new BMenuItem(label.String(), msg);
1022 	if (fTargetHandler)
1023 		newItem->SetTarget(fTargetHandler);
1024 
1025 	// If no group, just add it to ourself; else add it to group menu
1026 	BMenu *parentMenu = groupMenu ? groupMenu : this;
1027 	if (groupMenu) {
1028 		// Add ref to group super item.
1029 		BMessage *superMsg = superItem->Message();
1030 		superMsg->AddRef("refs", ref);
1031 	}
1032 
1033 	// Add it to the appropriate menu.  Use alphabetical order by sortKey to
1034 	// insert it in the right spot (a dumb linear search so this will be slow).
1035 	// Start searching from the end of the menu, since the main menu includes
1036 	// all the groups at the top and we don't want to mix it in with them.
1037 	// Thus the search starts at the bottom and ends when we hit a separator
1038 	// line or the top of the menu.
1039 
1040 	int32 index = parentMenu->CountItems();
1041 	while (index-- > 0) {
1042 		BMenuItem *item = parentMenu->ItemAt(index);
1043 		if (item == NULL ||	dynamic_cast<BSeparatorItem *>(item) != NULL)
1044 			break;
1045 
1046 		BMessage *message = item->Message();
1047 		BString key;
1048 
1049 		// Stop when testKey < sortKey.
1050 		if (message != NULL
1051 			&& message->FindString("sortkey", &key) == B_OK
1052 			&& ICompare(key, sortKey) < 0)
1053 			break;
1054 	}
1055 
1056 	if (!parentMenu->AddItem(newItem, index + 1)) {
1057 		fprintf (stderr, "QPopupMenu::AddPersonItem: Unable to add menu "
1058 			"item \"%s\" at index %ld.\n", sortKey.String(), index + 1);
1059 		delete newItem;
1060 	}
1061 }
1062 
1063 
1064 void
1065 QPopupMenu::EntryCreated(const entry_ref &ref, ino_t node)
1066 {
1067 	BNode file;
1068 	if (file.SetTo(&ref) < B_OK)
1069 		return;
1070 
1071 	// Make sure the pop-up menu is ready for additions.  Need a bunch of
1072 	// groups at the top, a divider line, and miscellaneous people added below
1073 	// the line.
1074 
1075 	int32 items = CountItems();
1076 	if (!items)
1077 		AddSeparatorItem();
1078 
1079 	// Does the file have a group attribute?  OK to have none.
1080 	BString groups;
1081 	const char *kNoGroup = "NoGroup!";
1082 	file.ReadAttrString("META:group", &groups);
1083 	if (groups.Length() <= 0)
1084 		groups = kNoGroup;
1085 
1086 	// Add the e-mail address to the all people group.  Then add it to all the
1087 	// group menus that it exists in (based on the comma separated list of
1088 	// groups from the People file), optionally making the group menu if it
1089 	// doesn't exist.  If it's in the special NoGroup!  list, then add it below
1090 	// the groups.
1091 
1092 	bool allPeopleGroupDone = false;
1093 	BMenu *groupMenu;
1094 	do {
1095 		BString group;
1096 
1097 		if (!allPeopleGroupDone) {
1098 			// Create the default group for all people, if it doesn't exist yet.
1099 			group = "All People";
1100 			allPeopleGroupDone = true;
1101 		} else {
1102 			// Break out the next group from the comma separated string.
1103 			int32 comma;
1104 			if ((comma = groups.FindFirst(',')) > 0) {
1105 				groups.MoveInto(group, 0, comma);
1106 				groups.Remove(0, 1);
1107 			} else
1108 				group.Adopt(groups);
1109 		}
1110 
1111 		// trim white spaces
1112 		int32 i = 0;
1113 		for (i = 0; isspace(group.ByteAt(i)); i++) {}
1114 		if (i)
1115 			group.Remove(0, i);
1116 		for (i = group.Length() - 1; isspace(group.ByteAt(i)); i--) {}
1117 		group.Truncate(i + 1);
1118 
1119 		groupMenu = NULL;
1120 		BMenuItem *superItem = NULL; // Corresponding item for group menu.
1121 
1122 		if (group.Length() > 0 && group != kNoGroup) {
1123 			BMenu *sub;
1124 
1125 			// Look for submenu with label == group name
1126 			for (int32 i = 0; i < items; i++) {
1127 				if ((sub = SubmenuAt(i)) != NULL) {
1128 					superItem = sub->Superitem();
1129 					if (!strcmp(superItem->Label(), group.String())) {
1130 						groupMenu = sub;
1131 						i++;
1132 						break;
1133 					}
1134 				}
1135 			}
1136 
1137 			// If no submenu, create one
1138 			if (!groupMenu) {
1139 				// Find where it should go (alphabetical)
1140 				int32 mindex = 0;
1141 				for (; mindex < fGroups; mindex++) {
1142 					if (strcmp(ItemAt(mindex)->Label(), group.String()) > 0)
1143 						break;
1144 				}
1145 
1146 				groupMenu = new BMenu(group.String());
1147 				groupMenu->SetFont(be_plain_font);
1148 				AddItem(groupMenu, mindex);
1149 
1150 				superItem = groupMenu->Superitem();
1151 				superItem->SetMessage(new BMessage(B_SIMPLE_DATA));
1152 				if (fTargetHandler)
1153 					superItem->SetTarget(fTargetHandler);
1154 
1155 				fGroups++;
1156 			}
1157 		}
1158 
1159 		BString	name;
1160 		file.ReadAttrString("META:name", &name);
1161 
1162 		BString email;
1163 		file.ReadAttrString("META:email", &email);
1164 
1165 		if (email.Length() != 0 || name.Length() != 0)
1166 			AddPersonItem(&ref, node, name, email, NULL, groupMenu, superItem);
1167 
1168 		// support for 3rd-party People apps
1169 		for (int16 i = 2; i < 6; i++) {
1170 			char attr[16];
1171 			sprintf(attr, "META:email%d", i);
1172 			if (file.ReadAttrString(attr, &email) >= B_OK && email.Length() > 0)
1173 				AddPersonItem(&ref, node, name, email, attr, groupMenu, superItem);
1174 		}
1175 	} while (groups.Length() > 0);
1176 }
1177 
1178 
1179 void
1180 QPopupMenu::EntryRemoved(ino_t /*node*/)
1181 {
1182 }
1183 
1184