]> git.neil.brown.name Git - edlib.git/blob - doc-email.c
TODO: clean out done items.
[edlib.git] / doc-email.c
1 /*
2  * Copyright Neil Brown ©2016-2023 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * doc-email: Present an email message as its intended content, with
6  * part recognition and decoding etc.
7  *
8  * A multipart document is created from sets of three documents.
9  * In each set:
10  * - The first is the original email, overlayed with 'crop' to select
11  *   one section, then overlayed with handlers for the transfer-encoding
12  *   and (optionally) charset.  There will be one for the headers
13  *   and either one for the body, or one for each part of the body
14  *   is multipart.  All nested multiparts have their parts linearized.
15  * - The second is a scratch text document which can contain a transformed
16  *   copy of the content when the tranformation is too complex for an
17  *   overlay layer.  This includes HTML and PDF which can be converted
18  *   to text, but the conversion is complex, and the headers, which need to be
19  *   re-ordered as well as filtered and decoded.  For images, this document has
20  *   trivial content but specifier a rendering function that displays the image.
21  * - The final section is a 'spacer' which has fixed content and displays
22  *   as a summary line for the part (e.g. MIME-type, file name) and
23  *   provides active buttons for acting on the content (save, external viewer
24  *   etc).
25  *
26  * The middle part has attributes set on the document which can be
27  * accessed from the spacer using "multipart-prev:"
28  * - email:path identify the part in the nexted multipart struture
29  *   e.g. "header", "body", "body,multipart/mixed:0,mulitpart/alternate:1"
30  * - email:actions a ':' separated list of buttons. "hide:save:view"
31  * - email:content-type the MIME type of the content. "image/png"
32  *
33  */
34
35 #define _GNU_SOURCE /* for asprintf */
36 #include <unistd.h>
37 #include <stdlib.h>
38 #include <fcntl.h>
39 #include <string.h>
40 #include <wctype.h>
41 #include <ctype.h>
42 #include <stdio.h>
43
44 #define PANE_DATA_TYPE struct email_view
45 #define DOC_NEXT email_next
46 #define DOC_PREV email_prev
47 #include "core.h"
48 #include "misc.h"
49
50 struct email_view {
51         int     parts;
52         char    *invis;
53 };
54
55 #include "core-pane.h"
56
57 static inline bool is_orig(int p)
58 {
59         return p >= 0 && p % 3 == 0;
60 }
61
62 static inline bool is_transformed(int p)
63 {
64         return p >= 0 && p % 3 == 1;
65 }
66
67 static inline bool is_spacer(int p)
68 {
69         return p >= 0 && p % 3 == 2;
70 }
71
72 static inline int to_orig(int p)
73 {
74         if (p < 0)
75                 return p;
76         return p - p % 3;
77 }
78
79 static bool handle_content(struct pane *p safe,
80                            char *type, char *xfer, char *disp,
81                            struct mark *start safe, struct mark *end safe,
82                            struct pane *mp safe, struct pane *spacer safe,
83                            char *path safe);
84 static bool handle_rfc822(struct pane *email safe,
85                           struct mark *start safe, struct mark *end safe,
86                           struct pane *mp safe, struct pane *spacer safe,
87                           char *path safe);
88
89 static bool cond_append(struct buf *b safe, char *txt safe, char *tag safe,
90                         int offset, int *pos safe)
91 {
92         char *tagf = "action-activate:email-";
93         int prelen = 1 + strlen(tagf) + strlen(tag) + 1 + 1;
94         int postlen = 1 + 3;
95         int len = prelen + strlen(txt) + postlen;
96         if (offset >= 0 && offset <= b->len + len)
97                 return False;
98         buf_concat(b, "<");
99         buf_concat(b, tagf);
100         buf_concat(b, tag);
101         buf_concat(b, ">[");
102         *pos = b->len;
103         buf_concat(b, txt);
104         buf_concat(b, "]</>");
105         return True;
106 }
107
108 static bool is_attr(char *a safe, char *attrs safe)
109 {
110         int l = strlen(a);
111         if (strncmp(a, attrs, l) != 0)
112                 return False;
113         if (attrs[l] == ':' || attrs[l] == '\0')
114                 return True;
115         return False;
116 }
117
118 DEF_CMD(email_spacer)
119 {
120         struct buf b;
121         int visible = 1;
122         int orig = 0;
123         int extras = 0;
124         struct mark *m = ci->mark;
125         struct mark *pm = ci->mark2;
126         int o = ci->num;
127         int cp = -1;
128         int ret_pos = 0;
129         char *attr;
130         int ret;
131         bool ok = True;
132
133         if (!m)
134                 return Enoarg;
135         if (pm) {
136                 /* Count the number of chars before the cursor.
137                  * This tells us which button to highlight.
138                  */
139                 cp = 0;
140                 pm = mark_dup(pm);
141                 while (!mark_ordered_or_same(pm, m)) {
142                         doc_prev(ci->focus, pm);
143                         cp += 1;
144                 }
145                 mark_free(pm);
146         }
147
148         buf_init(&b);
149         buf_concat(&b, "<fg:red>");
150
151         attr = pane_mark_attr(ci->home, m, "multipart-prev:email:path");
152         if (attr) {
153                 buf_concat(&b, attr);
154                 buf_concat(&b, " ");
155         }
156
157         attr = pane_mark_attr(ci->focus, m, "email:visible");
158         if (attr && strcmp(attr, "none") == 0)
159                 visible = 0;
160         if (attr && strcmp(attr, "orig") == 0)
161                 orig = 1;
162         attr = attr_find(ci->home->attrs, "email:extra-headers");
163         extras = attr && *attr;
164
165         attr = pane_mark_attr(ci->home, m, "multipart-prev:email:actions");
166         if (!attr)
167                 attr = "hide";
168
169         while (ok && attr && *attr) {
170                 int pos = 0;
171                 char *a = strchr(attr, ':');
172                 if (a)
173                         *a = 0;
174                 if (is_attr("hide", attr))
175                         ok = cond_append(&b, visible ? "HIDE" : "SHOW", attr,
176                                          o, &pos);
177                 else if (is_attr("full", attr))
178                         ok = cond_append(&b, orig ? "BRIEF" : "FULL", attr,
179                                          o, &pos);
180                 else if (is_attr("extras", attr))
181                         ok = cond_append(&b, extras ? "-" : "EXTRAS", attr,
182                                          o, &pos);
183                 else
184                         ok = cond_append(&b, attr, attr, o, &pos);
185                 if (ok)
186                         doc_next(ci->focus, m);
187                 if (cp >= 0) {
188                         cp -= 1;
189                         ret_pos = pos;
190                 }
191                 attr = a;
192                 if (attr)
193                         *attr++ = ':';
194         }
195         /* end of line, only display if we haven't reached
196          * the cursor or offset
197          *
198          * if cp < 0, we aren't looking for a cursor, so don't stop.
199          * if cp > 0, we haven't reached cursor yet, so don't stop
200          * if cp == 0, this is cursor pos, so stop.
201          */
202         if (ok && o < 0) {
203                 wint_t wch;
204                 buf_concat(&b, "</>");
205                 attr = pane_mark_attr(ci->focus, m,
206                                       "multipart-prev:email:content-type");
207                 if (attr) {
208                         buf_concat(&b, " ");
209                         buf_concat(&b, attr);
210                 }
211                 attr = pane_mark_attr(ci->focus, m,
212                                       "multipart-prev:email:charset");
213                 if (attr) {
214                         buf_concat(&b, " ");
215                         buf_concat(&b, attr);
216                 }
217                 attr = pane_mark_attr(ci->focus, m,
218                                       "multipart-prev:email:filename");
219                 if (attr) {
220                         buf_concat(&b, " file=");
221                         buf_concat(&b, attr);
222                 }
223                 if (cp >= 0)
224                         ret_pos = b.len;
225                 buf_concat(&b, "\n");
226                 if (cp >= 1)
227                         ret_pos = b.len;
228                 while ((wch = doc_next(ci->focus, m)) &&
229                        wch != '\n' && wch != WEOF)
230                         ;
231         }
232
233         ret = comm_call(ci->comm2, "callback:render", ci->focus, ret_pos, NULL,
234                         buf_final(&b));
235         free(b.b);
236         return ret;
237 }
238
239 static int get_part(struct pane *p safe, struct mark *m safe)
240 {
241         char *a = pane_mark_attr(p, m, "multipart:part-num");
242
243         if (!a)
244                 return Efail;
245         return atoi(a);
246 }
247
248 DEF_CMD(email_image)
249 {
250         char *c = NULL;
251         int p;
252         int ret;
253         int retlen = 0;
254         int i;
255         struct xy scale;
256         struct mark *point = ci->mark2;
257         int max_chars = ci->num;
258         int map_start;
259
260         if (!ci->mark)
261                 return Enoarg;
262         p = get_part(ci->home, ci->mark);
263         scale = pane_scale(ci->focus);
264         if (scale.x < 1)
265                 scale.x = 1;
266         asprintf(&c, "<image:comm:doc:multipart-%d-doc:get-bytes,width:%d,height:%d,noupscale,map:RccRccRcc>\n",
267                  to_orig(p),
268                  ci->focus->w * 1000 / scale.x,
269                  ci->focus->h * 750 / scale.x);
270         if (!c)
271                 return Efail;
272         map_start = strlen(c) - 2 - 9;
273         if (max_chars >= 0 && max_chars < (int)strlen(c))
274                 c[max_chars] = '\0';
275
276         for (i = 0; i < 9 ; i++) {
277                 if (point && mark_ordered_or_same(point, ci->mark))
278                         retlen = map_start + i;
279                 if (max_chars >= 0 && map_start + i >= max_chars)
280                         break;
281                 doc_next(ci->focus, ci->mark);
282         }
283
284         ret = comm_call(ci->comm2, "callback:render", ci->focus,
285                         retlen, NULL, c);
286         free(c);
287         return ret;
288 }
289
290 DEF_CMD(email_select_hide)
291 {
292         int vis = 1;
293         char *a;
294         struct mark *m = ci->mark;
295
296         if (!m)
297                 return Enoarg;
298
299         a = pane_mark_attr(ci->focus, m, "email:visible");
300         if (a && strcmp(a, "none") == 0)
301                 vis = 0;
302         call("doc:set-attr", ci->focus, 1, m, "email:visible", 0, NULL,
303              vis ? "none" : "preferred");
304         return 1;
305 }
306
307 DEF_CMD(email_select_full)
308 {
309         int want_orig = 1;
310         char *a;
311         struct mark *m = ci->mark;
312
313         if (!m)
314                 return Enoarg;
315
316         a = pane_mark_attr(ci->focus, m, "email:visible");
317         if (a && strcmp(a, "orig") == 0)
318                 want_orig = 0;
319         call("doc:set-attr", ci->focus, 1, m, "email:visible", 0, NULL,
320              want_orig ? "orig" : "transformed");
321         return 1;
322 }
323
324 DEF_CMD(email_select_extras)
325 {
326         char *a = attr_find(ci->home->attrs, "email:extra-headers");
327         struct mark *m = ci->mark;
328         struct mark *point;
329         struct pane *hdrdoc, *headers;
330
331         if (!m)
332                 return Enoarg;
333         if (a && *a)
334                 return 1;
335         headers = call_ret(pane, "doc:multipart:get-part", ci->focus, 0);
336         hdrdoc = call_ret(pane, "doc:multipart:get-part", ci->focus, 1);
337         if (!headers || !hdrdoc)
338                 return Einval;
339         point = point_new(hdrdoc);
340         if (point) {
341                 char *file;
342
343                 call("doc:set-ref", hdrdoc, 0, point);
344                 home_call(headers, "get-header", hdrdoc, 0, point, "Message-ID", 1);
345                 home_call(headers, "get-header", hdrdoc, 0, point, "In-Reply-To", 1);
346                 home_call(headers, "get-header", hdrdoc, 0, point, "References", 1, NULL, "list");
347                 file = pane_attr_get(headers, "filename");
348                 if (file) {
349                         call("doc:replace", hdrdoc, 1, point, "Filename: ",
350                              0, point, ",render:rfc822header=9");
351                         call("doc:replace", hdrdoc, 1, point, file, 0, point);
352                         call("doc:replace", hdrdoc, 1, point, "\n", 0, point);
353                 }
354         }
355         mark_free(point);
356
357         attr_set_str(&ci->home->attrs, "email:extra-headers", "done");
358         call("doc:set-attr", ci->focus, 1, m, "email:visible", 0, NULL,
359              "transformed");
360         return 1;
361 }
362
363 static struct map *email_view_map safe;
364
365 DEF_LOOKUP_CMD(email_view_handle, email_view_map);
366
367 static const char tspecials[] = "()<>@,;:\\\"/[]?=";
368
369 static int lws(char c) {
370         return c == ' '  || c == '\t' || c == '\r' || c == '\n';
371 }
372
373 static char *get_822_token(char **hdrp safe, int *len safe)
374 {
375         /* A "token" is one of:
376          * - Quoted string ""
377          * - single char from tspecials (except '(' or '"')
378          * - string of non-LWS, and non-tspecials
379          *
380          * (comments) are skipped.
381          * Start is returned, hdrp is moved, len is reported.
382          */
383         char *hdr = *hdrp;
384         char *start;
385         *len = 0;
386         if (!hdr)
387                 return NULL;
388         while (1) {
389                 while (lws(*hdr))
390                         hdr++;
391                 if (*hdr == '(') {
392                         while (*hdr && *hdr != ')')
393                                 hdr++;
394                         continue;
395                 }
396                 if (*hdr == '"') {
397                         start = ++hdr;
398                         while (*hdr && *hdr != '"')
399                                 hdr++;
400                         *len = hdr - start;
401                         hdr++;
402                         *hdrp = hdr;
403                         return start;
404                 }
405                 if (!*hdr) {
406                         *hdrp = NULL;
407                         return NULL;
408                 }
409                 if (strchr(tspecials, *hdr)) {
410                         start = hdr;
411                         hdr++;
412                         *len = 1;
413                         *hdrp = hdr;
414                         return start;
415                 }
416                 start = hdr;
417                 while (*hdr && !lws(*hdr) && !strchr(tspecials, *hdr))
418                         hdr++;
419                 *len = hdr - start;
420                 *hdrp = hdr;
421                 return start;
422         }
423 }
424
425 static char *get_822_attr(char *shdr safe, char *attr safe)
426 {
427         /* If 'hdr' contains "$attr=...", return "..."
428          * with "quotes" stripped.  Return value can be used
429          * until the next call, when it will be free.
430          */
431         int len, alen;
432         char *hdr = shdr;
433         char *h;
434         static char *last = NULL;
435
436         free(last);
437         last = NULL;
438
439         alen = strlen(attr);
440         while (hdr) {
441                 while ((h = get_822_token(&hdr, &len)) != NULL &&
442                        (len != alen || strncasecmp(h, attr, alen) != 0))
443                         ;
444                 h = get_822_token(&hdr, &len);
445                 if (!h || len != 1 || *h != '=')
446                         continue;
447                 h = get_822_token(&hdr, &len);
448                 if (!h)
449                         continue;
450                 last = strndup(h, len);
451                 return last;
452         }
453         return NULL;
454 }
455
456 static char *get_822_word(char *hdr safe)
457 {
458         /* Get the first word from header, in static
459          * space (freed on next call)
460          */
461         static char *last = NULL;
462         int len;
463         char *h;
464
465         free(last);
466         last = NULL;
467         h = get_822_token(&hdr, &len);
468         if (!h)
469                 return h;
470         last = strndup(h, len);
471         return last;
472 }
473
474 static bool tok_matches(char *tok, int len, char *match safe)
475 {
476         if (!tok)
477                 return False;
478         if (len != (int)strlen(match))
479                 return False;
480         return strncasecmp(tok, match, len) == 0;
481 }
482
483 static bool handle_text(struct pane *p safe, char *type, char *xfer, char *disp,
484                         struct mark *start safe, struct mark *end safe,
485                         struct pane *mp safe, struct pane *spacer safe,
486                         char *path)
487 {
488         struct pane *h, *transformed = NULL;
489         int need_charset = 0;
490         char *charset = NULL;
491         char *major, *minor = NULL;
492         int majlen, minlen;
493         char *ctype = NULL;
494         char *fname = NULL;
495
496         h = call_ret(pane, "attach-crop", p, 0, start, NULL, 0, end);
497         if (!h)
498                 return False;
499
500         if (xfer) {
501                 int xlen;
502                 xfer = get_822_token(&xfer, &xlen);
503                 if (xfer && xlen == 16 &&
504                     strncasecmp(xfer, "quoted-printable", 16) == 0) {
505                         struct pane *hx = call_ret(pane,
506                                                    "attach-quoted_printable",
507                                                    h);
508                         if (hx) {
509                                 h = hx;
510                                 need_charset = 1;
511                         }
512                 }
513                 if (xfer && xlen == 6 &&
514                     strncasecmp(xfer, "base64", 6) == 0) {
515                         struct pane *hx = call_ret(pane, "attach-base64", h);
516                         if (hx) {
517                                 h = hx;
518                                 need_charset = 1;
519                         }
520                 }
521                 if (xfer && xlen == 4 &&
522                     strncasecmp(xfer, "8bit", 6) == 0)
523                         need_charset = 2; // only if not utf-8
524         }
525         if (type)
526                 charset = get_822_attr(type, "charset");
527         if (need_charset && !charset && strncasecmp(type, "text/", 5) == 0)
528                 /* We really do need a charset, as the doc might
529                  * only provide bytes, not chars.
530                  */
531                 charset = "utf-8";
532         if (type && need_charset && charset != NULL &&
533             !(need_charset == 2 && strcasecmp(charset, "utf-8") == 0)) {
534                 char *c = NULL, *cp;
535                 struct pane *hx = NULL;
536                 charset = strsave(h, charset);
537                 asprintf(&c, "attach-charset-%s", charset);
538                 for (cp = c; cp && *cp; cp++)
539                         if (isupper(*cp))
540                                 *cp = tolower(*cp);
541                 if (c)
542                         hx = call_ret(pane, c, h);
543                 free(c);
544                 if (!hx)
545                         /* windows-1251 is safer than utf-8 as the latter
546                          * rejects some byte sequences, and iso-8859-* has
547                          * lots of control characters.
548                          */
549                         hx = call_ret(pane, "attach-charset-windows-1251", h);
550                 if (hx)
551                         h = hx;
552         } else
553                 charset = NULL;
554         if (type && (fname = get_822_attr(type, "name")))
555                 fname = strsave(h, fname);
556         if (disp && !fname &&
557             (fname = get_822_attr(disp, "filename")))
558                 fname = strsave(h, fname);
559         major = get_822_token(&type, &majlen);
560         if (major) {
561                 minor = get_822_token(&type, &minlen);
562                 if (minor && tok_matches(minor, minlen, "/"))
563                         minor = get_822_token(&type, &minlen);
564                 else
565                         minor = NULL;
566         }
567         if (minor)
568                 asprintf(&ctype, "%1.*s/%1.*s", majlen, major, minlen, minor);
569         else
570                 asprintf(&ctype, "%1.*s", majlen, major);
571         if (ctype && strcasecmp(ctype, "text/html") == 0) {
572                 transformed = call_ret(pane, "html-to-text-w3m", h, 1);
573                 if (!transformed)
574                         transformed = call_ret(pane, "html-to-text", h, 1);
575         }
576         if (ctype && strcasecmp(ctype, "text/calendar") == 0)
577                 transformed = call_ret(pane, "ical-to-text", h, 1);
578         if (ctype && strcasecmp(ctype, "application/pdf") == 0)
579                 transformed = call_ret(pane, "pdf-to-text", h, 1);
580         if (ctype && strcasecmp(ctype, "application/octet-stream") == 0 &&
581             fname && strcasestr(fname, ".pdf") != NULL)
582                 transformed = call_ret(pane, "pdf-to-text", h, 1);
583         if (major && strncasecmp(major, "application", majlen) == 0 &&
584             fname && (strcasestr(fname, ".docx") != NULL ||
585                       strcasestr(fname, ".doc") != NULL ||
586                       strcasestr(fname, ".odt") != NULL))
587                 transformed = call_ret(pane, "doc-to-text", h, 1, NULL, fname);
588
589         if (ctype && strncasecmp(ctype, "image/", 6) == 0) {
590                 struct mark *m;
591                 transformed = call_ret(pane, "doc:from-text", h,
592                                        0, NULL, NULL, 0, NULL,
593                                        "");
594                 if (transformed) {
595                         m = mark_new(transformed);
596                         call("doc:set-ref", transformed, 1, m);
597                         call("doc:replace", transformed, 1, m, "01",
598                              0, m, ",markup:func=doc:email:render-image");
599                         call("doc:replace", transformed, 1, m, "\n01",
600                              0, m, ",markup:not_eol=1");
601                         call("doc:replace", transformed, 1, m, "\n01\n",
602                              0, m, ",markup:not_eol=1");
603                         mark_free(m);
604                 }
605         }
606         if (transformed) {
607                 attr_set_str(&transformed->attrs, "email:is_transformed", "yes");
608                 attr_set_str(&transformed->attrs, "email:preferred", "transformed");
609         } else {
610                 transformed = call_ret(pane, "doc:from-text", h,
611                                        0, NULL, NULL, 0, NULL, "\n");
612                 if (transformed)
613                         attr_set_str(&transformed->attrs, "email:preferred",
614                                      "orig");
615         }
616         if (!transformed) {
617                 pane_close(h);
618                 return False;
619         }
620         call("doc:set:autoclose", transformed, 1);
621         if (ctype) {
622                 int i;
623                 for (i = 0; ctype[i]; i++)
624                         if (isupper(ctype[i]))
625                                 ctype[i] = tolower(ctype[i]);
626                 attr_set_str(&transformed->attrs, "email:content-type", ctype);
627                 free(ctype);
628         } else
629                 attr_set_str(&h->attrs, "email:content-type", "text/plain");
630         attr_set_str(&transformed->attrs, "email:actions", "hide:save");
631         attr_set_str(&transformed->attrs, "email:path", path);
632         attr_set_str(&transformed->attrs, "email:which", "transformed");
633         if (charset)
634                 attr_set_str(&transformed->attrs, "email:charset", charset);
635         if (fname)
636                 attr_set_str(&transformed->attrs, "email:filename", fname);
637         if (disp)
638                 attr_set_str(&transformed->attrs,
639                              "email:content-disposition", disp);
640         attr_set_str(&h->attrs, "email:which", "orig");
641
642         home_call(mp, "multipart-add", h);
643         home_call(mp, "multipart-add", transformed);
644         home_call(mp, "multipart-add", spacer);
645         return True;
646 }
647
648 /* Find a multipart boundary between start and end, moving
649  * 'start' to after the boundary, and 'pos' to just before it.
650  * Return 0 if a non-terminal boundary is found
651  * Return 1 if a terminal boundary is found (trailing --)
652  * Return -1 if nothing is found.
653  */
654 #define is_lws(c) ({int _c2 = c; _c2 == ' ' || _c2 == '\t' || is_eol(_c2); })
655 static int find_boundary(struct pane *p safe,
656                          struct mark *start safe, struct mark *end safe,
657                          struct mark *pos,
658                          char *boundary safe)
659 {
660         char *patn = NULL;
661         int ret;
662         int len = strlen(boundary);
663
664         asprintf(&patn, "^--(?%d:%s)(--)?[ \\t\\r]*$", len, boundary);
665         ret = call("text-search", p, 0, start, patn, 0, end);
666         if (ret <= 0)
667                 return -1;
668         ret -= 1;
669         if (pos) {
670                 int cnt = ret;
671                 mark_to_mark(pos, start);
672                 while (cnt > 0 && doc_prev(p, pos) != WEOF)
673                         cnt -= 1;
674                 /* Previous char is CRLF, and must be swallowed */
675                 if (doc_prior(p, pos) == '\n')
676                         doc_prev(p, pos);
677                 if (doc_prior(p, pos) == '\r')
678                         doc_prev(p, pos);
679         }
680         while (is_lws(doc_prior(p, start))) {
681                 len -= 1;
682                 doc_prev(p, start);
683         }
684         while (is_lws(doc_following(p, start)))
685                 doc_next(p, start);
686         if (ret == 2 + len)
687                 return 0;
688         if (ret == 2 + len + 2)
689                 return 1;
690         return -1;
691 }
692
693 static bool handle_multipart(struct pane *p safe, char *type safe,
694                              struct mark *start safe, struct mark *end safe,
695                              struct pane *mp safe, struct pane *spacer safe,
696                              char *path safe)
697 {
698         char *boundary = get_822_attr(type, "boundary");
699         int found_end = 0;
700         struct mark *pos, *part_end;
701         char *tok;
702         int len;
703         int partnum = 0;
704         char *newpath;
705
706         if (!boundary)
707                 /* FIXME need a way to say "just display the text" */
708                 return True;
709
710         found_end = find_boundary(p, start, end, NULL, boundary);
711         if (found_end != 0)
712                 return True;
713         tok = get_822_token(&type, &len);
714         if (tok) {
715                 tok = get_822_token(&type, &len);
716                 if (tok && tok[0] == '/')
717                         tok = get_822_token(&type, &len);
718         }
719         boundary = strdup(boundary);
720         pos = mark_dup(start);
721         part_end = mark_dup(pos);
722         while (found_end == 0 &&
723                (found_end = find_boundary(p, pos, end, part_end,
724                                           boundary)) >= 0) {
725                 struct pane *hdr = call_ret(pane, "attach-rfc822header", p,
726                                             0, start, NULL,
727                                             0, part_end);
728                 char *ptype, *pxfer, *pdisp;
729
730                 if (!hdr)
731                         break;
732                 call("get-header", hdr, 0, NULL, "content-type",
733                      0, NULL, "cmd");
734                 call("get-header", hdr, 0, NULL, "content-transfer-encoding",
735                      0, NULL, "cmd");
736                 call("get-header", hdr, 0, NULL, "content-disposition",
737                      0, NULL, "cmd");
738                 ptype = attr_find(hdr->attrs, "rfc822-content-type");
739                 pxfer = attr_find(hdr->attrs,
740                                   "rfc822-content-transfer-encoding");
741                 pdisp = attr_find(hdr->attrs,
742                                   "rfc822-content-disposition");
743
744                 pane_close(hdr);
745
746                 newpath = NULL;
747                 asprintf(&newpath, "%s%s%1.*s:%d", path, path[0] ? ",":"",
748                          len, tok, partnum);
749                 partnum++;
750
751                 if (part_end->seq < start->seq)
752                         mark_to_mark(part_end, start);
753                 handle_content(p, ptype, pxfer, pdisp, start, part_end, mp,
754                                spacer, newpath ?:"");
755                 free(newpath);
756                 mark_to_mark(start, pos);
757         }
758         mark_to_mark(start, pos);
759         mark_free(pos);
760         mark_free(part_end);
761         free(boundary);
762         return True;
763 }
764
765 static bool handle_content(struct pane *p safe,
766                            char *type, char *xfer, char *disp,
767                            struct mark *start safe, struct mark *end safe,
768                            struct pane *mp safe, struct pane *spacer safe,
769                            char *path safe)
770 {
771         char *hdr;
772         char *major, *minor = NULL;
773         int mjlen, mnlen;
774
775         if (mark_ordered_or_same(end, start))
776                 return True;
777
778         if (!type)
779                 type = "text/plain";
780         hdr = type;
781
782         major = get_822_token(&hdr, &mjlen);
783         if (major) {
784                 minor = get_822_token(&hdr, &mnlen);
785                 if (minor && minor[0] == '/')
786                         minor = get_822_token(&hdr, &mnlen);
787         }
788
789         if (tok_matches(major, mjlen, "multipart"))
790                 return handle_multipart(p, type, start, end, mp, spacer, path);
791
792         if (tok_matches(major, mjlen, "message") && tok_matches(minor, mnlen, "rfc822"))
793                 return handle_rfc822(p, start, end, mp, spacer, path);
794
795         if (tok_matches(major, mjlen, "text") && tok_matches(minor, mnlen, "rfc822-headers"))
796                 return handle_rfc822(p, start, end, mp, spacer, path);
797
798         if (major == NULL ||
799             tok_matches(major, mjlen, "text"))
800                 return handle_text(p, type, xfer, disp, start, end,
801                                    mp, spacer, path);
802
803         /* default to plain text until we get a better default */
804         return handle_text(p, type, xfer, disp, start, end, mp, spacer, path);
805 }
806
807 static bool handle_rfc822(struct pane *email safe,
808                           struct mark *start safe, struct mark *end safe,
809                           struct pane *mp safe, struct pane *spacer safe,
810                           char *path safe)
811 {
812         struct pane *h2, *h3;
813         struct pane *hdrdoc = NULL;
814         struct mark *point = NULL;
815         struct mark *hdr_start;
816         char *xfer = NULL, *type = NULL, *disp = NULL;
817         char *mime;
818         char *newpath = NULL;
819
820         hdr_start = mark_dup(start);
821         h2 = call_ret(pane, "attach-rfc822header", email, 0, start, NULL, 0, end);
822         if (!h2)
823                 goto out;
824         attr_set_str(&h2->attrs, "email:which", "orig");
825
826         hdrdoc = call_ret(pane, "doc:from-text", email, 0, NULL, NULL, 0, NULL, "");
827         if (!hdrdoc)
828                 goto out;
829         call("doc:set:autoclose", hdrdoc, 1);
830         point = point_new(hdrdoc);
831         if (!point)
832                 goto out;
833
834         /* copy some headers to the header temp document */
835         home_call(h2, "get-header", hdrdoc, 0, point, "From", 1, NULL, "list");
836         home_call(h2, "get-header", hdrdoc, 0, point, "Date", 1);
837         home_call(h2, "get-header", hdrdoc, 0, point, "Subject", 1, NULL, "text");
838         home_call(h2, "get-header", hdrdoc, 0, point, "To", 1, NULL, "list");
839         home_call(h2, "get-header", hdrdoc, 0, point, "Cc", 1, NULL, "list");
840         home_call(h2, "get-header", hdrdoc, 0, point, "Reply-To", 1, NULL, "list");
841
842         /* copy some headers into attributes for later analysis */
843         call("get-header", h2, 0, NULL, "MIME-Version", 0, NULL, "cmd");
844         call("get-header", h2, 0, NULL, "content-type", 0, NULL, "cmd");
845         call("get-header", h2, 0, NULL, "content-transfer-encoding",
846              0, NULL, "cmd");
847         call("get-header", h2, 0, NULL, "content-disposition",
848              0, NULL, "cmd");
849         mime = attr_find(h2->attrs, "rfc822-mime-version");
850         if (mime)
851                 mime = get_822_word(mime);
852         /* Some email doesn't contain MIME-Type, but is still mime... */
853         if (!mime || strcmp(mime, "1.0") == 0) {
854                 type = attr_find(h2->attrs, "rfc822-content-type");
855                 xfer = attr_find(h2->attrs, "rfc822-content-transfer-encoding");
856                 disp = attr_find(h2->attrs, "rfc822-content-disposition");
857         }
858
859         newpath = NULL;
860         asprintf(&newpath, "%s%sheaders", path, path[0] ? ",":"");
861         attr_set_str(&hdrdoc->attrs, "email:actions", "hide:extras:full");
862         attr_set_str(&hdrdoc->attrs, "email:which", "transformed");
863         attr_set_str(&hdrdoc->attrs, "email:content-type", "text/rfc822-headers");
864         attr_set_str(&hdrdoc->attrs, "email:path", newpath);
865         attr_set_str(&hdrdoc->attrs, "email:is_transformed", "yes");
866         h3 = call_ret(pane, "attach-crop", h2, 0, hdr_start, NULL, 0, start);
867         if (!h3)
868                 h3 = h2;
869         home_call(mp, "multipart-add", h3);
870         home_call(mp, "multipart-add", hdrdoc);
871         home_call(mp, "multipart-add", spacer);
872         free(newpath);
873
874         newpath = NULL;
875         asprintf(&newpath, "%s%sbody", path, path[0] ? ",":"");
876         if (!handle_content(email, type, xfer, disp, start, end,
877                             mp, spacer, newpath?:""))
878                 goto out;
879         free(newpath);
880         return True;
881 out:
882         free(newpath);
883         if (h2)
884                 pane_close(h2);
885         if (point)
886                 mark_free(point);
887         if (hdrdoc)
888                 pane_close(hdrdoc);
889         return False;
890 }
891
892 DEF_CMD(open_email)
893 {
894         int fd;
895         struct pane *email = NULL;
896         struct pane *spacer = NULL;
897         struct mark *start, *end;
898         struct pane *p;
899         struct mark *point;
900
901         if (ci->str == NULL || !strstarts(ci->str, "email:"))
902                 return Efallthrough;
903         fd = open(ci->str+6, O_RDONLY);
904         if (fd < 0)
905                 return Efallthrough;
906         p = call_ret(pane, "doc:open", ci->focus, fd, NULL, ci->str + 6, 1);
907         close(fd);
908         if (!p)
909                 return Efallthrough;
910         start = mark_new(p);
911         if (!start) {
912                 pane_close(p);
913                 return Efallthrough;
914         }
915         end = mark_dup(start);
916         call("doc:set-ref", p, 0, end);
917
918         call("doc:set:autoclose", p, 1);
919         email = p;
920
921         /* create spacer doc to be attached between each part */
922         p = call_ret(pane, "doc:from-text", p, 0, NULL, NULL, 0, NULL,
923                      "0123456789\n");
924         if (!p)
925                 goto out;
926
927         attr_set_str(&p->attrs, "email:which", "spacer");
928         call("doc:set:autoclose", p, 1);
929         spacer = p;
930         point = point_new(p);
931         call("doc:set-ref", p, 1, point);
932         call("doc:set-attr", p, 1, point, "markup:func", 0,
933              NULL, "doc:email:render-spacer");
934         mark_free(point);
935
936         /* Create the multipart in which all the parts are assembled. */
937         p = call_ret(pane, "attach-doc-multipart", ci->home);
938         if (!p)
939                 goto out;
940         call("doc:set:autoclose", p, 1);
941         attr_set_str(&p->attrs, "render-default", "text");
942         attr_set_str(&p->attrs, "filename", ci->str+6);
943         attr_set_str(&p->attrs, "doc-type", "email");
944
945         if (!handle_rfc822(email, start, end, p, spacer, ""))
946                 goto out;
947         mark_free(start);
948         mark_free(end);
949
950         return comm_call(ci->comm2, "callback:attach", p);
951
952 out:
953         mark_free(start);
954         mark_free(end);
955         if (spacer)
956                 pane_close(spacer);
957         pane_close(email);
958         return Efail;
959 }
960
961 DEF_CMD_CLOSED(email_view_close)
962 {
963         struct email_view *evi = ci->home->data;
964
965         free(evi->invis);
966         evi->invis = NULL;
967         return 1;
968 }
969
970 static int count_buttons(struct pane *p safe, struct mark *m safe)
971 {
972         int cnt = 0;
973         char *attr = pane_mark_attr(p, m, "multipart-prev:email:actions");
974         if (!attr)
975                 attr = "hide";
976         while (attr) {
977                 cnt += 1;
978                 attr = strchr(attr, ':');
979                 if (attr)
980                         attr++;
981         }
982         return cnt;
983 }
984
985 static inline wint_t email_next(struct pane *p safe, struct mark *m safe,
986                                 struct doc_ref *r safe, bool bytes)
987 {
988         struct email_view *evi = p->data;
989         bool move = r == &m->ref;
990         wint_t ret;
991         int n = -1;
992
993         ret = home_call(p->parent, "doc:char", p,
994                         move ? 1 : 0,
995                         m, evi->invis,
996                         move ? 0 : 1);
997         n = get_part(p->parent, m);
998         if (move && is_spacer(n)) {
999                 /* Moving in a spacer.  IF after valid buttons,
1000                  * move to end.
1001                  */
1002                 wint_t c;
1003                 unsigned int buttons = count_buttons(p, m);
1004                 while ((c = doc_following(p->parent, m)) != WEOF &&
1005                        iswdigit(c) && (c-'0') >= buttons)
1006                         doc_next(p->parent, m);
1007         }
1008         return ret;
1009 }
1010
1011 static inline wint_t email_prev(struct pane *p safe, struct mark *m safe,
1012                                 struct doc_ref *r safe, bool bytes)
1013 {
1014         struct email_view *evi = p->data;
1015         bool move = r == &m->ref;
1016         wint_t ret;
1017         int n = -1;
1018
1019         ret = home_call(p->parent, "doc:char", p,
1020                         move ? -1 : 0,
1021                         m, evi->invis,
1022                         move ? 0 : -1);
1023         n = get_part(p->parent, m);
1024         if (is_spacer(n) && move &&
1025             ret != CHAR_RET(WEOF) && iswdigit(ret & 0x1fffff)) {
1026                 /* Just stepped back over the 9 at the end of a spacer,
1027                  * Maybe step further if there aren't 10 buttons.
1028                  */
1029                 unsigned int buttons = count_buttons(p, m);
1030                 wint_t c = ret & 0x1fffff;
1031
1032                 while (c != WEOF && iswdigit(c) && c - '0' >= buttons)
1033                         c = doc_prev(p->parent, m);
1034                 ret = c;
1035         }
1036         return ret;
1037 }
1038
1039 DEF_CMD(email_char)
1040 {
1041         return do_char_byte(ci);
1042 }
1043
1044 DEF_CMD(email_content)
1045 {
1046         /* Call the multipart doc:content telling it
1047          * what is invisible, marking all spacers as invisible
1048          */
1049         struct pane *p = ci->home;
1050         struct email_view *evi = p->data;
1051         char *invis2 = strsave(p, evi->invis);
1052         int i;
1053
1054         for (i = 0; invis2 && invis2[i]; i++)
1055                 if (is_spacer(i))
1056                         invis2[i] = 'i';
1057         return home_call(p->parent, ci->key, p,
1058                          ci->num, ci->mark, invis2,
1059                          ci->num2, ci->mark2, ci->str2,
1060                          ci->x, ci->y, ci->comm2);
1061 }
1062
1063 DEF_CMD(email_set_ref)
1064 {
1065         struct pane *p = ci->home;
1066         struct email_view *evi = p->data;
1067
1068         if (!ci->mark)
1069                 return Enoarg;
1070         home_call_comm(p->parent, ci->key, ci->focus, ci->comm2,
1071                        ci->num, ci->mark, evi->invis);
1072         return 1;
1073 }
1074
1075 DEF_CMD(email_step_part)
1076 {
1077         struct pane *p = ci->home;
1078         struct email_view *evi = p->data;
1079
1080         if (!ci->mark)
1081                 return Enoarg;
1082         home_call(p->parent, "doc:step-part", ci->focus, ci->num, ci->mark, evi->invis);
1083         return 1;
1084 }
1085
1086 DEF_CMD(email_view_get_attr)
1087 {
1088         int p;
1089         char *v;
1090         struct email_view *evi = ci->home->data;
1091
1092         if (!ci->str || !ci->mark)
1093                 return Enoarg;
1094         if (strcmp(ci->str, "email:visible") == 0) {
1095                 p = get_part(ci->home->parent, ci->mark);
1096                 /* only parts can be invisible, not separators */
1097                 p = to_orig(p);
1098                 if (p < 0 || p >= evi->parts)
1099                         v = "none";
1100                 else if (!evi->invis)
1101                         v = "none";
1102                 else if (evi->invis[p] != 'i')
1103                         v = "orig";
1104                 else if (evi->invis[p+1] != 'i')
1105                         v = "transformed";
1106                 else
1107                         v = "none";
1108
1109                 return comm_call(ci->comm2, "callback", ci->focus, 0, ci->mark,
1110                                  v, 0, NULL, ci->str);
1111         }
1112         if (strcmp(ci->str, "email:image") == 0) {
1113                 char im[100];
1114
1115                 p = get_part(ci->home->parent, ci->mark);
1116                 sprintf(im, "comm:doc:multipart-%d-doc:get-bytes", to_orig(p));
1117                 return comm_call(ci->comm2, "cb", ci->focus, 0, ci->mark,
1118                                  im, 0, NULL, ci->str);
1119         }
1120         return Efallthrough;
1121 }
1122
1123 DEF_CMD(email_view_set_attr)
1124 {
1125         int p;
1126         struct email_view *evi = ci->home->data;
1127
1128         if (!ci->str || !ci->mark)
1129                 return Enoarg;
1130         if (strcmp(ci->str, "email:visible") == 0) {
1131                 struct mark *m1, *m2;
1132                 const char *w;
1133
1134                 p = get_part(ci->home->parent, ci->mark);
1135                 /* only parts can be invisible, not separators */
1136                 p = to_orig(p);
1137                 if (p < 0 || p >= evi->parts || !evi->invis)
1138                         return Efail;
1139
1140                 m1 = mark_dup(ci->mark);
1141                 while (get_part(ci->home->parent, m1) > p &&
1142                        home_call(ci->home->parent, "doc:step-part",
1143                                  ci->focus, -1, m1) > 0)
1144                         ;
1145
1146                 w = ci->str2;
1147                 if (w && strcmp(w, "preferred") == 0) {
1148                         w = pane_mark_attr(ci->focus, m1,
1149                                            "multipart-next:email:preferred");
1150                         if (!w)
1151                                 w = "orig";
1152                 } else if (w && (strcmp(w, "orig") == 0 ||
1153                                  strcmp(w, "transformed") == 0)) {
1154                         call("doc:set-attr", ci->focus, 1, m1,
1155                              "multipart-next:email:preferred", 0, NULL, w);
1156                 }
1157                 evi->invis[p] = 'i';
1158                 evi->invis[p+1] = 'i';
1159                 if (w && strcmp(w, "orig") == 0)
1160                         evi->invis[p] = 'v';
1161                 if (w && strcmp(w, "transformed") == 0)
1162                         evi->invis[p+1] = 'v';
1163
1164                 /* Tell viewers that visibility has changed */
1165                 mark_step(m1, 0);
1166                 m2 = mark_dup(m1);
1167                 home_call(ci->home->parent, "doc:step-part", ci->focus,
1168                           1, m2);
1169                 home_call(ci->home->parent, "doc:step-part", ci->focus,
1170                           1, m2);
1171                 call("view:changed", ci->focus, 0, m1, NULL, 0, m2);
1172                 call("Notify:clip", ci->focus, 0, m1, NULL, 0, m2);
1173                 mark_free(m1);
1174                 mark_free(m2);
1175
1176                 if (w && strcmp(w, "transformed") == 0) {
1177                         /* If the transformation was delayed, tell it
1178                          * to start now.
1179                          */
1180                         struct pane *part =
1181                                 call_ret(pane, "doc:multipart:get-part",
1182                                          ci->home->parent, p+1);
1183                         if (part)
1184                                 call("doc:notify:convert-now", part);
1185                 }
1186
1187                 return 1;
1188         }
1189         return Efallthrough;
1190 }
1191
1192 DEF_CMD(attach_email_view)
1193 {
1194         struct pane *p, *p2;
1195         struct email_view *evi;
1196         struct mark *m;
1197         int n, i;
1198
1199         m = mark_new(ci->focus);
1200         if (!m)
1201                 return Efail;
1202         call("doc:set-ref", ci->focus, 0, m);
1203         n = get_part(ci->focus, m);
1204         mark_free(m);
1205         if (n <= 0 || n > 1000 )
1206                 return Einval;
1207
1208         p = pane_register(ci->focus, 0, &email_view_handle.c);
1209         if (!p)
1210                 return Efail;
1211         evi = p->data;
1212         evi->parts = n;
1213         evi->invis = calloc(n+1, sizeof(char));
1214         for (i = 0; i < n; i++) {
1215                 if (is_spacer(i))
1216                         /* Spacers must be visible */
1217                         evi->invis[i] = 'v';
1218                 else if (is_orig(i) && i < 2*3)
1219                         /* Headers and first part can be visible */
1220                         evi->invis[i] = 'v';
1221                 else
1222                         /* Everything else default to invisible */
1223                         evi->invis[i] = 'i';
1224         }
1225         p2 = call_ret(pane, "attach-line-count", p);
1226         if (p2)
1227                 p = p2;
1228         attr_set_str(&p->attrs, "render-hide-CR", "yes");
1229         return comm_call(ci->comm2, "callback:attach", p);
1230 }
1231
1232 static void email_init_map(void)
1233 {
1234         email_view_map = key_alloc();
1235         key_add(email_view_map, "Close", &email_view_close);
1236         key_add(email_view_map, "doc:char", &email_char);
1237         key_add(email_view_map, "doc:content", &email_content);
1238         key_add(email_view_map, "doc:content-bytes", &email_content);
1239         key_add(email_view_map, "doc:set-ref", &email_set_ref);
1240         key_add(email_view_map, "doc:get-boundary", &email_set_ref);
1241         key_add(email_view_map, "doc:email-step-part", &email_step_part);
1242         key_add(email_view_map, "doc:set-attr", &email_view_set_attr);
1243         key_add(email_view_map, "doc:get-attr", &email_view_get_attr);
1244         key_add(email_view_map, "doc:email:render-spacer", &email_spacer);
1245         key_add(email_view_map, "doc:email:render-image", &email_image);
1246         key_add(email_view_map, "email:select:hide", &email_select_hide);
1247         key_add(email_view_map, "email:select:full", &email_select_full);
1248         key_add(email_view_map, "email:select:extras", &email_select_extras);
1249 }
1250
1251 void edlib_init(struct pane *ed safe)
1252 {
1253         email_init_map();
1254         call_comm("global-set-command", ed, &open_email, 0, NULL,
1255                   "open-doc-email");
1256         call_comm("global-set-command", ed, &attach_email_view, 0, NULL,
1257                   "attach-email-view");
1258 }