1 /*  $Id: message.cpp 1658 2009-10-19 20:35:45Z terpstra $
2  *
3  *  message.cpp - Handle a message/ command
4  *
5  *  Copyright (C) 2002 - Wesley W. Terpstra
6  *
7  *  License: GPL
8  *
9  *  Authors: 'Wesley W. Terpstra' <wesley@terpstra.ca>
10  *
11  *    This program is free software; you can redistribute it and/or modify
12  *    it under the terms of the GNU General Public License as published by
13  *    the Free Software Foundation; version 2.
14  *
15  *    This program is distributed in the hope that it will be useful,
16  *    but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  *    GNU General Public License for more details.
19  *
20  *    You should have received a copy of the GNU General Public License
21  *    along with this program; if not, write to the Free Software
22  *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
23  */
24 
25 #define _FILE_OFFSET_BITS 64
26 
27 #include <mimelib/headers.h>
28 #include <mimelib/message.h>
29 #include <mimelib/datetime.h>
30 #include <mimelib/addrlist.h>
31 #include <mimelib/address.h>
32 #include <mimelib/group.h>
33 #include <mimelib/mboxlist.h>
34 #include <mimelib/mailbox.h>
35 #include <mimelib/text.h>
36 #include <mimelib/enum.h>
37 #include <mimelib/body.h>
38 #include <mimelib/bodypart.h>
39 #include <mimelib/utility.h>
40 #include <mimelib/disptype.h>
41 #include <mimelib/param.h>
42 
43 #include <CharsetEscape.h>
44 #include <XmlEscape.h>
45 #include <Keys.h>
46 
47 #include <fstream>
48 #include <cstdio>
49 #include <cstring>
50 #include <cerrno>
51 
52 #include <unistd.h>
53 #include <sys/wait.h>
54 
55 #include "commands.h"
56 #include "Threading.h"
57 #include "Search.h"
58 #include "Cache.h"
59 
60 #define OLD_PGP_HEADER	"-----BEGIN PGP SIGNED MESSAGE-----\n"
61 #define OLD_PGP_DIVIDER	"-----BEGIN PGP SIGNATURE-----\n"
62 #define OLD_PGP_ENDER	"-----END PGP SIGNATURE-----\n"
63 
64 const char* find_art_end(const char* start, const char* end);
65 const char* find_quote_end(const char* start, const char* end);
66 const char* find_email_end(const char* start, const char* end);
67 const char* find_url_end(const char* start, const char* end);
68 char* find_art_starts(const char* start, const char* end, char* scratch);
69 char* find_quote_starts(const char* start, const char* end, char* scratch);
70 char* find_email_starts(const char* start, const char* end, char* scratch);
71 char* find_url_starts(const char* start, const char* end, char* scratch);
72 
my_service_mailto(ostream & o,char * s0,const char * b0,const char * bE,const Config & cfg)73 void my_service_mailto(
74 	ostream&		o,
75 	char*                   s0,
76 	const char*		b0,
77 	const char*		bE,
78 	const Config&		cfg)
79 {
80 	if (b0 == bE) return;
81 
82 	char* sE = s0+(bE-b0);
83 	char* si = s0;
84 	const char* bi = b0;
85 
86 	find_email_starts(b0, bE, s0);
87 	while (si != sE)
88         {
89 		if (*si)
90 		{
91 			o << xmlEscape << string(bi, (si-s0)-(bi-b0));
92 			bi = b0 + (si-s0);
93 			const char* bj = find_email_end(bi, bE);
94 			if (cfg.hide_email)
95 			{
96 				string addr(bi, bj-bi);
97 				string::size_type l = addr.find('@');
98 				if (l != string::npos) addr.resize(l);
99 				o << xmlEscape << addr << "@???";
100 			}
101 			else
102 			{
103 				o << "<mailto>";
104 				o << xmlEscape << string(bi, bj-bi);
105 				o << "</mailto>";
106 			}
107 			bi = bj;
108 			si = s0 + (bi - b0);
109 		}
110 		else
111 		{
112 			++si;
113 		}
114 	}
115 
116 	o << xmlEscape << string(bi, bE-bi);
117 }
118 
my_service_url(ostream & o,char * s0,const char * b0,const char * bE,const Config & cfg)119 void my_service_url(
120 	ostream&		o,
121 	char*			s0,
122 	const char*		b0,
123 	const char*		bE,
124 	const Config&		cfg)
125 {
126 	if (b0 == bE) return;
127 
128 	char* sE = s0+(bE-b0);
129 	char* si = s0;
130 	const char* bi = b0;
131 
132 	find_url_starts(b0, bE, s0);
133 	while (si != sE)
134         {
135 		if (*si)
136 		{
137 			my_service_mailto(o, s0, bi, b0+(si-s0), cfg);
138 			bi = b0 + (si-s0);
139 			const char* bj = find_url_end(bi, bE);
140 			o << "<url>";
141 			o << xmlEscape << string(bi, bj-bi);
142 			o << "</url>";
143 			bi = bj;
144 			si = s0 + (bi - b0);
145 		}
146 		else
147 		{
148 			++si;
149 		}
150 	}
151 	my_service_mailto(o, s0, bi, bE, cfg);
152 }
153 
my_service_art(ostream & o,char * s0,const char * b0,const char * bE,const Config & cfg)154 void my_service_art(
155 	ostream&		o,
156 	char*			s0,
157 	const char*		b0,
158 	const char*		bE,
159 	const Config&		cfg)
160 {
161 	if (b0 == bE) return;
162 
163 	char* sE = s0+(bE-b0);
164 	char* si = s0;
165 	const char* bi = b0;
166 
167 	find_art_starts(b0, bE, s0);
168 	while (si != sE)
169         {
170 		if (*si)
171 		{
172 			my_service_url(o, s0, bi, b0+(si-s0), cfg);
173 			o << "<art>";
174 			bi = b0 + (si-s0);
175 			const char* bj = find_art_end(bi, bE);
176 			my_service_url(o, s0, bi, bj, cfg);
177 			o << "</art>";
178 			bi = bj;
179 			si = s0 + (bi - b0);
180 		}
181 		else
182 		{
183 			++si;
184 		}
185 	}
186 	my_service_url(o, s0, bi, bE, cfg);
187 }
188 
my_service_quote(ostream & o,char * s0,const char * b0,const char * bE,const Config & cfg)189 void my_service_quote(
190 	ostream&		o,
191 	char*			s0,
192 	const char*		b0,
193 	const char*		bE,
194 	const Config&		cfg)
195 {
196 	if (b0 == bE) return;
197 
198 	char* sE = s0+(bE-b0);
199 	char* si = s0;
200 	const char* bi = b0;
201 
202 	find_quote_starts(b0, bE, s0);
203 	while (si != sE)
204         {
205 		if (*si)
206 		{
207 			my_service_art(o, s0, bi, b0+(si-s0), cfg);
208 			o << "<quote>";
209 			bi = b0 + (si-s0);
210 			const char* bj = find_quote_end(bi, bE);
211 			my_service_art(o, s0, bi, bj, cfg);
212 			o << "</quote>";
213 			bi = bj;
214 			si = s0 + (bi - b0);
215 		}
216 		else
217 		{
218 			++si;
219 		}
220 	}
221 
222 	my_service_art(o, s0, bi, bE, cfg);
223 }
224 
my_service_process(ostream & o,const char * b0,long len,const Config & cfg)225 void my_service_process(
226 	ostream&		o,
227 	const char*		b0,
228 	long			len,
229 	const Config&		cfg)
230 {
231 	const char* bE = b0 + len;
232 	char* s0 = new char [len];
233 	my_service_quote(o, s0, b0, bE, cfg);
234 	delete s0;
235 }
236 
find_and_replace(string & target,const string & token,const string & value)237 static void find_and_replace(string& target, const string& token, const string& value)
238 {
239 	string::size_type x = 0;
240 	while ((x = target.find(token, x)) != string::npos)
241 		target.replace(x, token.length(), value);
242 }
243 
244 const Config* pgp_config = 0;
245 string pgp_name_prefix;
246 int pgp_part = 0;
247 
pgp_tmpfile(const string & type)248 string pgp_tmpfile(const string& type)
249 {
250 	char buf[10];
251 	sprintf(buf, "%d", pgp_part);
252 	return string("../attach/") + buf + "@" + pgp_name_prefix + "." + type;
253 }
254 
pgp_writefile(ostream & o,const DwString & data)255 void pgp_writefile(ostream& o, const DwString& data)
256 {
257 	size_t s, e;
258 
259 	s = 0;
260 	while (1) // signed data must have CRLF endcoding
261 	{
262 		e = data.find_first_of("\r\n", s);
263 		if (e == DwString::npos) break;
264 
265 		o.write(data.c_str() + s, e - s);
266 		if (data[e] == '\n') o << "\r\n";
267 		s = e+1;
268 	}
269 
270 	o.write(data.c_str() + s, data.length() - s);
271 }
272 
run_pgp(ostream & o,string & command)273 void run_pgp(ostream& o, string& command)
274 {
275 	string photo = pgp_tmpfile("photo");
276 	find_and_replace(command, "%p", photo);
277 
278 	string details;
279 	int status;
280 
281 	FILE* pgp = popen(command.c_str(), "r");
282 	if (pgp != 0)
283 	{
284 		char buf[1024];
285 		size_t got;
286 		while ((got = fread(buf, 1, sizeof(buf), pgp)) > 0)
287 		{
288 			details.append(buf, got);
289 			if (got != sizeof(buf)) break;
290 		}
291 
292 		status = pclose(pgp);
293 		if (WIFEXITED(status))
294 		{
295 			status = WEXITSTATUS(status);
296 		}
297 		else
298 		{
299 			details += "\n" + command + " exited abnormally";
300 			status = 2;
301 		}
302 	}
303 	else
304 	{
305 		details = command + " failed with " + strerror(errno);
306 		status = 2;
307 	}
308 
309 	o << "<signed ok=\"";
310 
311 	if      (status == 0) o << "yes";
312 	else if (status == 1) o << "no";
313 	else                  o << "unknown";
314 
315 	o << "\">"
316 	  << "<details>" << xmlEscape << details << "</details>";
317 
318 	if (access(photo.c_str(), R_OK) == 0)
319 	{
320 		o << "<photo>" << photo << "</photo>";
321 	}
322 }
323 
handle_signed_inline(ostream & o,const DwString & s)324 bool handle_signed_inline(ostream& o, const DwString& s)
325 {
326 	string command = pgp_config->pgpv_inline;
327 	if (command == "off") return false;
328 
329 	string cleartext = pgp_tmpfile("cleartext");
330 	find_and_replace(command, "%b", cleartext);
331 
332 	if (1)
333 	{ // create the cleartext
334 		std::ofstream body(cleartext.c_str());
335 		pgp_writefile(body, s);
336 	}
337 
338 	run_pgp(o, command);
339 	return true;
340 }
341 
handle_signed_mime(ostream & o,DwEntity & e)342 bool handle_signed_mime(ostream& o, DwEntity& e)
343 {
344 	// rfc 1847 says we have 2 bodyparts:
345 	//  1. the original data
346 	//  2. the signature
347 
348 	DwBodyPart* body = e.Body().FirstBodyPart();
349 	if (!body) return false;
350 	DwBodyPart* sig = body->Next();
351 	if (!sig) return false;
352 	if (sig->Next() != 0) return false;
353 
354 	// signature has no type
355 	if (!sig->Headers().HasContentType() ||
356 	    sig->Headers().ContentType().Type() != DwMime::kTypeApplication)
357 		return false;
358 
359 	DwString st = sig->Headers().ContentType().SubtypeStr();
360 	st.ConvertToLowerCase();
361 	if (st != "pgp-signature")
362 		return false;
363 
364 	string command = pgp_config->pgpv_mime;
365 	if (command == "off") return false;
366 
367 	string cleartext = pgp_tmpfile("cleartext");
368 	string signature = pgp_tmpfile("signature");
369 	find_and_replace(command, "%b", cleartext);
370 	find_and_replace(command, "%s", signature);
371 
372 	if (1)
373 	{ // create the cleartext
374 		std::ofstream bodyf(cleartext.c_str());
375 		pgp_writefile(bodyf, body->AsString());
376 	}
377 
378 	if (1)
379 	{ // create the signature
380 		std::ofstream sigf(signature.c_str());
381 		pgp_writefile(sigf, sig->Body().AsString());
382 	}
383 
384 	run_pgp(o, command);
385 	return true;
386 }
387 
process_text(ostream & o,bool html,const string & charset,const DwString & out,const Config & cfg)388 void process_text(ostream& o, bool html, const string& charset, const DwString& out, const Config& cfg)
389 {
390 	CharsetEscape decode(charset.c_str());
391 	string utf8 = decode.write(out.c_str(), out.length());
392 
393 	if (!decode.valid())
394 	{
395 		utf8 = "<-- Warning: charset '" + charset + "' is not supported -->\n\n"
396 		     + utf8;
397 	}
398 
399 	if (html)
400 	{
401 		string::size_type start, end;
402 
403 		start = 0;
404 		while ((end = utf8.find('<', start)) != string::npos)
405 		{
406 			my_service_process(o, utf8.c_str()+start, end-start, cfg);
407 			start = utf8.find('>', end);
408 
409 			if (start == string::npos) break;
410 			++start;
411 		}
412 
413 		// deal with half-open tag at end of input
414 		if (start != string::npos)
415 			my_service_process(o, utf8.c_str()+start, utf8.length()-start, cfg);
416 	}
417 	else
418 	{
419 		my_service_process(o, utf8.c_str(), utf8.length(), cfg);
420 	}
421 }
422 
message_display(ostream & o,DwEntity & e,const string & charset,bool html,const Config & cfg)423 void message_display(ostream& o, DwEntity& e, const string& charset, bool html, const Config& cfg)
424 {
425 	// Oldschool pgp usually works by invoking a helper program which
426 	// cannot control how the email client then encodes the signed data.
427 	// Hence we nede to decode the transfer-encoding for verification
428 	// to get back what the helper program probably gave the MUA.
429 
430 	DwString out;
431 	// if (e.hasHeaders() &&
432 	if (e.Headers().HasContentTransferEncoding())
433 	{
434 		switch (e.Headers().ContentTransferEncoding().AsEnum())
435 		{
436 		case DwMime::kCteQuotedPrintable:
437 			DwDecodeQuotedPrintable(e.Body().AsString(), out);
438 			break;
439 
440 		case DwMime::kCteBase64:
441 			DwDecodeBase64(e.Body().AsString(), out);
442 			break;
443 
444 		case DwMime::kCteNull:
445 		case DwMime::kCteUnknown:
446 		case DwMime::kCte7bit:
447 		case DwMime::kCte8bit:
448 		case DwMime::kCteBinary:
449 			out = e.Body().AsString();
450 			break;
451 		}
452 
453 	}
454 	else
455 	{
456 		out = e.Body().AsString();
457 	}
458 
459 	// We do NOT convert the charset because the user probably signed
460 	// the text in the charset as which it was delivered. If it wasn't,
461 	// we wouldn't be able to help anyways because we don't know what
462 	// to convert to.
463 
464 	size_t pgp_last, pgp_header, pgp_divider, pgp_ender;
465 	for (pgp_last = 0;
466 	     ((pgp_header  = out.find(OLD_PGP_HEADER,  pgp_last))   != DwString::npos) &&
467 	     ((pgp_divider = out.find(OLD_PGP_DIVIDER, pgp_header)) != DwString::npos) &&
468 	     ((pgp_ender   = out.find(OLD_PGP_ENDER,   pgp_divider))!= DwString::npos);
469 	     pgp_last = pgp_ender)
470 	{
471 		pgp_ender += sizeof(OLD_PGP_ENDER)-1; // include endline, not null
472 
473 		// deal with leading text (substr is copy-free)
474 		process_text(o, html, charset,
475 			out.substr(pgp_last, pgp_header-pgp_last), cfg);
476 
477 		bool signOpen = false;
478 		if (handle_signed_inline(o,
479 			out.substr(pgp_header, pgp_ender-pgp_header)))
480 		{
481 			signOpen = true;
482 			o << "<data>";
483 		}
484 
485 		// skip the header + hash line + blank line
486 		// (safe b/c we have 3 \n s for sure)
487 		pgp_header += sizeof(OLD_PGP_HEADER)-1; // eol, !null
488 		pgp_header = out.find('\n', pgp_header) + 1;
489 		pgp_header = out.find('\n', pgp_header) + 1;
490 
491 		if (pgp_header < pgp_divider)
492 		{
493 			// signed text
494 			process_text(o, html, charset,
495 				out.substr(pgp_header, pgp_divider-pgp_header), cfg);
496 		}
497 
498 		if (signOpen)
499 		{
500 			o << "</data></signed>";
501 		}
502 	}
503 	// trailing text
504 	process_text(o, html, charset,
505 		out.substr(pgp_last, out.length()-pgp_last), cfg);
506 }
507 
508 // this will only output mime information if the dump is false
message_build(ostream & o,DwEntity & e,const string & parentCharset,bool dump,long & x,const Config & cfg)509 void message_build(ostream& o, DwEntity& e,
510 	const string& parentCharset, bool dump, long& x, const Config& cfg)
511 {
512 	// We are the requested entity.
513 	pgp_part = ++x;
514 
515 	string charset = parentCharset;
516 	string type = "text/plain";
517 	string name = "";
518 
519 	// if (e.hasHeaders() &&
520 	if (e.Headers().HasContentType())
521 	{
522 		DwMediaType& mt = e.Headers().ContentType();
523 
524 		DwString ftype = mt.TypeStr() + "/" + mt.SubtypeStr();
525 		ftype.ConvertToLowerCase();
526 		type = ftype.c_str();
527 		name = mt.Name().c_str();
528 
529 		for (DwParameter* p = mt.FirstParameter(); p; p = p->Next())
530 		{
531 			DwString attr = p->Attribute();
532 			attr.ConvertToLowerCase(); // case insens
533 			if (attr == "charset") charset = p->Value().c_str();
534 		}
535 	}
536 
537 	if (e.Headers().HasContentDisposition())
538 	{
539 		DwDispositionType& dt = e.Headers().ContentDisposition();
540 		if (dt.Filename() != "")
541 			name = dt.Filename().c_str();
542 	}
543 
544 	// The question is: which charset affects the headers?
545 	// I claim that the parent charset does - this is being friendly
546 	// anyways since one shouldn't have non us-ascii in the headers
547 	CharsetEscape ches(parentCharset.c_str());
548 	o << "<mime id=\"" << x << "\" type=\"" << xmlEscape << ches.write(type) << "\"";
549 	if (name != "") o << " name=\"" << xmlEscape << ches.write(name) << "\"";
550 	o << ">";
551 
552 	bool signedopen = false;
553 
554 	// if (e.hasHeaders() &&
555 	if (e.Headers().HasContentType())
556 	{
557 		DwMediaType& t = e.Headers().ContentType();
558 		switch (t.Type())
559 		{
560 		case DwMime::kTypeMessage:
561 			if (e.Body().Message())
562 				message_build(o, *e.Body().Message(), charset, dump, x, cfg);
563 			break;
564 
565 		case DwMime::kTypeMultipart:
566 			if (1)
567 			{ // scope in the string
568 				DwString s = t.SubtypeStr();
569 				s.ConvertToLowerCase();
570 				if (s == "signed")
571 				{	// verify the signature
572 					signedopen = handle_signed_mime(o, e);
573 				}
574 			}
575 
576 			// first body part is the signed data
577 			if (signedopen)	o << "<data>";
578 
579 			for (DwBodyPart* p = e.Body().FirstBodyPart(); p != 0; p = p->Next())
580 			{
581 				bool plain = false;
582 				if (p->Headers().HasContentType())
583 				{
584 					DwMediaType& mt = p->Headers().ContentType();
585 
586 					plain =	mt.Type()    == DwMime::kTypeText &&
587 						mt.Subtype() == DwMime::kSubtypePlain;
588 				}
589 
590 				if (t.Subtype() != DwMime::kSubtypeAlternative ||
591 				    p->Next() == 0 || plain)
592 				{	// display all parts, or plain, or last
593 					message_build(o, *p, charset, dump, x, cfg);
594 
595 					// if we printed something, we are done
596 					if (t.Subtype() == DwMime::kSubtypeAlternative)
597 						dump = false;
598 				}
599 				else
600 				{
601 					message_build(o, *p, charset, false, x, cfg);
602 				}
603 
604 				if (signedopen)
605 				{	// done the first section which was signed
606 					o << "</data></signed>";
607 					signedopen = false;
608 				}
609 			}
610 			break;
611 
612 		case DwMime::kTypeText:
613 			if (dump) message_display(o, e, charset, t.Subtype() == DwMime::kSubtypeHtml, cfg);
614 			break;
615 		}
616 	}
617 	else
618 	{
619 		if (dump) message_display(o, e, charset, false, cfg);
620 	}
621 
622 	o << "</mime>";
623 }
624 
message_format_address(ostream & o,DwAddress * a,const string & charset,const Config & cfg)625 void message_format_address(ostream& o, DwAddress* a, const string& charset, const Config& cfg)
626 {
627 	for (; a != 0; a = a->Next())
628 	{
629 		if (a->IsGroup())
630 		{
631 			DwGroup* g = dynamic_cast<DwGroup*>(a);
632 			if (g)
633 				message_format_address(
634 					o,
635 					g->MailboxList().FirstMailbox(),
636 					charset, cfg);
637 		}
638 		else
639 		{
640 			DwMailbox* m = dynamic_cast<DwMailbox*>(a);
641 			if (m)
642 			{
643 				string name = m->FullName().c_str();
644 				if (name.length() >= 2 && name[0] == '"')
645 					name = name.substr(1, name.length()-2);
646 				if (name == "")
647 					name = m->LocalPart().c_str();
648 
649 				// Deal with the horror
650 				name = decode_header(name, charset.c_str());
651 				if (name.length() >= 2 && name[0] == '"')
652 					name = name.substr(1, name.length()-2);
653 
654 				DwString addr = m->LocalPart() + "@" + m->Domain();
655 				for (size_t i = 0; i < addr.length(); ++i)
656 				{
657 					if (addr[i] <= 0x20 || addr[i] >= 0x7f)
658 					{	// fucked up address
659 						addr = "";
660 						break;
661 					}
662 				}
663 				if (addr.length() > 128) addr = "";
664 
665 				o << "<email";
666 				if (name != "")
667 					o << " name=\"" << xmlEscape
668 					  << whitespace_sanitize(name) << "\"";
669 				if (addr != "" && !cfg.hide_email)
670 					o << " address=\"" << xmlEscape
671 					  << addr.c_str() << "\"";
672 				o << "/>";
673 			}
674 		}
675 	}
676 }
677 
678 struct MBox
679 {
680 	List	cfg;
681 	Summary prev;
682 	Summary next;
683 
MBoxMBox684 	MBox() { }
MBoxMBox685 	MBox(const List& cfg_) : cfg(cfg_) { }
686 
687 	string load(ESort::Reader* db, const MessageId& rel, const Config& cfg);
688 };
689 
load(ESort::Reader * db,const MessageId & rel,const Config & conf)690 string MBox::load(ESort::Reader* db, const MessageId& rel, const Config& conf)
691 {
692 	string ok;
693 	vector<Summary> sum;
694 
695 	Search n(conf, db, Forward, rel);
696 	n.keyword(LU_KEYWORD_LIST + cfg.mbox);
697 
698 	if (!n.pull(2, sum)) return "Pulling next two failed";
699 
700 	if (sum.size() < 1 || sum[0].id() != rel)
701 		return "Relative message does not exist";
702 
703 	if (sum.size() >= 2)
704 	{
705 		next = sum[1];
706 		if ((ok = next.load(db, conf)) != "") return ok;
707 	}
708 
709 	sum.clear();
710 
711 	Search p(conf, db, Backward, rel);
712 	p.keyword(LU_KEYWORD_LIST + cfg.mbox);
713 
714 	if (!p.pull(1, sum)) return "Pulling previous failed";
715 
716 	if (sum.size() >= 1)
717 	{
718 		prev = sum[0];
719 		if ((ok = prev.load(db, conf)) != "") return ok;
720 	}
721 
722 	return "";
723 }
724 
handle_message(const Config & cfg,ESort::Reader * db,const string & param)725 int handle_message(const Config& cfg, ESort::Reader* db, const string& param)
726 {
727 	Request req = parse_request(param);
728 	cfg.options = req.options;
729 
730 	if (!MessageId::is_full(req.options.c_str()) ||
731 	    req.options.length() != MessageId::full_len)
732 		error(_("Bad request"), param,
733 		      _("The given parameter was not of the correct format. "
734 		        "A message request must be formatted like: "
735 		        "message/YYYYMMDD.HHMMSS.hashcode.lc.xml"));
736 
737 	MessageId id(req.options.c_str());
738 
739 	pgp_config = &cfg; // hackish
740 	pgp_name_prefix = id.serialize();
741 
742 	string ok;
743 
744 	Summary source(id);
745 	// Identical error if missing or forbidden (security)
746 	if ((ok = source.load(db, cfg)) != "" || !source.allowed())
747 	{
748 		if (ok == "") ok = "not in a mailbox"; // fake
749 		error(_("Database message source pull failure"), ok,
750 		      _("The specified message does not exist."));
751 	}
752 
753 	if (source.deleted())
754 		error(_("Database message source pull failure"), "not found",
755 		      _("The specified message has been deleted."));
756 
757 	Threading::Key spot;
758 	Threading thread;
759 	if ((ok = thread.load(db, source, spot)) != "" ||
760 	    (ok = thread.draw_snippet(db, spot, cfg)) != "")
761 		error(_("Database message tree load failure"), ok,
762 		      _("Something internal to the database failed. "
763 		        "Please contact the lurker user mailing list for "
764 		        "further assistence."));
765 
766 	Summary thread_prev;
767 	Summary thread_next;
768 
769 	if ((ok = thread.findprev(spot, db, cfg, thread_prev)) != "" ||
770 	    (ok = thread.findnext(spot, db, cfg, thread_next)) != "")
771 		error(_("Thread prev/next load failure"), ok,
772 		      _("Something internal to the database failed. "
773 		        "Please contact the lurker user mailing list for "
774 		        "further assistence."));
775 
776 	DwMessage message;
777 	if ((ok = source.message(cfg.dbdir, message)) != "")
778 		error(_("MBox read failure"), ok,
779 		      _("Unable to open message in the mailbox. "
780 		        "Perhaps it has been deleted or moved?"));
781 
782 	map<string, Summary> followups; // these are all followups NOT in the tree
783 	if (message.Headers().HasMessageId())
784 	{
785 		vector<string> mids = extract_message_ids(
786 			message.Headers().MessageId().AsString().c_str());
787 
788 		vector<Summary> sums;
789 		vector<Summary>::iterator sum;
790 		vector<string>::iterator mid;
791 
792 		for (mid = mids.begin(); mid != mids.end(); ++mid)
793 		{
794 			// cout << "MID: " << *mid << "\n";
795 			Search k(cfg, db, Forward);
796 			k.keyword(LU_KEYWORD_REPLY_TO + *mid);
797 
798 			if (!k.pull(1000, sums))
799 				break;
800 		}
801 
802 		if (ok == "")
803 		for (sum = sums.begin(); sum != sums.end(); ++sum)
804 		{
805 			// cout << "SUM: " << *sum << "\n";
806 			string hash = sum->id().hash();
807 			if (thread.hasMessage(hash)) continue;
808 			if (followups.find(hash) != followups.end()) continue;
809 			followups[hash] = *sum;
810 			if ((ok = followups[hash].load(db, cfg)) != "")
811 				break;
812 		}
813 	}
814 	if (ok != "")
815 		error(_("Database followups load failure"), ok,
816 		      _("Something internal to the database failed. "
817 		        "Please contact the lurker user mailing list for "
818 		        "further assistence."));
819 
820 	map<string, Summary> repliesTo; // what messages this one replies to
821 	if (message.Headers().HasInReplyTo())
822 	{
823 		vector<string> mids = extract_message_ids(
824 			message.Headers().InReplyTo().AsString().c_str());
825 
826 		vector<Summary> sums;
827 		vector<Summary>::iterator sum;
828 		vector<string>::iterator mid;
829 
830 		for (mid = mids.begin(); mid != mids.end(); ++mid)
831 		{
832 			Search k(cfg, db, Forward);
833 			k.keyword(LU_KEYWORD_MESSAGE_ID + *mid);
834 
835 			if (!k.pull(1000, sums))
836 				break;
837 		}
838 
839 		if (ok == "")
840 		for (sum = sums.begin(); sum != sums.end(); ++sum)
841 		{
842 			string hash = sum->id().hash();
843 			// only things not in the tree
844 			if (thread.hasMessage(hash)) continue;
845 			if (repliesTo.find(hash) != repliesTo.end()) continue;
846 			repliesTo[hash] = *sum;
847 			if ((ok = repliesTo[hash].load(db, cfg)) != "")
848 				break;
849 		}
850 	}
851 	if (ok != "")
852 		error(_("Database replies load failure"), ok,
853 		      _("Something internal to the database failed. "
854 		        "Please contact the lurker user mailing list for "
855 		        "further assistence."));
856 
857 	vector<MBox> boxes;
858 	set<string>::iterator mbox;
859 	for (mbox = source.mboxs().begin(); mbox != source.mboxs().end(); ++mbox)
860 	{
861 		Config::Lists::const_iterator j = cfg.lists.find(*mbox);
862 		if (j == cfg.lists.end()) continue; // impossible!
863 		if (!j->second.allowed) continue;
864 
865 		boxes.push_back(MBox(j->second));
866 		if ((ok = boxes.back().load(db, id, cfg)) != "") break;
867 	}
868 	if (ok != "")
869 		error(_("Database list links load failure"), ok,
870 		      _("Something internal to the database failed. "
871 		        "Please contact the lurker user mailing list for "
872 		        "further assistence."));
873 
874 	Cache cache(cfg, "message", param, req.ext);
875 
876 	cache.o << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
877 		<< "<?xml-stylesheet type=\"text/xsl\" href=\"../ui/message.xsl\"?>\n"
878 		<< "<message xml:lang=\"" << req.language << "\">\n"
879 		<< " <mode>" << req.ext << "</mode>\n"
880 		<< " " << cfg(req.language) << "\n"
881 		<< " " << source << "\n";
882 
883 	vector<MBox>::iterator m;
884 	for (m = boxes.begin(); m != boxes.end(); ++m)
885 	{
886 		cache.o	<< " <mbox>\n"
887 			<< "  " << m->cfg(req.language) << "\n";
888 
889 		if (m->next.id().timestamp() != 0)
890 			cache.o << "  <next>" << m->next << "</next>\n";
891 		if (m->prev.id().timestamp() != 0)
892 			cache.o << "  <prev>" << m->prev << "</prev>\n";
893 
894 		cache.o	<< " </mbox>\n";
895 	}
896 
897 	// Find the charset for the overall message, if any.
898 	string charset;
899 	if (message.Headers().HasContentType())
900 	{
901 		DwParameter* p = message.Headers().ContentType().FirstParameter();
902 		while (p)
903 		{
904 			if (p->Attribute() == "charset")
905 				charset = p->Value().c_str();
906 			p = p->Next();
907 		}
908 	}
909 
910 	// if (message.hasHeaders() &&
911 	if (message.Headers().HasTo())
912 	{
913 		cache.o	<< " <to>";
914 		message_format_address(
915 			cache.o,
916 			message.Headers().To().FirstAddress(),
917 			charset, cfg);
918 		cache.o	<< "</to>\n";
919 	}
920 
921 	// if (message.hasHeaders() &&
922 	if (message.Headers().HasCc())
923 	{
924 		cache.o	<< " <cc>";
925 		message_format_address(
926 			cache.o,
927 			message.Headers().Cc().FirstAddress(),
928 			charset, cfg);
929 		cache.o	<< "</cc>\n";
930 	}
931 
932 	// Output the snippet
933 	cache.o << " <threading>\n  <snippet>\n   <tree>";
934 	int head = -2; // magic, don't ask.
935 	thread.draw_snippet_row(cache.o, &head, 0, spot);
936 	cache.o << "</tree>\n   <tree>";
937 	thread.draw_snippet_row(cache.o, &head, 1, spot);
938 	cache.o << "</tree>\n   <tree>";
939 	thread.draw_snippet_row(cache.o, &head, 2, spot);
940 	cache.o << "</tree>\n";
941 
942 	// Output all summaries needed by the snippet
943 	Threading::Key i;
944 	for (i = 0; i < thread.size(); ++i)
945 	{
946 		Summary& sum = thread.getSummary(i);
947 		if (sum.loaded())
948 			cache.o << "   " << sum << "\n";
949 	}
950 
951 	cache.o << "  </snippet>\n";
952 	if (thread_prev.id() != id)
953 		cache.o << "  <prev>" << thread_prev << "</prev>\n";
954 	if (thread_next.id() != id)
955 		cache.o << "  <next>" << thread_next << "</next>\n";
956 
957 	if (!repliesTo.empty())
958 	{
959 		cache.o << "  <inreplyto>\n";
960 
961 		map<string, Summary>::iterator irt;
962 		for (irt = repliesTo.begin(); irt != repliesTo.end(); ++irt)
963 			cache.o << "   " << irt->second << "\n";
964 
965 		cache.o << "  </inreplyto>\n";
966 	}
967 
968 	if (!followups.empty())
969 	{
970 		cache.o << "  <drift>\n";
971 
972 		map<string, Summary>::iterator drift;
973 		for (drift = followups.begin(); drift != followups.end(); ++drift)
974 			cache.o << "   " << drift->second << "\n";
975 
976 		cache.o << "  </drift>\n";
977 	}
978 
979 #if 0
980 	// These are already included; don't print twice
981 	set<Summary> replies = thread.replies(spot);
982 	if (!replies.empty())
983 	{
984 		cache.o << "  <replies>\n";
985 
986 		set<Summary>::iterator rep;
987 		for (rep = replies.begin(); rep != replies.end(); ++rep)
988 			cache.o << "   " << *rep << "\n";
989 
990 		cache.o << "  </replies>\n";
991 
992 	}
993 #endif
994 
995 	cache.o << " </threading>\n";
996 
997 	if (message.Headers().HasMessageId())
998 	{
999 		vector<string> mids = extract_message_ids(
1000 			message.Headers().MessageId().AsString().c_str());
1001 		if (mids.size() > 0)
1002 			cache.o << " <message-id>" << xmlEscape << mids[0]
1003 				<< "</message-id>\n";
1004 	}
1005 
1006 	long aid = 0;
1007 	// default charset is ISO-8859-1
1008 	message_build(cache.o, message, "ISO-8859-1", true, aid, cfg);
1009 
1010 	cache.o	<< "</message>\n";
1011 
1012 	return 0;
1013 }
1014