Don't parse any headers in the real body of an email message.
[git.git] / mailinfo.c
1 /*
2  * Another stupid program, this one parsing the headers of an
3  * email to figure out authorship and subject
4  */
5 #define _GNU_SOURCE
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <ctype.h>
10 #ifndef NO_ICONV
11 #include <iconv.h>
12 #endif
13 #include "git-compat-util.h"
14 #include "cache.h"
15
16 static FILE *cmitmsg, *patchfile;
17
18 static int keep_subject = 0;
19 static char *metainfo_charset = NULL;
20 static char line[1000];
21 static char date[1000];
22 static char name[1000];
23 static char email[1000];
24 static char subject[1000];
25
26 static enum  {
27         TE_DONTCARE, TE_QP, TE_BASE64,
28 } transfer_encoding;
29 static char charset[256];
30
31 static char multipart_boundary[1000];
32 static int multipart_boundary_len;
33 static int patch_lines = 0;
34
35 static char *sanity_check(char *name, char *email)
36 {
37         int len = strlen(name);
38         if (len < 3 || len > 60)
39                 return email;
40         if (strchr(name, '@') || strchr(name, '<') || strchr(name, '>'))
41                 return email;
42         return name;
43 }
44
45 static int bogus_from(char *line)
46 {
47         /* John Doe <johndoe> */
48         char *bra, *ket, *dst, *cp;
49
50         /* This is fallback, so do not bother if we already have an
51          * e-mail address.
52          */ 
53         if (*email)
54                 return 0;
55
56         bra = strchr(line, '<');
57         if (!bra)
58                 return 0;
59         ket = strchr(bra, '>');
60         if (!ket)
61                 return 0;
62
63         for (dst = email, cp = bra+1; cp < ket; )
64                 *dst++ = *cp++;
65         *dst = 0;
66         for (cp = line; isspace(*cp); cp++)
67                 ;
68         for (bra--; isspace(*bra); bra--)
69                 *bra = 0;
70         cp = sanity_check(cp, email);
71         strcpy(name, cp);
72         return 1;
73 }
74
75 static int handle_from(char *in_line)
76 {
77         char line[1000];
78         char *at;
79         char *dst;
80
81         strcpy(line, in_line);
82         at = strchr(line, '@');
83         if (!at)
84                 return bogus_from(line);
85
86         /*
87          * If we already have one email, don't take any confusing lines
88          */
89         if (*email && strchr(at+1, '@'))
90                 return 0;
91
92         /* Pick up the string around '@', possibly delimited with <>
93          * pair; that is the email part.  White them out while copying.
94          */
95         while (at > line) {
96                 char c = at[-1];
97                 if (isspace(c))
98                         break;
99                 if (c == '<') {
100                         at[-1] = ' ';
101                         break;
102                 }
103                 at--;
104         }
105         dst = email;
106         for (;;) {
107                 unsigned char c = *at;
108                 if (!c || c == '>' || isspace(c)) {
109                         if (c == '>')
110                                 *at = ' ';
111                         break;
112                 }
113                 *at++ = ' ';
114                 *dst++ = c;
115         }
116         *dst++ = 0;
117
118         /* The remainder is name.  It could be "John Doe <john.doe@xz>"
119          * or "john.doe@xz (John Doe)", but we have whited out the
120          * email part, so trim from both ends, possibly removing
121          * the () pair at the end.
122          */
123         at = line + strlen(line);
124         while (at > line) {
125                 unsigned char c = *--at;
126                 if (!isspace(c)) {
127                         at[(c == ')') ? 0 : 1] = 0;
128                         break;
129                 }
130         }
131
132         at = line;
133         for (;;) {
134                 unsigned char c = *at;
135                 if (!c || !isspace(c)) {
136                         if (c == '(')
137                                 at++;
138                         break;
139                 }
140                 at++;
141         }
142         at = sanity_check(at, email);
143         strcpy(name, at);
144         return 1;
145 }
146
147 static int handle_date(char *line)
148 {
149         strcpy(date, line);
150         return 0;
151 }
152
153 static int handle_subject(char *line)
154 {
155         strcpy(subject, line);
156         return 0;
157 }
158
159 /* NOTE NOTE NOTE.  We do not claim we do full MIME.  We just attempt
160  * to have enough heuristics to grok MIME encoded patches often found
161  * on our mailing lists.  For example, we do not even treat header lines
162  * case insensitively.
163  */
164
165 static int slurp_attr(const char *line, const char *name, char *attr)
166 {
167         char *ends, *ap = strcasestr(line, name);
168         size_t sz;
169
170         if (!ap) {
171                 *attr = 0;
172                 return 0;
173         }
174         ap += strlen(name);
175         if (*ap == '"') {
176                 ap++;
177                 ends = "\"";
178         }
179         else
180                 ends = "; \t";
181         sz = strcspn(ap, ends);
182         memcpy(attr, ap, sz);
183         attr[sz] = 0;
184         return 1;
185 }
186
187 static int handle_subcontent_type(char *line)
188 {
189         /* We do not want to mess with boundary.  Note that we do not
190          * handle nested multipart.
191          */
192         if (strcasestr(line, "boundary=")) {
193                 fprintf(stderr, "Not handling nested multipart message.\n");
194                 exit(1);
195         }
196         slurp_attr(line, "charset=", charset);
197         if (*charset) {
198                 int i, c;
199                 for (i = 0; (c = charset[i]) != 0; i++)
200                         charset[i] = tolower(c);
201         }
202         return 0;
203 }
204
205 static int handle_content_type(char *line)
206 {
207         *multipart_boundary = 0;
208         if (slurp_attr(line, "boundary=", multipart_boundary + 2)) {
209                 memcpy(multipart_boundary, "--", 2);
210                 multipart_boundary_len = strlen(multipart_boundary);
211         }
212         slurp_attr(line, "charset=", charset);
213         return 0;
214 }
215
216 static int handle_content_transfer_encoding(char *line)
217 {
218         if (strcasestr(line, "base64"))
219                 transfer_encoding = TE_BASE64;
220         else if (strcasestr(line, "quoted-printable"))
221                 transfer_encoding = TE_QP;
222         else
223                 transfer_encoding = TE_DONTCARE;
224         return 0;
225 }
226
227 static int is_multipart_boundary(const char *line)
228 {
229         return (!memcmp(line, multipart_boundary, multipart_boundary_len));
230 }
231
232 static int eatspace(char *line)
233 {
234         int len = strlen(line);
235         while (len > 0 && isspace(line[len-1]))
236                 line[--len] = 0;
237         return len;
238 }
239
240 #define SEEN_FROM 01
241 #define SEEN_DATE 02
242 #define SEEN_SUBJECT 04
243 #define SEEN_BOGUS_UNIX_FROM 010
244 #define SEEN_PREFIX  020
245
246 /* First lines of body can have From:, Date:, and Subject: */
247 static void handle_inbody_header(int *seen, char *line)
248 {
249         if (*seen & SEEN_PREFIX)
250                 return;
251         if (!memcmp(">From", line, 5) && isspace(line[5])) {
252                 if (!(*seen & SEEN_BOGUS_UNIX_FROM)) {
253                         *seen |= SEEN_BOGUS_UNIX_FROM;
254                         return;
255                 }
256         }
257         if (!memcmp("From:", line, 5) && isspace(line[5])) {
258                 if (!(*seen & SEEN_FROM) && handle_from(line+6)) {
259                         *seen |= SEEN_FROM;
260                         return;
261                 }
262         }
263         if (!memcmp("Date:", line, 5) && isspace(line[5])) {
264                 if (!(*seen & SEEN_DATE)) {
265                         handle_date(line+6);
266                         *seen |= SEEN_DATE;
267                         return;
268                 }
269         }
270         if (!memcmp("Subject:", line, 8) && isspace(line[8])) {
271                 if (!(*seen & SEEN_SUBJECT)) {
272                         handle_subject(line+9);
273                         *seen |= SEEN_SUBJECT;
274                         return;
275                 }
276         }
277         if (!memcmp("[PATCH]", line, 7) && isspace(line[7])) {
278                 if (!(*seen & SEEN_SUBJECT)) {
279                         handle_subject(line);
280                         *seen |= SEEN_SUBJECT;
281                         return;
282                 }
283         }
284         *seen |= SEEN_PREFIX;
285 }
286
287 static char *cleanup_subject(char *subject)
288 {
289         if (keep_subject)
290                 return subject;
291         for (;;) {
292                 char *p;
293                 int len, remove;
294                 switch (*subject) {
295                 case 'r': case 'R':
296                         if (!memcmp("e:", subject+1, 2)) {
297                                 subject +=3;
298                                 continue;
299                         }
300                         break;
301                 case ' ': case '\t': case ':':
302                         subject++;
303                         continue;
304
305                 case '[':
306                         p = strchr(subject, ']');
307                         if (!p) {
308                                 subject++;
309                                 continue;
310                         }
311                         len = strlen(p);
312                         remove = p - subject;
313                         if (remove <= len *2) {
314                                 subject = p+1;
315                                 continue;
316                         }       
317                         break;
318                 }
319                 return subject;
320         }
321 }                       
322
323 static void cleanup_space(char *buf)
324 {
325         unsigned char c;
326         while ((c = *buf) != 0) {
327                 buf++;
328                 if (isspace(c)) {
329                         buf[-1] = ' ';
330                         c = *buf;
331                         while (isspace(c)) {
332                                 int len = strlen(buf);
333                                 memmove(buf, buf+1, len);
334                                 c = *buf;
335                         }
336                 }
337         }
338 }
339
340 static void decode_header_bq(char *it);
341 typedef int (*header_fn_t)(char *);
342 struct header_def {
343         const char *name;
344         header_fn_t func;
345         int namelen;
346 };
347
348 static void check_header(char *line, struct header_def *header)
349 {
350         int i;
351
352         if (header[0].namelen <= 0) {
353                 for (i = 0; header[i].name; i++)
354                         header[i].namelen = strlen(header[i].name);
355         }
356         for (i = 0; header[i].name; i++) {
357                 int len = header[i].namelen;
358                 if (!strncasecmp(line, header[i].name, len) &&
359                     line[len] == ':' && isspace(line[len + 1])) {
360                         /* Unwrap inline B and Q encoding, and optionally
361                          * normalize the meta information to utf8.
362                          */
363                         decode_header_bq(line + len + 2);
364                         header[i].func(line + len + 2);
365                         break;
366                 }
367         }
368 }
369
370 static void check_subheader_line(char *line)
371 {
372         static struct header_def header[] = {
373                 { "Content-Type", handle_subcontent_type },
374                 { "Content-Transfer-Encoding",
375                   handle_content_transfer_encoding },
376                 { NULL },
377         };
378         check_header(line, header);
379 }
380 static void check_header_line(char *line)
381 {
382         static struct header_def header[] = {
383                 { "From", handle_from },
384                 { "Date", handle_date },
385                 { "Subject", handle_subject },
386                 { "Content-Type", handle_content_type },
387                 { "Content-Transfer-Encoding",
388                   handle_content_transfer_encoding },
389                 { NULL },
390         };
391         check_header(line, header);
392 }
393
394 static int is_rfc2822_header(char *line)
395 {
396         /*
397          * The section that defines the loosest possible
398          * field name is "3.6.8 Optional fields".
399          *
400          * optional-field = field-name ":" unstructured CRLF
401          * field-name = 1*ftext
402          * ftext = %d33-57 / %59-126
403          */
404         int ch;
405         char *cp = line;
406         while ((ch = *cp++)) {
407                 if (ch == ':')
408                         return cp != line;
409                 if ((33 <= ch && ch <= 57) ||
410                     (59 <= ch && ch <= 126))
411                         continue;
412                 break;
413         }
414         return 0;
415 }
416
417 static int read_one_header_line(char *line, int sz, FILE *in)
418 {
419         int ofs = 0;
420         while (ofs < sz) {
421                 int peek, len;
422                 if (fgets(line + ofs, sz - ofs, in) == NULL)
423                         break;
424                 len = eatspace(line + ofs);
425                 if (len == 0)
426                         break;
427                 if (!is_rfc2822_header(line)) {
428                         /* Re-add the newline */
429                         line[ofs + len] = '\n';
430                         line[ofs + len + 1] = '\0';
431                         break;
432                 }
433                 ofs += len;
434                 /* Yuck, 2822 header "folding" */
435                 peek = fgetc(in); ungetc(peek, in);
436                 if (peek != ' ' && peek != '\t')
437                         break;
438         }
439         /* Count mbox From headers as headers */
440         if (!ofs && !memcmp(line, "From ", 5))
441                 ofs = 1;
442         return ofs;
443 }
444
445 static unsigned hexval(int c)
446 {
447         if (c >= '0' && c <= '9')
448                 return c - '0';
449         if (c >= 'a' && c <= 'f')
450                 return c - 'a' + 10;
451         if (c >= 'A' && c <= 'F')
452                 return c - 'A' + 10;
453         return ~0;
454 }
455
456 static int decode_q_segment(char *in, char *ot, char *ep, int rfc2047)
457 {
458         int c;
459         while ((c = *in++) != 0 && (in <= ep)) {
460                 if (c == '=') {
461                         int d = *in++;
462                         if (d == '\n' || !d)
463                                 break; /* drop trailing newline */
464                         *ot++ = ((hexval(d) << 4) | hexval(*in++));
465                         continue;
466                 }
467                 if (rfc2047 && c == '_') /* rfc2047 4.2 (2) */
468                         c = 0x20;
469                 *ot++ = c;
470         }
471         *ot = 0;
472         return 0;
473 }
474
475 static int decode_b_segment(char *in, char *ot, char *ep)
476 {
477         /* Decode in..ep, possibly in-place to ot */
478         int c, pos = 0, acc = 0;
479
480         while ((c = *in++) != 0 && (in <= ep)) {
481                 if (c == '+')
482                         c = 62;
483                 else if (c == '/')
484                         c = 63;
485                 else if ('A' <= c && c <= 'Z')
486                         c -= 'A';
487                 else if ('a' <= c && c <= 'z')
488                         c -= 'a' - 26;
489                 else if ('0' <= c && c <= '9')
490                         c -= '0' - 52;
491                 else if (c == '=') {
492                         /* padding is almost like (c == 0), except we do
493                          * not output NUL resulting only from it;
494                          * for now we just trust the data.
495                          */
496                         c = 0;
497                 }
498                 else
499                         continue; /* garbage */
500                 switch (pos++) {
501                 case 0:
502                         acc = (c << 2);
503                         break;
504                 case 1:
505                         *ot++ = (acc | (c >> 4));
506                         acc = (c & 15) << 4;
507                         break;
508                 case 2:
509                         *ot++ = (acc | (c >> 2));
510                         acc = (c & 3) << 6;
511                         break;
512                 case 3:
513                         *ot++ = (acc | c);
514                         acc = pos = 0;
515                         break;
516                 }
517         }
518         *ot = 0;
519         return 0;
520 }
521
522 static void convert_to_utf8(char *line, char *charset)
523 {
524 #ifndef NO_ICONV
525         char *in, *out;
526         size_t insize, outsize, nrc;
527         char outbuf[4096]; /* cheat */
528         static char latin_one[] = "latin1";
529         char *input_charset = *charset ? charset : latin_one;
530         iconv_t conv = iconv_open(metainfo_charset, input_charset);
531
532         if (conv == (iconv_t) -1) {
533                 static int warned_latin1_once = 0;
534                 if (input_charset != latin_one) {
535                         fprintf(stderr, "cannot convert from %s to %s\n",
536                                 input_charset, metainfo_charset);
537                         *charset = 0;
538                 }
539                 else if (!warned_latin1_once) {
540                         warned_latin1_once = 1;
541                         fprintf(stderr, "tried to convert from %s to %s, "
542                                 "but your iconv does not work with it.\n",
543                                 input_charset, metainfo_charset);
544                 }
545                 return;
546         }
547         in = line;
548         insize = strlen(in);
549         out = outbuf;
550         outsize = sizeof(outbuf);
551         nrc = iconv(conv, &in, &insize, &out, &outsize);
552         iconv_close(conv);
553         if (nrc == (size_t) -1)
554                 return;
555         *out = 0;
556         strcpy(line, outbuf);
557 #endif
558 }
559
560 static void decode_header_bq(char *it)
561 {
562         char *in, *out, *ep, *cp, *sp;
563         char outbuf[1000];
564
565         in = it;
566         out = outbuf;
567         while ((ep = strstr(in, "=?")) != NULL) {
568                 int sz, encoding;
569                 char charset_q[256], piecebuf[256];
570                 if (in != ep) {
571                         sz = ep - in;
572                         memcpy(out, in, sz);
573                         out += sz;
574                         in += sz;
575                 }
576                 /* E.g.
577                  * ep : "=?iso-2022-jp?B?GyR...?= foo"
578                  * ep : "=?ISO-8859-1?Q?Foo=FCbar?= baz"
579                  */
580                 ep += 2;
581                 cp = strchr(ep, '?');
582                 if (!cp)
583                         return; /* no munging */
584                 for (sp = ep; sp < cp; sp++)
585                         charset_q[sp - ep] = tolower(*sp);
586                 charset_q[cp - ep] = 0;
587                 encoding = cp[1];
588                 if (!encoding || cp[2] != '?')
589                         return; /* no munging */
590                 ep = strstr(cp + 3, "?=");
591                 if (!ep)
592                         return; /* no munging */
593                 switch (tolower(encoding)) {
594                 default:
595                         return; /* no munging */
596                 case 'b':
597                         sz = decode_b_segment(cp + 3, piecebuf, ep);
598                         break;
599                 case 'q':
600                         sz = decode_q_segment(cp + 3, piecebuf, ep, 1);
601                         break;
602                 }
603                 if (sz < 0)
604                         return;
605                 if (metainfo_charset)
606                         convert_to_utf8(piecebuf, charset_q);
607                 strcpy(out, piecebuf);
608                 out += strlen(out);
609                 in = ep + 2;
610         }
611         strcpy(out, in);
612         strcpy(it, outbuf);
613 }
614
615 static void decode_transfer_encoding(char *line)
616 {
617         char *ep;
618
619         switch (transfer_encoding) {
620         case TE_QP:
621                 ep = line + strlen(line);
622                 decode_q_segment(line, line, ep, 0);
623                 break;
624         case TE_BASE64:
625                 ep = line + strlen(line);
626                 decode_b_segment(line, line, ep);
627                 break;
628         case TE_DONTCARE:
629                 break;
630         }
631 }
632
633 static void handle_info(void)
634 {
635         char *sub;
636
637         sub = cleanup_subject(subject);
638         cleanup_space(name);
639         cleanup_space(date);
640         cleanup_space(email);
641         cleanup_space(sub);
642
643         printf("Author: %s\nEmail: %s\nSubject: %s\nDate: %s\n\n",
644                name, email, sub, date);
645 }
646
647 /* We are inside message body and have read line[] already.
648  * Spit out the commit log.
649  */
650 static int handle_commit_msg(int *seen)
651 {
652         if (!cmitmsg)
653                 return 0;
654         do {
655                 if (!memcmp("diff -", line, 6) ||
656                     !memcmp("---", line, 3) ||
657                     !memcmp("Index: ", line, 7))
658                         break;
659                 if ((multipart_boundary[0] && is_multipart_boundary(line))) {
660                         /* We come here when the first part had only
661                          * the commit message without any patch.  We
662                          * pretend we have not seen this line yet, and
663                          * go back to the loop.
664                          */
665                         return 1;
666                 }
667
668                 /* Unwrap transfer encoding and optionally
669                  * normalize the log message to UTF-8.
670                  */
671                 decode_transfer_encoding(line);
672                 if (metainfo_charset)
673                         convert_to_utf8(line, charset);
674
675                 handle_inbody_header(seen, line);
676                 if (!(*seen & SEEN_PREFIX))
677                         continue;
678
679                 fputs(line, cmitmsg);
680         } while (fgets(line, sizeof(line), stdin) != NULL);
681         fclose(cmitmsg);
682         cmitmsg = NULL;
683         return 0;
684 }
685
686 /* We have done the commit message and have the first
687  * line of the patch in line[].
688  */
689 static void handle_patch(void)
690 {
691         do {
692                 if (multipart_boundary[0] && is_multipart_boundary(line))
693                         break;
694                 /* Only unwrap transfer encoding but otherwise do not
695                  * do anything.  We do *NOT* want UTF-8 conversion
696                  * here; we are dealing with the user payload.
697                  */
698                 decode_transfer_encoding(line);
699                 fputs(line, patchfile);
700                 patch_lines++;
701         } while (fgets(line, sizeof(line), stdin) != NULL);
702 }
703
704 /* multipart boundary and transfer encoding are set up for us, and we
705  * are at the end of the sub header.  do equivalent of handle_body up
706  * to the next boundary without closing patchfile --- we will expect
707  * that the first part to contain commit message and a patch, and
708  * handle other parts as pure patches.
709  */
710 static int handle_multipart_one_part(int *seen)
711 {
712         int n = 0;
713
714         while (fgets(line, sizeof(line), stdin) != NULL) {
715         again:
716                 n++;
717                 if (is_multipart_boundary(line))
718                         break;
719                 if (handle_commit_msg(seen))
720                         goto again;
721                 handle_patch();
722                 break;
723         }
724         if (n == 0)
725                 return -1;
726         return 0;
727 }
728
729 static void handle_multipart_body(void)
730 {
731         int seen = 0;
732         int part_num = 0;
733
734         /* Skip up to the first boundary */
735         while (fgets(line, sizeof(line), stdin) != NULL)
736                 if (is_multipart_boundary(line)) {
737                         part_num = 1;
738                         break;
739                 }
740         if (!part_num)
741                 return;
742         /* We are on boundary line.  Start slurping the subhead. */
743         while (1) {
744                 int hdr = read_one_header_line(line, sizeof(line), stdin);
745                 if (!hdr) {
746                         if (handle_multipart_one_part(&seen) < 0)
747                                 return;
748                         /* Reset per part headers */
749                         transfer_encoding = TE_DONTCARE;
750                         charset[0] = 0;
751                 }
752                 else
753                         check_subheader_line(line);
754         }
755         fclose(patchfile);
756         if (!patch_lines) {
757                 fprintf(stderr, "No patch found\n");
758                 exit(1);
759         }
760 }
761
762 /* Non multipart message */
763 static void handle_body(void)
764 {
765         int seen = 0;
766
767         if (line[0] || fgets(line, sizeof(line), stdin) != NULL) {
768                 handle_commit_msg(&seen);
769                 handle_patch();
770         }
771         fclose(patchfile);
772         if (!patch_lines) {
773                 fprintf(stderr, "No patch found\n");
774                 exit(1);
775         }
776 }
777
778 static const char mailinfo_usage[] =
779         "git-mailinfo [-k] [-u | --encoding=<encoding>] msg patch <mail >info";
780
781 int main(int argc, char **argv)
782 {
783         /* NEEDSWORK: might want to do the optional .git/ directory
784          * discovery
785          */
786         git_config(git_default_config);
787
788         while (1 < argc && argv[1][0] == '-') {
789                 if (!strcmp(argv[1], "-k"))
790                         keep_subject = 1;
791                 else if (!strcmp(argv[1], "-u"))
792                         metainfo_charset = git_commit_encoding;
793                 else if (!strncmp(argv[1], "--encoding=", 11))
794                         metainfo_charset = argv[1] + 11;
795                 else
796                         usage(mailinfo_usage);
797                 argc--; argv++;
798         }
799
800         if (argc != 3)
801                 usage(mailinfo_usage);
802         cmitmsg = fopen(argv[1], "w");
803         if (!cmitmsg) {
804                 perror(argv[1]);
805                 exit(1);
806         }
807         patchfile = fopen(argv[2], "w");
808         if (!patchfile) {
809                 perror(argv[2]);
810                 exit(1);
811         }
812         while (1) {
813                 int hdr = read_one_header_line(line, sizeof(line), stdin);
814                 if (!hdr) {
815                         if (multipart_boundary[0])
816                                 handle_multipart_body();
817                         else
818                                 handle_body();
819                         handle_info();
820                         break;
821                 }
822                 check_header_line(line);
823         }
824         return 0;
825 }