]> git.neil.brown.name Git - edlib.git/blob - lib-rfc822header.c
TODO: clean out done items.
[edlib.git] / lib-rfc822header.c
1 /*
2  * Copyright Neil Brown ©2016-2021 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * lib-rfc822header: parse rfc822 email headers.
6  * When instanciated, headers in the parent document are parsed and a mark
7  * is moved beyond the headers.
8  * Subsequently the "get-header" command and be used to extract headers.
9  * If a focus/point is given, the header is copied into the target pane
10  * with charset decoding performed and some attributes added to allow
11  * control over the display.
12  * If no point is given, the named header is parsed and added to this
13  * pane as an attribute. Optionally comments are removed.
14  *
15  * RFC2047 allows headers to contains words:
16  *  =?charset?encoding?text?=
17  *  "charset" can be an set, e.g. "iso-8859-1" "utf-8" "us-ascii" "Windows-1252"
18  *     Currently support utf-8 and us-ascii transparently, others if
19  *     a converter exists.
20  *  "encoding" can be Q or B (or q or b)
21  *     Q recognizes '=' and treat next 2 has HEX, and '_' implies SPACE
22  *     B is base64.
23  */
24
25 #define _GNU_SOURCE /*  for asprintf */
26 #include <unistd.h>
27 #include <stdlib.h>
28 #include <fcntl.h>
29 #include <string.h>
30 #include <stdio.h>
31 #include <ctype.h>
32
33 #define PANE_DATA_TYPE struct header_info
34 #include "core.h"
35 #include "misc.h"
36
37 struct header_info {
38         int vnum;
39 };
40 #include "core-pane.h"
41
42 static char *get_hname(struct pane *p safe, struct mark *m safe)
43 {
44         char hdr[80];
45         int len = 0;
46         wint_t ch;
47
48         while ((ch = doc_next(p, m)) != ':' &&
49                (ch > ' ' && ch <= '~')) {
50                 hdr[len++] = ch;
51                 if (len > 77)
52                         break;
53         }
54         hdr[len] = 0;
55         if (len == 0 || ch != ':')
56                 return NULL;
57         return strdup(hdr);
58 }
59
60 static void find_headers(struct pane *p safe, struct mark *start safe,
61                          struct mark *end safe)
62 {
63         struct header_info *hi = p->data;
64         struct mark *m, *hm safe;
65         wint_t ch;
66         char *hname;
67
68         m = vmark_new(p, hi->vnum, p);
69         if (!m)
70                 return;
71         mark_to_mark(m, start);
72         hm = mark_dup_view(m);
73         while (m->seq < end->seq &&
74                (hname = get_hname(p, m)) != NULL) {
75                 attr_set_str(&hm->attrs, "header", hname);
76                 free(hname);
77                 while ((ch = doc_next(p, m)) != WEOF &&
78                        m->seq < end->seq &&
79                        (ch != '\n' ||
80                         (ch = doc_following(p, m)) == ' ' || ch == '\t'))
81                         ;
82                 hm = mark_dup_view(m);
83         }
84         /* Skip over trailing blank line */
85         if (doc_following(p, m) == '\r')
86                 doc_next(p, m);
87         if (doc_following(p, m) == '\n')
88                 doc_next(p, m);
89         mark_to_mark(start, m);
90         mark_free(m);
91 }
92
93 static int from_hex(char c)
94 {
95         if (c >= '0' && c <= '9')
96                 return c - '0';
97         if (c >= 'a' && c <= 'f')
98                 return 10 + c - 'a';
99         if (c >= 'A' && c <= 'F')
100                 return 10 + c - 'A';
101         return 0;
102 }
103
104 static int is_b64(char c)
105 {
106         return (c >= 'A' && c <= 'Z') ||
107                 (c >= 'a' && c <= 'z') ||
108                 (c >= '0' && c <= '9') ||
109                 c == '+' || c == '/' || c == '=';
110 }
111
112 static int from_b64(char c)
113 {
114         /* This assumes that 'c' is_b64() */
115         if (c <= '+')
116                 return 62;
117         else if (c <= '9')
118                 return (c - '0') + 52;
119         else if (c == '=')
120                 return 64;
121         else if (c <= 'Z')
122                 return (c - 'A') + 0;
123         else if (c == '/')
124                 return 63;
125         else
126                 return (c - 'a') + 26;
127 }
128
129 static char *safe charset_word(struct pane *doc safe, struct mark *m safe)
130 {
131         /* RFC2047 decoding.
132          * Search for second '?' and capture charset, detect 'Q' or 'B',
133          * then decode based on that.
134          * Finish on ?= or non-printable
135          * =?charset?encoding?code?=
136          */
137         struct buf buf;
138         int qmarks = 0;
139         char code = 0;
140         int bits = -1;
141         int tmp = 0;
142         static char *last = NULL;
143         char *charset = NULL;
144         wint_t ch;
145         struct mark *m2;
146
147         free(last);
148         last = NULL;
149
150         buf_init(&buf);
151         while ((ch = doc_next(doc, m)) != WEOF &&
152                ch > ' ' && ch < 0x7f && qmarks < 4) {
153                 if (ch == '?') {
154                         if (qmarks == 2) {
155                                 charset = buf_final(&buf);
156                                 buf_init(&buf);
157                         }
158                         qmarks++;
159                         continue;
160                 }
161                 if (qmarks < 3 && isupper(ch))
162                         ch = tolower(ch);
163                 if (qmarks == 1) {
164                         /* gathering charset */
165                         buf_append(&buf, ch);
166                         continue;
167                 }
168                 if (qmarks == 2 && ch == 'q')
169                         code = 'q';
170                 if (qmarks == 2 && ch == 'b')
171                         code = 'b';
172                 if (qmarks != 3)
173                         continue;
174                 switch(code) {
175                 default:
176                         buf_append(&buf, ch);
177                         break;
178                 case 'q':
179                         if (bits >= 0) {
180                                 tmp = (tmp<<4) + from_hex(ch);
181                                 bits += 4;
182                                 if (bits == 8) {
183                                         buf_append_byte(&buf, tmp);
184                                         tmp = 0;
185                                         bits = -1;
186                                 }
187                                 break;
188                         }
189                         switch(ch) {
190                         default:
191                                 buf_append(&buf, ch);
192                                 break;
193                         case '_':
194                                 buf_append(&buf, ' ');
195                                 break;
196                         case '=':
197                                 tmp = 0;
198                                 bits = 0;
199                                 break;
200                         }
201                         break;
202
203                 case 'b':
204                         if (bits < 0) {
205                                 bits = 0;
206                                 tmp = 0;
207                         }
208                         if (!is_b64(ch) || ch == '=')
209                                 break;
210                         tmp = (tmp << 6) | from_b64(ch);
211                         bits += 6;
212                         if (bits >= 8) {
213                                 bits -= 8;
214                                 buf_append_byte(&buf, (tmp >> bits) & 255);
215                                 tmp &= (1<<bits)-1;
216                         }
217                         break;
218                 }
219         }
220         last = buf_final(&buf);
221         if (charset && last) {
222                 char *cmd = NULL;
223                 char *cvt = NULL;
224
225                 asprintf(&cmd, "charset-to-utf8-%s", charset);
226                 if (cmd)
227                         cvt = call_ret(str, cmd, doc, 0, NULL, last);
228                 if (cvt) {
229                         free(last);
230                         last = cvt;
231                 }
232         }
233         /* If there is only LWS to the next quoted word,
234          * skip that so words join up
235          */
236         m2 = mark_dup(m);
237         if (!m2)
238                 return last;
239         while ((ch = doc_next(doc, m2)) == ' ' ||
240                ch == '\t' || ch == '\r' || ch == '\n')
241                 ;
242         if (ch == '=' && doc_following(doc, m2) == '?') {
243                 doc_prev(doc, m2);
244                 mark_to_mark(m, m2);
245         }
246         mark_free(m2);
247         return last;
248 }
249
250 static void add_addr(struct pane *p safe, struct mark *m safe,
251                      struct mark *pnt safe, int len,
252                      const char *hdr)
253 {
254         char buf[2*sizeof(int)*8/3 + 3 + 20];
255         char *addr;
256         int tag;
257
258         if (len <= 0)
259                 return;
260         tag = attr_find_int(p->attrs, "rfc822-addr-cnt");
261         if (tag < 0)
262                 tag = 0;
263         tag += 1;
264         snprintf(buf, sizeof(buf), "%d,%d,%s", len, tag, hdr);
265         call("doc:set-attr", p, 1, m,
266              "render:rfc822header-addr", 0, NULL, buf);
267
268         addr = call_ret(str,"doc:get-str", p, 0, m, NULL, 0, pnt);
269         while (addr && utf8_strlen(addr) > len) {
270                 int l = utf8_round_len(addr, strlen(addr)-1);
271                 addr[l] = 0;
272         }
273         snprintf(buf, sizeof(buf), "addr-%d", tag);
274         attr_set_str(&p->attrs, buf, addr);
275
276         attr_set_int(&p->attrs, "rfc822-addr-cnt", tag);
277 }
278
279 static void copy_header(struct pane *doc safe,
280                         const char *hdr safe, const char *hdr_found safe,
281                         const char *type,
282                         struct mark *start safe, struct mark *end safe,
283                         struct pane *p safe, struct mark *point safe)
284 {
285         /* Copy the header in 'doc' from 'start' to 'end' into
286          * the document 'p' at 'point'.
287          * 'type' can be:
288          *  NULL : no explicit wrapping
289          *  "text": no explicit wrapping
290          *  "list": convert commas to wrap points.
291          * 'hdr' is the name of the header -  before the ':'.
292          * '\n', '\r' are copied as a single space, and subsequent
293          * spaces are skipped.
294          */
295         struct mark *m;
296         struct mark *hstart;
297         int sol = 0;
298         char buf[20];
299         wint_t ch;
300         char attr[100];
301         char *a;
302         int is_list = type && strcmp(type, "list") == 0;
303         struct mark *istart = NULL;
304         int ilen = 0, isince = 0;
305         bool seen_colon = False;
306
307         m = mark_dup(start);
308         hstart = mark_dup(point);
309         /* put hstart before point, so it stays here */
310         mark_step(hstart, 0);
311         while ((ch = doc_next(doc, m)) != WEOF &&
312                m->seq < end->seq) {
313                 char *b;
314                 int i;
315
316                 if (ch < ' ' && ch != '\t') {
317                         sol = 1;
318                         continue;
319                 }
320                 if (sol && (ch == ' ' || ch == '\t'))
321                         continue;
322                 if (sol && !(is_list && ilen == 0)) {
323                         call("doc:replace", p, 1, NULL, " ", 0, point);
324                         isince += 1;
325                 }
326                 sol = 0;
327                 buf[0] = ch;
328                 buf[1] = 0;
329                 if (ch == '=' && doc_following(doc, m) == '?')
330                         b = charset_word(doc, m);
331                 else
332                         b = buf;
333                 for (i = 0; b[i]; i++)
334                         if (b[i] > 0 && b[i] < ' ')
335                                 b[i] = ' ';
336                 if (is_list && seen_colon && !istart && b[0] != ',' &&
337                     (b[0] != ' ' || b[1] != '\0')) {
338                         /* This looks like the start of a list item. */
339                         istart = mark_dup(point);
340                         mark_step(istart, 0);
341                         ilen = isince = 0;
342                 }
343                 if (b[0] == ':')
344                         seen_colon = True;
345                 call("doc:replace", p, 1, NULL, b, 0, point);
346                 if (ch == ',' && istart) {
347                         add_addr(p, istart, point, ilen, hdr);
348                         mark_free(istart);
349                         istart = NULL;
350                 }
351                 isince += utf8_strlen(b);
352                 if (b[0] != ' ')
353                         ilen = isince;
354                 if (ch == ',' && is_list) {
355                         /* This comma is not in a quoted word, so it really marks
356                          * part of a list, and so is a wrap-point.  Consume any
357                          * following spaces and include just one space in
358                          * the result.
359                          */
360                         struct mark *p2 = mark_dup(point);
361                         doc_prev(p, p2);
362                         while ((ch = doc_following(doc, m)) == ' ')
363                                 doc_next(doc, m);
364
365                         call("doc:replace", p, 1, NULL, " ", 0, point);
366                         call("doc:set-attr", p, 1, p2,
367                              "render:rfc822header-wrap", 0, NULL, "2");
368                         mark_free(p2);
369
370                         istart = mark_dup(point);
371                         mark_step(istart, 0);
372                         ilen = isince = 0;
373                 }
374         }
375         if (istart) {
376                 add_addr(p, istart, point, ilen, hdr);
377                 mark_free(istart);
378         }
379         call("doc:replace", p, 1, NULL, "\n", 0, point);
380         snprintf(buf, sizeof(buf), "%zd", strlen(hdr_found)+1);
381         call("doc:set-attr", p, 1, hstart, "render:rfc822header", 0, NULL, buf);
382         snprintf(attr, sizeof(attr), "render:rfc822header:%s", hdr_found);
383         /* make header name lowercase */
384         for (a = attr; *a; a++) {
385                 if ((unsigned char)(*a) < 128 && isupper(*a))
386                         *a = tolower(*a);
387         }
388         call("doc:set-attr", p, 1, hstart, attr, 0, NULL, type);
389
390         mark_free(hstart);
391         mark_free(m);
392 }
393
394 static void copy_headers(struct pane *p safe, const char *hdr safe,
395                          const char *type,
396                          struct pane *doc safe, struct mark *pt safe,
397                          bool resent)
398 {
399         struct header_info *hi = p->data;
400         struct mark *m, *n;
401
402         for (m = vmark_first(p, hi->vnum, p); m ; m = n) {
403                 char *h = attr_find(m->attrs, "header");
404                 char *horig = h;
405                 while (resent && h &&
406                        strncasecmp(h, "resent-", 7) == 0)
407                         h += 7;
408                 n = vmark_next(m);
409                 if (n && horig && h && strcasecmp(h, hdr) == 0)
410                         copy_header(p, hdr, horig, type, m, n, doc, pt);
411         }
412 }
413
414 static char *extract_header(struct pane *p safe, struct mark *start safe,
415                             struct mark *end safe)
416
417 {
418         /* This is used for headers that control parsing, such as
419          * MIME-Version and Content-type.
420          */
421         struct mark *m;
422         int sol = 0;
423         int found = 0;
424         struct buf buf;
425         wint_t ch;
426
427         buf_init(&buf);
428         m = mark_dup(start);
429         while ((ch = doc_next(p, m)) != WEOF &&
430                m->seq < end->seq) {
431                 if (!found && ch == ':') {
432                         found = 1;
433                         continue;
434                 }
435                 if (!found)
436                         continue;
437                 if (ch < ' ' && ch != '\t') {
438                         sol = 1;
439                         continue;
440                 }
441                 if (sol && (ch == ' ' || ch == '\t'))
442                         continue;
443                 if (sol) {
444                         buf_append(&buf, ' ');
445                         sol = 0;
446                 }
447                 if (ch == '=' && doc_following(p, m) == '?') {
448                         char *b = charset_word(p, m);
449                         buf_concat(&buf, b);
450                 } else
451                         buf_append(&buf, ch);
452         }
453         return buf_final(&buf);
454 }
455
456 static char *load_header(struct pane *home safe, const char *hdr safe)
457 {
458         struct header_info *hi = home->data;
459         struct mark *m, *n;
460
461         for (m = vmark_first(home, hi->vnum, home); m; m = n) {
462                 char *h = attr_find(m->attrs, "header");
463                 n = vmark_next(m);
464                 if (n && h && strcasecmp(h, hdr) == 0)
465                         return extract_header(home, m, n);
466         }
467         return NULL;
468 }
469
470 DEF_CMD(header_get)
471 {
472         const char *hdr = ci->str;
473         const char *type = ci->str2;
474         bool resent = ci->num2 == 1;
475         char *attr = NULL;
476         char *c, *t;
477
478         if (!hdr)
479                 return Enoarg;
480
481         if (ci->mark) {
482                 copy_headers(ci->home, hdr, type, ci->focus, ci->mark, resent);
483                 return 1;
484         }
485         asprintf(&attr, "rfc822-%s", hdr);
486         if (!attr)
487                 return Efail;
488         for (c = attr; *c; c++)
489                 if (isupper(*c))
490                         *c = tolower(*c);
491         t = load_header(ci->home, hdr);
492         attr_set_str(&ci->home->attrs, attr, t);
493         free(attr);
494         free(t);
495         return t ? 1 : 2;
496 }
497
498 DEF_CMD(header_list)
499 {
500         /* Call comm2 for each header matching str */
501         struct header_info *hi = ci->home->data;
502         struct mark *m, *n;
503
504         if (!ci->str || !ci->comm2)
505                 return Enoarg;
506         for (m = vmark_first(ci->home, hi->vnum, ci->home); m; m = n) {
507                 char *h = attr_find(m->attrs, "header");
508                 n = vmark_next(m);
509                 if (n && h && strcasecmp(h, ci->str) == 0) {
510                         h = extract_header(ci->home, m, n);
511                         if (comm_call(ci->comm2, "cb", ci->focus,
512                                       0, NULL, h) <= 0)
513                                 n = NULL;
514                         free(h);
515                 }
516         }
517         return 1;
518 }
519
520 DEF_CMD(header_clip)
521 {
522         struct header_info *hi = ci->home->data;
523
524         marks_clip(ci->home, ci->mark, ci->mark2, hi->vnum, ci->home, !!ci->num);
525         return Efallthrough;
526 }
527
528 static struct map *header_map safe;
529
530 static void header_init_map(void)
531 {
532         header_map = key_alloc();
533         key_add(header_map, "get-header", &header_get);
534         key_add(header_map, "list-headers", &header_list);
535         key_add(header_map, "Notify:clip", &header_clip);
536 }
537
538 DEF_LOOKUP_CMD(header_handle, header_map);
539 DEF_CMD(header_attach)
540 {
541         struct header_info *hi;
542         struct pane *p;
543         struct mark *start = ci->mark;
544         struct mark *end = ci->mark2;
545
546         p = pane_register(ci->focus, 0, &header_handle.c);
547         if (!p)
548                 return Efail;
549         hi = p->data;
550
551         hi->vnum = home_call(ci->focus, "doc:add-view", p) - 1;
552         if (start && end)
553                 find_headers(p, start, end);
554
555         return comm_call(ci->comm2, "callback:attach", p);
556 }
557
558 void edlib_init(struct pane *ed safe)
559 {
560         header_init_map();
561         call_comm("global-set-command", ed, &header_attach, 0, NULL, "attach-rfc822header");
562 }