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