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