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