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