addrutil: Re-indent.
[wk-misc.git] / scrutmime.c
1 /* scrutmime.c - Look at MIME mails.
2  *      Copyright (C) 2004 g10 Code GmbH
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
17  */
18
19
20 /* Utility to help filter out spam.  This one identifies attachments. */
21
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <stddef.h>
25 #include <string.h>
26 #include <errno.h>
27 #include <stdarg.h>
28 #include <assert.h>
29 #include <time.h>
30 #include <signal.h>
31 #include <unistd.h>
32 #include <fcntl.h>
33 #include <sys/wait.h>
34
35 #include "rfc822parse.h"
36
37
38 #define PGM "scrutmime"
39 #define VERSION "1.0"
40
41 /* Option flags. */
42 static int verbose;
43 static int quiet;
44 static int debug;
45 static int opt_match_zip;
46 static int opt_match_exe;
47 static int opt_match_html;
48
49
50 enum mime_types 
51   {
52     MT_NONE = 0,
53     MT_OCTET_STREAM,
54     MT_AUDIO,
55     MT_IMAGE,
56     MT_TEXT_HTML
57   };
58
59 enum transfer_encodings
60   {
61     TE_NONE = 0,
62     TE_BASE64
63   };
64
65
66 /* Structure used to communicate with the parser callback. */
67 struct parse_info_s {
68   enum mime_types mime_type;
69   enum transfer_encodings transfer_encoding;
70   int test_base64; /* Set if we should decode and test base64 data. */
71   int got_probe;
72   int no_mime;    /* Set if this is not a MIME message. */
73   int top_seen;
74   int wk_seen;
75 };
76
77
78 /* Base64 conversion tables. */
79 static unsigned char bintoasc[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
80                                   "abcdefghijklmnopqrstuvwxyz"
81                                   "0123456789+/";
82 static unsigned char asctobin[256]; /* runtime initialized */
83
84
85
86 /* Print diagnostic message and exit with failure. */
87 static void
88 die (const char *format, ...)
89 {
90   va_list arg_ptr;
91
92   fflush (stdout);
93   fprintf (stderr, "%s: ", PGM);
94
95   va_start (arg_ptr, format);
96   vfprintf (stderr, format, arg_ptr);
97   va_end (arg_ptr);
98   putc ('\n', stderr);
99
100   exit (1);
101 }
102
103
104 /* Print diagnostic message. */
105 static void
106 err (const char *format, ...)
107 {
108   va_list arg_ptr;
109
110   fflush (stdout);
111   fprintf (stderr, "%s: ", PGM);
112
113   va_start (arg_ptr, format);
114   vfprintf (stderr, format, arg_ptr);
115   va_end (arg_ptr);
116   putc ('\n', stderr);
117 }
118
119 /* static void * */
120 /* xmalloc (size_t n) */
121 /* { */
122 /*   void *p = malloc (n); */
123 /*   if (!p) */
124 /*     die ("out of core: %s", strerror (errno)); */
125 /*   return p; */
126 /* } */
127
128 /* static void * */
129 /* xcalloc (size_t n, size_t m) */
130 /* { */
131 /*   void *p = calloc (n, m); */
132 /*   if (!p) */
133 /*     die ("out of core: %s", strerror (errno)); */
134 /*   return p; */
135 /* } */
136
137 /* static void * */
138 /* xrealloc (void *old, size_t n) */
139 /* { */
140 /*   void *p = realloc (old, n); */
141 /*   if (!p) */
142 /*     die ("out of core: %s", strerror (errno)); */
143 /*   return p; */
144 /* } */
145
146 /* static char * */
147 /* xstrdup (const char *string) */
148 /* { */
149 /*   void *p = malloc (strlen (string)+1); */
150 /*   if (!p) */
151 /*     die ("out of core: %s", strerror (errno)); */
152 /*   strcpy (p, string); */
153 /*   return p; */
154 /* } */
155
156 /* static char * */
157 /* stpcpy (char *a,const char *b) */
158 /* { */
159 /*   while (*b) */
160 /*     *a++ = *b++; */
161 /*   *a = 0; */
162   
163 /*   return (char*)a; */
164 /* } */
165
166
167 /* Simple but sufficient and locale independend lowercase function. */
168 static void
169 lowercase_string (unsigned char *string)
170 {
171   for (; *string; string++)
172     if (*string >= 'A' && *string <= 'Z')
173       *string = *string - 'A' + 'a';
174 }
175
176
177 /* Inplace Base64 decoder. Returns the length of the valid databytes
178    in BUFFER. Note, that BUFFER should initially be a C-string. */
179 static size_t
180 decode_base64 (unsigned char *buffer)
181 {
182   int state, c, value=0;
183   unsigned char *d, *s;
184   
185   for (state=0, d=s=buffer; *s; s++)
186     {
187       if ((c = asctobin[*s]) == 255 )
188         continue;  /* Simply skip invalid base64 characters. */
189       
190       switch (state)
191         {
192         case 0: 
193           value = c << 2;
194           break;
195         case 1:
196           value |= (c>>4)&3;
197           *d++ = value;
198           value = (c<<4)&0xf0;
199           break; 
200         case 2:
201           value |= (c>>2)&15;
202           *d++ = value;
203           value = (c<<6)&0xc0;
204           break; 
205         case 3:
206           value |= c&0x3f;
207           *d++ = value;
208           break; 
209         }
210       state++;
211       state = state & 3;
212     }
213
214   return d - buffer;
215 }
216
217
218 /* Given a Buffer starting with the magic MZ, check whethere thsi is a
219    Windows PE executable. */
220 static int
221 is_windows_pe (const unsigned char *buffer, size_t buflen)
222 {
223   unsigned long off;
224
225   if ( buflen < 0x3c + 4 )
226     return 0;
227   /* The offset is little endian. */
228   off = ((buffer[0x3c]) | (buffer[0x3d] << 8)
229          | (buffer[0x3e] << 16) | (buffer[0x3f] << 24));
230   return (off < buflen - 4 && !memcmp (buffer+off, "PE\0", 4));
231 }
232
233
234 /* See whether we can identify the binary data in BUFFER. */
235 static void
236 identify_binary (const unsigned char *buffer, size_t buflen)
237 {
238   if (buflen > 5 && !memcmp (buffer, "PK\x03\x04", 4))
239     {
240       if (!quiet)
241         fputs ("ZIP\n", stdout);
242       if (opt_match_zip)
243         exit (0);
244     }
245   else if (buflen > 132 && buffer[0] == 'M' && buffer[1] == 'Z' 
246            && is_windows_pe (buffer, buflen))
247     {
248       if (!quiet)
249         fputs ("EXE (Windows PE)\n", stdout);
250       if (opt_match_exe)
251         exit (0);
252     }
253 }
254
255
256
257 /* Print the event received by the parser for debugging as comment
258    line. */
259 static void
260 show_event (rfc822parse_event_t event)
261 {
262   const char *s;
263
264   switch (event)
265     {
266     case RFC822PARSE_OPEN: s= "Open"; break;
267     case RFC822PARSE_CLOSE: s= "Close"; break;
268     case RFC822PARSE_CANCEL: s= "Cancel"; break;
269     case RFC822PARSE_T2BODY: s= "T2Body"; break;
270     case RFC822PARSE_FINISH: s= "Finish"; break;
271     case RFC822PARSE_RCVD_SEEN: s= "Rcvd_Seen"; break;
272     case RFC822PARSE_LEVEL_DOWN: s= "Level_Down"; break;
273     case RFC822PARSE_LEVEL_UP: s= "Level_Up"; break;
274     case RFC822PARSE_BOUNDARY: s= "Boundary"; break;
275     case RFC822PARSE_LAST_BOUNDARY: s= "Last_Boundary"; break;
276     case RFC822PARSE_BEGIN_HEADER: s= "Begin_Header"; break;
277     case RFC822PARSE_PREAMBLE: s= "Preamble"; break;
278     case RFC822PARSE_EPILOGUE: s= "Epilogue"; break;
279     default: s= "[unknown event]"; break;
280     }
281   printf ("# *** got RFC822 event %s\n", s);
282 }
283
284 /* This function is called by the parser to communicate events.  This
285    callback communicates with the main program using a structure
286    passed in OPAQUE. Should retrun 0 or set errno and return -1. */
287 static int
288 message_cb (void *opaque, rfc822parse_event_t event, rfc822parse_t msg)
289 {
290   struct parse_info_s *info = opaque;
291
292   if (debug)
293     show_event (event);
294   if (event == RFC822PARSE_T2BODY)
295     {
296       rfc822parse_field_t ctx;
297       size_t off;
298       char *p;
299
300       info->mime_type = MT_NONE;
301       info->transfer_encoding = TE_NONE;
302       info->test_base64 = 0;
303       info->got_probe = 0;
304       info->no_mime = 0;
305       ctx = rfc822parse_parse_field (msg, "Content-Type", -1);
306       if (ctx)
307         {
308           const char *s1, *s2;
309           s1 = rfc822parse_query_media_type (ctx, &s2);
310           if (!s1)
311             ;
312           else if (!strcmp (s1, "application") 
313                    && !strcmp (s2, "octet-stream"))
314             info->mime_type = MT_OCTET_STREAM;
315           else if (!strcmp (s1, "text") 
316                    && !strcmp (s2, "html"))
317             info->mime_type = MT_TEXT_HTML;
318           else if (!strcmp (s1, "audio"))
319             info->mime_type = MT_AUDIO;
320           else if (!strcmp (s1, "image"))
321             info->mime_type = MT_IMAGE;
322
323           if (verbose)
324             {
325               printf ("# Content-Type: %s/%s", s1?s1:"", s2?s2:"");
326               s1 = rfc822parse_query_parameter (ctx, "charset", 0);
327               if (s1)
328                 printf ("; charset=%s", s1);
329               putchar ('\n');
330             }
331
332           rfc822parse_release_field (ctx);
333         }
334       else
335         {
336           p = rfc822parse_get_field (msg, "MIME-Version", -1, NULL);
337           if (p)
338             free (p);
339           else
340             info->no_mime = 1;
341         }
342
343       if (verbose)
344         {
345           const char *s1;
346
347           p = rfc822parse_get_field (msg, "Content-Disposition", -1, NULL);
348           if (p)
349             {
350               printf ("# %s\n", p);
351               free (p);
352             }
353
354           ctx = rfc822parse_parse_field (msg, "Content-Disposition", -1);
355           if (ctx)
356             {
357               s1 = rfc822parse_query_parameter (ctx, "filename", 0);
358               if (s1)
359                 printf ("# Content-Disposition has filename=`%s'\n", s1);
360               rfc822parse_release_field (ctx);
361             }
362         }
363
364       p = rfc822parse_get_field (msg, "Content-Transfer-Encoding", -1, &off);
365       if (p)
366         {
367           lowercase_string (p+off);
368           if (!strcmp (p+off, "base64"))
369             info->transfer_encoding = TE_BASE64;
370           free (p);
371         }
372
373       if (!info->top_seen)
374         {
375           info->top_seen = 1;
376           p = rfc822parse_get_field (msg, "To", -1, NULL);
377           if (p)
378             {
379               if ( strstr (p, "Werner Koch") )
380                 {
381                   if (verbose)
382                     fputs ("# Found known name in To\n", stdout);
383                   info->wk_seen = 1;
384                 }
385               free (p);
386             }
387           if (!info->wk_seen)
388             {
389               p = rfc822parse_get_field (msg, "Cc", -1, NULL);
390               if (p)
391                 {
392                   if ( strstr (p, "Werner Koch") )
393                     {
394                       if (verbose)
395                         fputs ("# Found known name in Cc\n", stdout);
396                       info->wk_seen = 1;
397                     }
398                   free (p);
399                 }
400             }
401         }
402
403       if ((info->mime_type == MT_OCTET_STREAM
404            || info->mime_type == MT_AUDIO
405            || info->mime_type == MT_IMAGE)
406           && info->transfer_encoding == TE_BASE64)
407         info->test_base64 = 1;
408       else if (info->mime_type == MT_TEXT_HTML)
409         {
410           if (!quiet)
411             fputs ("HTML\n", stdout);
412           if (opt_match_html && !info->wk_seen)
413             exit (0);
414         }
415
416     }
417   else if (event == RFC822PARSE_PREAMBLE)
418     ;
419   else if (event == RFC822PARSE_BOUNDARY || event == RFC822PARSE_LAST_BOUNDARY)
420     {
421       if (info->test_base64)
422         info->got_probe = 1;
423       info->test_base64 = 0;
424     }
425   else if (event == RFC822PARSE_BEGIN_HEADER)
426     {
427     }
428
429   return 0;
430 }
431
432
433 /* Read a message from FP and process it according to the global
434    options. */
435 static void
436 parse_message (FILE *fp)
437 {
438   char line[2000];
439   unsigned char buffer[1000]; 
440   size_t buflen = 0;   
441   size_t length;
442   rfc822parse_t msg;
443   unsigned int lineno = 0;
444   int no_cr_reported = 0;
445   struct parse_info_s info;
446   int body_lines = 0;
447   int skip_leading_empty_lines = 0;
448
449  restart:
450   memset (&info, 0, sizeof info);
451
452   msg = rfc822parse_open (message_cb, &info);
453   if (!msg)
454     die ("can't open parser: %s", strerror (errno));
455
456   /* Fixme: We should not use fgets because it can't cope with
457      embedded nul characters. */
458   while (fgets (line, sizeof (line), fp))
459     {
460       lineno++;
461       if (lineno == 1 && !strncmp (line, "From ", 5))
462         continue;  /* We better ignore a leading From line. */
463
464       length = strlen (line);
465       if (length && line[length - 1] == '\n')
466         line[--length] = 0;
467       else if (verbose)
468         err ("line number %u too long or last line not terminated", lineno);
469       if (length && line[length - 1] == '\r')
470         line[--length] = 0;
471       else if (verbose && !no_cr_reported)
472         {
473           err ("non canonical ended line detected (line %u)", lineno);
474           no_cr_reported = 1;
475         }
476
477       if (skip_leading_empty_lines)
478         {
479           if (skip_leading_empty_lines == 1)
480             {
481               /* Sometimes additional information follows the
482                  indication line indicated by 6 dashes.  Skip them
483                  before detecting empty lines. */
484               if (length && !strncmp (line , "------ ", 7))
485                 continue;
486               skip_leading_empty_lines++;
487             }
488           if (!length)
489             continue;
490
491           skip_leading_empty_lines = 0;
492         }
493
494       if (rfc822parse_insert (msg, line, length))
495         die ("parser failed: %s", strerror (errno));
496
497       if (info.no_mime && body_lines < 50)
498         {
499           body_lines++;
500           if (!strncmp (line, "------ This is a copy of the message, "
501                         "including all the headers.", 64))
502             {
503               /* This may be followed by empty lines, thus we set a
504                  flag to skip them. */
505               skip_leading_empty_lines = 1;
506               body_lines = 500; /* Avoid going here a second time. */
507               rfc822parse_close (msg);
508               goto restart;
509             }
510         }
511
512       
513       if (info.got_probe && buflen)
514         {
515           info.got_probe = 0;
516           buffer[buflen] = 0;
517           buflen = decode_base64 (buffer);
518           if (debug)
519             {
520               int i;
521               
522               printf ("# %4d bytes base64:", (int)buflen);
523               for (i=0; i < buflen; i++)
524                 {
525                   if (i && !(i % 16))
526                     printf ("\n#            0x%04X:", i);
527                   printf (" %02X", buffer[i]);
528                 }
529               putchar ('\n'); 
530             }
531           identify_binary (buffer, buflen);
532         }
533
534       if (info.test_base64)
535         {
536           if (info.test_base64 == 1)
537             { 
538               /* This is the empty marker line. */
539               buflen = 0;
540             }
541           else
542             {
543               if (length > sizeof buffer - 1 - buflen)
544                 {
545                   length = sizeof buffer - 1 - buflen;
546                   info.got_probe = 1;  /* We got enough. */
547                 }
548               if (length)
549                 {
550                   memcpy (buffer+buflen, line, length);
551                   buflen += length;
552                 }
553             }
554           if (info.got_probe)
555             info.test_base64 = 0;
556           else
557             info.test_base64++;
558         }
559     }
560
561   rfc822parse_close (msg);
562 }
563
564
565
566 int 
567 main (int argc, char **argv)
568 {
569   int last_argc = -1;
570   int any_match = 0;
571  
572   if (argc)
573     {
574       argc--; argv++;
575     }
576   while (argc && last_argc != argc )
577     {
578       last_argc = argc;
579       if (!strcmp (*argv, "--"))
580         {
581           argc--; argv++;
582           break;
583         }
584       else if (!strcmp (*argv, "--help"))
585         {
586           puts (
587                 "Usage: " PGM " [OPTION] [FILE]\n"
588                 "Scrutinize a mail message.\n\n"
589                 "  --match-zip  return true if a ZIP body was found\n"
590                 "  --match-exe  return true if an EXE body was found\n"
591                 "  --match-html return true if a HTML body was found\n"
592                 "  --verbose    enable extra informational output\n"
593                 "  --debug      enable additional debug output\n"
594                 "  --help       display this help and exit\n\n"
595                 "With no FILE, or when FILE is -, read standard input.\n\n"
596                 "Report bugs to <bugs@g10code.com>.");
597           exit (0);
598         }
599       else if (!strcmp (*argv, "--version"))
600         {
601           puts (PGM " " VERSION "\n"
602                "Copyright (C) 2004 g10 Code GmbH\n"
603                "This program comes with ABSOLUTELY NO WARRANTY.\n"
604                "This is free software, and you are welcome to redistribute it\n"
605                 "under certain conditions. See the file COPYING for details.");
606           exit (0);
607         }
608       else if (!strcmp (*argv, "--verbose"))
609         {
610           verbose = 1;
611           argc--; argv++;
612         }
613       else if (!strcmp (*argv, "--quiet"))
614         {
615           quiet = 1;
616           argc--; argv++;
617         }
618       else if (!strcmp (*argv, "--debug"))
619         {
620           verbose = debug = 1;
621           argc--; argv++;
622         }
623       else if (!strcmp (*argv, "--match-zip"))
624         {
625           opt_match_zip = 1;
626           any_match = 1;
627           argc--; argv++;
628         }
629       else if (!strcmp (*argv, "--match-exe"))
630         {
631           opt_match_exe = 1;
632           any_match = 1;
633           argc--; argv++;
634         }
635       else if (!strcmp (*argv, "--match-html"))
636         {
637           opt_match_html = 1;
638           any_match = 1;
639           argc--; argv++;
640         }
641     }          
642  
643   if (argc > 1)
644     die ("usage: " PGM " [OPTION] [FILE] (try --help for more information)\n");
645
646   signal (SIGPIPE, SIG_IGN);
647
648   /* Build the helptable for radix64 to bin conversion. */
649   {
650     int i;
651     unsigned char *s;
652
653     for (i=0; i < 256; i++ )
654       asctobin[i] = 255; /* Used to detect invalid characters. */
655     for (s=bintoasc, i=0; *s; s++, i++)
656       asctobin[*s] = i;
657   }
658
659   /* Start processing. */
660   if (argc && strcmp (*argv, "-"))
661     {
662       FILE *fp = fopen (*argv, "rb");
663       if (!fp)
664         die ("can't open `%s': %s", *argv, strerror (errno));
665       parse_message (fp);
666       fclose (fp);
667     }
668   else
669     parse_message (stdin);
670
671   /* If any match option was used and we reach this here we return
672      false.  True is returned immediately on a match. */
673   return any_match? 1:0;
674 }
675
676
677 /*
678 Local Variables:
679 compile-command: "gcc -Wall -Wno-pointer-sign -g -o scrutmime rfc822parse.c scrutmime.c"
680 End:
681 */