2 * Copyright Neil Brown ©2016-2020 <neil@brown.name>
3 * May be distributed under terms of GPLv2 - see file:COPYING
5 * doc-email: Present an email message as its intended content, with
6 * part recognition and decoding etc.
8 * A multipart document is created where every other part is a "spacer"
9 * where buttons can be placed to control visibility of the previous part,
10 * or to act on it in some other way.
11 * The first part is the headers which are copied to a temp text document.
12 * Subsequent non-spacer parts are cropped sections of the email, possibly
13 * with filters overlayed to handle the transfer encoding.
14 * Alternately, they might be temp documents simplar to the headers
15 * storing e.g. transformed HTML or an image.
18 #define _GNU_SOURCE /* for asprintf */
29 struct pane *email safe;
30 struct pane *spacer safe;
33 static bool handle_content(struct pane *p safe, char *type, char *xfer,
34 struct mark *start safe, struct mark *end safe,
35 struct pane *mp safe, struct pane *spacer safe,
38 static bool cond_append(struct buf *b safe, char *txt safe, char *tag safe,
39 int offset, struct mark *m safe, int *cp safe)
41 char *tagf = "active-tag:email-";
42 int prelen = 1 + strlen(tagf) + strlen(tag) + 1 + 1;
44 int len = prelen + strlen(txt) + postlen;
45 if (offset != NO_NUMERIC && offset >= 0 && offset <= b->len + len)
56 buf_concat(b, "]</>");
60 static bool is_attr(char *a safe, char *attrs safe)
63 if (strncmp(a, attrs, l) != 0)
65 if (attrs[l] == ':' || attrs[l] == '\0')
74 struct mark *m = ci->mark;
75 struct mark *pm = ci->mark2;
85 /* Count the number of chars before the cursor.
86 * This tells us which button to highlight.
90 while (pm->seq > m->seq && !mark_same(pm, m)) {
91 doc_prev(ci->focus, pm);
98 buf_concat(&b, "<fg:red>");
100 attr = pane_mark_attr(ci->home, m, "multipart-prev:email:path");
102 buf_concat(&b, attr);
106 attr = pane_mark_attr(ci->focus, m, "email:visible");
107 if (attr && *attr == '0')
109 attr = pane_mark_attr(ci->home, m, "multipart-prev:email:actions");
113 while (ok && attr && *attr) {
114 if (is_attr("hide", attr))
115 ok = cond_append(&b, visible ? "HIDE" : "SHOW", "1",
117 else if (is_attr("save", attr))
118 ok = cond_append(&b, "Save", "2", o, m, &cp);
119 else if (is_attr("open", attr))
120 ok = cond_append(&b, "Open", "3", o, m, &cp);
121 attr = strchr(attr, ':');
125 /* end of line, only display if we haven't reached
126 * the cursor or offset
128 * if cp < 0, we aren't looking for a cursor, so don't stop.
129 * if cp > 0, we haven't reached cursor yet, so don't stop
130 * if cp == 0, this is cursor pos, so stop.
132 if (ok && cp != 0 && ((o < 0 || o == NO_NUMERIC))) {
134 buf_concat(&b, "</>");
135 attr = pane_mark_attr(ci->focus, m,
136 "multipart-prev:email:content-type");
139 buf_concat(&b, attr);
141 buf_concat(&b, "\n");
142 while ((wch = doc_next(ci->focus, m)) &&
143 wch != '\n' && wch != WEOF)
147 ret = comm_call(ci->comm2, "callback:render", ci->focus, 0, NULL,
153 DEF_CMD(email_select)
155 /* If mark is on a button, press it... */
156 struct mark *m = ci->mark;
162 a = pane_mark_attr(ci->focus, m, "markup:func");
163 if (!a || strcmp(a, "doc:email:render-spacer") != 0)
165 a = pane_mark_attr(ci->focus, m, "multipart-prev:email:actions");
174 if (a && is_attr("hide", a)) {
176 a = pane_mark_attr(ci->focus, m, "email:visible");
179 call("doc:set-attr", ci->focus, 1, m, "email:visible", 0, NULL,
185 static struct map *email_view_map safe;
187 DEF_LOOKUP_CMD(email_view_handle, email_view_map);
189 static char tspecials[] = "()<>@,;:\\\"/[]?=";
191 static int lws(char c) {
192 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
195 static char *get_822_token(char **hdrp safe, int *len safe)
197 /* A "token" is one of:
199 * single char from tspecials
200 * string on of LWS, and none tspecials
202 * (comments) are skipped.
203 * Start is returned, hdrp is moved, len is reported.
214 while (*hdr && *hdr != ')')
220 while (*hdr && *hdr != '"')
231 if (strchr(tspecials, *hdr)) {
239 while (*hdr && !lws(*hdr) && !strchr(tspecials, *hdr))
247 static char *get_822_attr(char *shdr safe, char *attr safe)
249 /* If 'hdr' contains "$attr=...", return "..."
250 * with "quotes" stripped
255 static char *last = NULL;
262 while ((h = get_822_token(&hdr, &len)) != NULL &&
263 (len != alen || strncasecmp(h, attr, alen) != 0))
265 h = get_822_token(&hdr, &len);
266 if (!h || len != 1 || *h != '=')
268 h = get_822_token(&hdr, &len);
271 last = strndup(h, len);
277 static char *get_822_word(char *hdr safe)
279 /* Get the first word from header, is static space */
280 static char *last = NULL;
286 h = get_822_token(&hdr, &len);
289 last = strndup(h, len);
293 static bool tok_matches(char *tok, int len, char *match safe)
297 if (len != (int)strlen(match))
299 return strncasecmp(tok, match, len) == 0;
302 static bool handle_text(struct pane *p safe, char *type, char *xfer,
303 struct mark *start safe, struct mark *end safe,
304 struct pane *mp safe, struct pane *spacer safe,
308 int need_charset = 0;
310 char *major, *minor = NULL;
314 h = call_ret(pane, "attach-crop", p, 0, start, NULL, 0, end);
320 xfer = get_822_token(&xfer, &xlen);
321 if (xfer && xlen == 16 &&
322 strncasecmp(xfer, "quoted-printable", 16) == 0) {
323 struct pane *hx = call_ret(pane,
324 "attach-quoted_printable",
331 if (xfer && xlen == 6 &&
332 strncasecmp(xfer, "base64", 6) == 0) {
333 struct pane *hx = call_ret(pane, "attach-base64", h);
340 if (type && need_charset &&
341 (charset = get_822_attr(type, "charset")) != NULL &&
342 strcasecmp(charset, "utf-8") == 0) {
343 struct pane *hx = call_ret(pane, "attach-utf8", h);
347 major = get_822_token(&type, &majlen);
348 if (major && tok_matches(major, majlen, "text"))
349 attr_set_str(&h->attrs, "email:actions", "hide:save");
351 attr_set_str(&h->attrs, "email:actions", "hide:open");
353 minor = get_822_token(&type, &minlen);
354 if (minor && tok_matches(minor, minlen, "/"))
355 minor = get_822_token(&type, &minlen);
360 asprintf(&ctype, "%1.*s/%1.*s", majlen, major, minlen, minor);
362 asprintf(&ctype, "%1.*s", majlen, major);
363 if (ctype && strcmp(ctype, "text/html") == 0) {
365 html = call_ret(pane, "html-to-text", h);
373 for (i = 0; ctype[i]; i++)
374 if (isupper(ctype[i]))
375 ctype[i] = tolower(ctype[i]);
376 attr_set_str(&h->attrs, "email:content-type", ctype);
379 attr_set_str(&h->attrs, "email:path", path);
381 home_call(mp, "multipart-add", h);
382 home_call(mp, "multipart-add", spacer);
386 /* Find a multipart boundary between start and end, moving
387 * 'start' to after the boundary, and 'pos' to just before it.
388 * Return 0 if a non-terminal boundary is found
389 * Return 1 if a terminal boundary is found (trailing --)
390 * Return -1 if nothing is found.
392 #define is_lws(c) ({int __c2 = c; __c2 == ' ' || __c2 == '\t' || is_eol(__c2); })
393 static int find_boundary(struct pane *p safe,
394 struct mark *start safe, struct mark *end safe,
400 int len = strlen(boundary);
402 asprintf(&patn, "^--(?%d:%s)(--)?[ \\t\\r]*$", len, boundary);
403 ret = call("text-search", p, 0, start, patn, 0, end);
409 mark_to_mark(pos, start);
410 while (cnt > 0 && doc_prev(p, pos) != WEOF)
412 /* Previous char is CRLF, and must be swallowed */
413 if (doc_prior(p, pos) == '\n')
415 if (doc_prior(p, pos) == '\r')
418 while (is_lws(doc_prior(p, start))) {
422 while (is_lws(doc_following(p, start)))
426 if (ret == 2 + len + 2)
431 static bool handle_multipart(struct pane *p safe, char *type safe,
432 struct mark *start safe, struct mark *end safe,
433 struct pane *mp safe, struct pane *spacer safe,
436 char *boundary = get_822_attr(type, "boundary");
438 struct mark *pos, *part_end;
445 /* FIXME need a way to say "just display the text" */
448 found_end = find_boundary (p, start, end, NULL, boundary);
451 tok = get_822_token(&type, &len);
453 tok = get_822_token(&type, &len);
454 if (tok && tok[0] == '/')
455 tok = get_822_token(&type, &len);
457 boundary = strdup(boundary);
458 pos = mark_dup(start);
459 part_end = mark_dup(pos);
460 while (found_end == 0 &&
461 (found_end = find_boundary(p, pos, end, part_end,
463 struct pane *hdr = call_ret(pane, "attach-rfc822header", p,
470 call("get-header", hdr, 0, NULL, "content-type",
472 call("get-header", hdr, 0, NULL, "content-transfer-encoding",
474 ptype = attr_find(hdr->attrs, "rfc822-content-type");
475 pxfer = attr_find(hdr->attrs,
476 "rfc822-content-transfer-encoding");
481 asprintf(&newpath, "%s%s%1.*s:%d", path, path[0] ? ",":"",
485 handle_content(p, ptype, pxfer, start, part_end, mp, spacer,
488 mark_to_mark(start, pos);
490 mark_to_mark(start, pos);
497 static bool handle_content(struct pane *p safe, char *type, char *xfer,
498 struct mark *start safe, struct mark *end safe,
499 struct pane *mp safe, struct pane *spacer safe,
503 char *major, *minor = NULL;
510 major = get_822_token(&hdr, &mjlen);
512 minor = get_822_token(&hdr, &mnlen);
513 if (minor && minor[0] == '/')
514 minor = get_822_token(&hdr, &mnlen);
517 tok_matches(major, mjlen, "text"))
518 return handle_text(p, type, xfer, start, end,
521 if (tok_matches(major, mjlen, "multipart"))
522 return handle_multipart(p, type, start, end, mp, spacer, path);
524 /* default to plain text until we get a better default */
525 return handle_text(p, type, xfer, start, end, mp, spacer, path);
531 struct email_info *ei;
532 struct mark *start, *end;
535 char *xfer = NULL, *type = NULL;
539 if (ci->str == NULL ||
540 strncmp(ci->str, "email:", 6) != 0)
542 fd = open(ci->str+6, O_RDONLY);
543 p = call_ret(pane, "doc:open", ci->focus, fd, NULL, ci->str + 6, 1);
546 start = vmark_new(p, MARK_UNGROUPED, NULL);
549 end = mark_dup(start);
550 call("doc:set-ref", p, 0, end);
554 h2 = call_ret(pane, "attach-rfc822header", p, 0, start, NULL, 0, end);
557 p = call_ret(pane, "doc:from-text", p, 0, NULL, NULL, 0, NULL,
564 point = vmark_new(p, MARK_POINT, NULL);
565 call("doc:set-ref", p, 1, point);
566 call("doc:set-attr", p, 1, point, "markup:func", 0,
567 NULL, "doc:email:render-spacer");
570 hdrdoc = call_ret(pane, "attach-doc-text", ci->focus);
573 call("doc:set:autoclose", hdrdoc, 1);
574 point = vmark_new(hdrdoc, MARK_POINT, NULL);
578 /* copy some headers to the header temp document */
579 home_call(h2, "get-header", hdrdoc, 0, point, "From");
580 home_call(h2, "get-header", hdrdoc, 0, point, "Date");
581 home_call(h2, "get-header", hdrdoc, 0, point, "Subject", 0, NULL, "text");
582 home_call(h2, "get-header", hdrdoc, 0, point, "To", 0, NULL, "list");
583 home_call(h2, "get-header", hdrdoc, 0, point, "Cc", 0, NULL, "list");
585 /* copy some headers into attributes for later analysis */
586 call("get-header", h2, 0, NULL, "MIME-Version", 0, NULL, "cmd");
587 call("get-header", h2, 0, NULL, "content-type", 0, NULL, "cmd");
588 call("get-header", h2, 0, NULL, "content-transfer-encoding",
590 mime = attr_find(h2->attrs, "rfc822-mime-version");
592 mime = get_822_word(mime);
593 if (mime && strcmp(mime, "1.0") == 0) {
594 type = attr_find(h2->attrs, "rfc822-content-type");
595 xfer = attr_find(h2->attrs, "rfc822-content-transfer-encoding");
599 p = call_ret(pane, "attach-doc-multipart", ci->home);
602 call("doc:set:autoclose", p, 1);
603 attr_set_str(&hdrdoc->attrs, "email:actions", "hide");
604 home_call(p, "multipart-add", hdrdoc);
605 home_call(p, "multipart-add", ei->spacer);
606 call("doc:set:autoclose", hdrdoc, 1);
608 attr_set_str(&hdrdoc->attrs, "email:path", "headers");
610 if (!handle_content(ei->email, type, xfer, start, end,
611 p, ei->spacer, "body"))
616 attr_set_str(&p->attrs, "render-default", "text");
617 attr_set_str(&p->attrs, "filename", ci->str+6);
618 attr_set_str(&p->attrs, "doc-type", "email");
619 return comm_call(ci->comm2, "callback:attach", p);
634 DEF_CMD(email_view_free)
636 struct email_view *evi = ci->home->data;
643 static int get_part(struct pane *p safe, struct mark *m safe)
645 char *a = pane_mark_attr(p, m, "multipart:part-num");
652 static int count_buttons(struct pane *p safe, struct mark *m safe)
655 char *attr = pane_mark_attr(p, m, "multipart-prev:email:actions");
660 attr = strchr(attr, ':');
669 struct pane *p = ci->home;
670 struct email_view *evi = p->data;
677 ret = home_call(p->parent, ci->key, ci->focus,
678 ci->num, ci->mark, evi->invis,
680 n = get_part(p->parent, ci->mark);
681 if (ci->num2 && n > 0 && (n & 1)) {
682 /* Moving in a spacer, If after valid buttons,
686 unsigned int buttons;
687 buttons = count_buttons(p, ci->mark);
688 while (isdigit(c = doc_following(p->parent, ci->mark)) &&
689 (c - '0') >= buttons)
690 doc_next(p->parent, ci->mark);
693 ret = home_call(p->parent, ci->key, ci->focus,
694 ci->num, ci->mark, evi->invis, 1);
695 n = get_part(p->parent, ci->mark);
696 if ((n & 1) && ci->num2 && isdigit(ret & 0xfffff)) {
697 /* Just stepped back over the 9 at the end of a spacer,
698 * Maybe step further if there aren't 10 buttons.
700 unsigned int buttons = count_buttons(p, ci->mark);
701 wint_t c = ret & 0xfffff;
703 while (isdigit(c) && c - '0' >= buttons)
704 c = doc_prev(p->parent, ci->mark);
711 DEF_CMD(email_set_ref)
713 struct pane *p = ci->home;
714 struct email_view *evi = p->data;
718 home_call(p->parent, ci->key, ci->focus, ci->num, ci->mark, evi->invis);
722 DEF_CMD(email_view_get_attr)
725 struct email_view *evi = ci->home->data;
727 if (!ci->str || !ci->mark)
729 if (strcmp(ci->str, "email:visible") == 0) {
730 p = get_part(ci->home->parent, ci->mark);
731 /* only parts can be invisible, not separators */
733 v = (p >= 0 && p < evi->parts) ? evi->invis[p] != 'i' : 0;
735 return comm_call(ci->comm2, "callback", ci->focus, 0, ci->mark,
736 v ? "1":"0", 0, NULL, ci->str);
741 DEF_CMD(email_view_set_attr)
744 struct email_view *evi = ci->home->data;
746 if (!ci->str || !ci->mark)
748 if (strcmp(ci->str, "email:visible") == 0) {
749 struct mark *m1, *m2;
751 p = get_part(ci->home->parent, ci->mark);
752 /* only parts can be invisible, not separators */
754 v = ci->str2 && atoi(ci->str2) >= 1;
755 if (p >= 0 && p < evi->parts)
756 evi->invis[p] = v ? 'v' : 'i';
758 /* Tell viewers that visibility has changed */
759 m1 = mark_dup(ci->mark);
760 home_call(ci->home->parent, "doc:step-part", ci->focus,
762 if (get_part(ci->home->parent, m1) != p)
763 home_call(ci->home->parent, "doc:step-part",
768 home_call(ci->home->parent, "doc:step-part", ci->focus,
770 call("view:changed", ci->focus, 0, m1, NULL, 0, m2);
771 call("Notify:clip", ci->focus, 0, m1, NULL, 0, m2);
780 DEF_CMD(attach_email_view)
783 struct email_view *evi;
787 m = vmark_new(ci->focus, MARK_UNGROUPED, NULL);
790 call("doc:set-ref", ci->focus, 0, m);
791 n = get_part(ci->focus, m);
793 if (n <= 0 || n > 1000 )
798 evi->invis = calloc(n+1, sizeof(char));
799 memset(evi->invis, 'v', n);
800 p = pane_register(ci->focus, 0, &email_view_handle.c, evi);
805 return comm_call(ci->comm2, "callback:attach", p);
808 static void email_init_map(void)
810 email_view_map = key_alloc();
811 key_add(email_view_map, "Free", &email_view_free);
812 key_add(email_view_map, "doc:step", &email_step);
813 key_add(email_view_map, "doc:set-ref", &email_set_ref);
814 key_add(email_view_map, "doc:set-attr", &email_view_set_attr);
815 key_add(email_view_map, "doc:get-attr", &email_view_get_attr);
816 key_add(email_view_map, "doc:email:render-spacer", &email_spacer);
817 key_add(email_view_map, "doc:email:select", &email_select);
820 void edlib_init(struct pane *ed safe)
823 call_comm("global-set-command", ed, &open_email, 0, NULL,
825 call_comm("global-set-command", ed, &attach_email_view, 0, NULL,
826 "attach-email-view");
828 call("global-load-module", ed, 0, NULL, "lib-html-to-text");