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