Merge from EXISCAN branch.
[exim.git] / src / src / mime.c
1 /* $Cambridge: exim/src/src/mime.c,v 1.2 2004/12/16 15:11:47 tom Exp $ */
2
3 /*************************************************
4 *     Exim - an Internet mail transport agent    *
5 *************************************************/
6
7 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004 */
8 /* License: GPL */
9
10 #include "exim.h"
11 #ifdef WITH_CONTENT_SCAN
12 #include "mime.h"
13 #include <sys/stat.h>
14
15 FILE *mime_stream = NULL;
16 uschar *mime_current_boundary = NULL;
17
18 /*************************************************
19 * set MIME anomaly level + text                  *
20 *************************************************/
21
22 /* Small wrapper to set the two expandables which
23    give info on detected "problems" in MIME
24    encodings. Those are defined in mime.h. */
25
26 void mime_set_anomaly(int level, char *text) {
27   mime_anomaly_level = level;
28   mime_anomaly_text = text;
29 };
30
31
32 /*************************************************
33 * decode quoted-printable chars                  *
34 *************************************************/
35
36 /* gets called when we hit a =
37    returns: new pointer position
38    result code in c:
39           -2 - decode error
40           -1 - soft line break, no char
41            0-255 - char to write
42 */
43
44 unsigned int mime_qp_hstr_i(uschar *cptr) {
45   unsigned int i, j = 0;
46   while (cptr && *cptr && isxdigit(*cptr)) {
47     i = *cptr++ - '0';
48     if (9 < i) i -= 7;
49     j <<= 4;
50     j |= (i & 0x0f);
51   }
52   return(j);
53 }
54
55 uschar *mime_decode_qp_char(uschar *qp_p,int *c) {
56   uschar hex[] = {0,0,0};
57   int nan = 0;
58   uschar *initial_pos = qp_p;
59   
60   /* advance one char */
61   qp_p++;
62   
63   REPEAT_FIRST:
64   if ( (*qp_p == '\t') || (*qp_p == ' ') || (*qp_p == '\r') )  {
65     /* tab or whitespace may follow
66        just ignore it, but remember
67        that this is not a valid hex
68        encoding any more */
69     nan = 1;
70     qp_p++;
71     goto REPEAT_FIRST;
72   }
73   else if ( (('0' <= *qp_p) && (*qp_p <= '9')) || (('A' <= *qp_p) && (*qp_p <= 'F'))  || (('a' <= *qp_p) && (*qp_p <= 'f')) ) {
74     /* this is a valid hex char, if nan is unset */
75     if (nan) {
76       /* this is illegal */
77       *c = -2;
78       return initial_pos;
79     }
80     else {
81       hex[0] = *qp_p;
82       qp_p++;
83     };
84   }
85   else if (*qp_p == '\n') {    
86     /* hit soft line break already, continue */
87     *c = -1;
88     return qp_p;
89   }
90   else {
91     /* illegal char here */
92     *c = -2;
93     return initial_pos;
94   };
95   
96   if ( (('0' <= *qp_p) && (*qp_p <= '9')) || (('A' <= *qp_p) && (*qp_p <= 'F')) || (('a' <= *qp_p) && (*qp_p <= 'f')) ) {
97     if (hex[0] > 0) {
98       hex[1] = *qp_p;
99       /* do hex conversion */
100       *c = mime_qp_hstr_i(hex);
101       qp_p++;
102       return qp_p;
103     }
104     else {
105       /* huh ? */
106       *c = -2;
107       return initial_pos;  
108     };
109   }
110   else {
111     /* illegal char */
112     *c = -2;
113     return initial_pos;  
114   };
115 }
116
117
118 uschar *mime_parse_line(uschar *buffer, uschar *encoding, int *num_decoded) {
119   uschar *data = NULL;
120
121   data = (uschar *)malloc(Ustrlen(buffer)+2);
122
123   if (encoding == NULL) {
124     /* no encoding type at all */
125     NO_DECODING:
126     memcpy(data, buffer, Ustrlen(buffer));
127     data[(Ustrlen(buffer))] = 0;
128     *num_decoded = Ustrlen(data);
129     return data;
130   }
131   else if (Ustrcmp(encoding,"base64") == 0) {
132     uschar *p = buffer;
133     int offset = 0;
134     
135     /* ----- BASE64 ---------------------------------------------------- */
136     /* NULL out '\r' and '\n' chars */
137     while (Ustrrchr(p,'\r') != NULL) {
138       *(Ustrrchr(p,'\r')) = '\0';
139     };
140     while (Ustrrchr(p,'\n') != NULL) {
141       *(Ustrrchr(p,'\n')) = '\0';
142     };
143
144     while (*(p+offset) != '\0') {
145       /* hit illegal char ? */
146       if (mime_b64[*(p+offset)] == 128) {
147         mime_set_anomaly(MIME_ANOMALY_BROKEN_BASE64);
148         offset++;
149       }
150       else {
151         *p = mime_b64[*(p+offset)];
152         p++;
153       };
154     };
155     *p = 255;
156    
157     /* line is translated, start bit shifting */
158     p = buffer;
159     *num_decoded = 0;  
160     while(*p != 255) {
161       uschar tmp_c;
162       
163       /* byte 0 ---------------------- */
164       if (*(p+1) == 255) {
165         mime_set_anomaly(MIME_ANOMALY_BROKEN_BASE64);
166         break;
167       }
168       data[(*num_decoded)] = *p;
169       data[(*num_decoded)] <<= 2;
170       tmp_c = *(p+1);
171       tmp_c >>= 4;
172       data[(*num_decoded)] |= tmp_c;
173       (*num_decoded)++;
174       p++;
175       /* byte 1 ---------------------- */
176       if (*(p+1) == 255) {
177         mime_set_anomaly(MIME_ANOMALY_BROKEN_BASE64);
178         break;
179       }
180       data[(*num_decoded)] = *p;
181       data[(*num_decoded)] <<= 4;
182       tmp_c = *(p+1);
183       tmp_c >>= 2;
184       data[(*num_decoded)] |= tmp_c;
185       (*num_decoded)++;
186       p++;
187       /* byte 2 ---------------------- */
188       if (*(p+1) == 255) {
189         mime_set_anomaly(MIME_ANOMALY_BROKEN_BASE64);
190         break;
191       }
192       data[(*num_decoded)] = *p;
193       data[(*num_decoded)] <<= 6;
194       data[(*num_decoded)] |= *(p+1); 
195       (*num_decoded)++;
196       p+=2;
197       
198     };
199     return data;
200     /* ----------------------------------------------------------------- */
201   }
202   else if (Ustrcmp(encoding,"quoted-printable") == 0) {
203     uschar *p = buffer;
204
205     /* ----- QP -------------------------------------------------------- */
206     *num_decoded = 0;
207     while (*p != 0) {
208       if (*p == '=') {
209         int decode_qp_result;
210         
211         p = mime_decode_qp_char(p,&decode_qp_result);
212               
213         if (decode_qp_result == -2) {
214           /* Error from decoder. p is unchanged. */
215           mime_set_anomaly(MIME_ANOMALY_BROKEN_QP);
216           data[(*num_decoded)] = '=';
217           (*num_decoded)++;
218           p++;
219         }
220         else if (decode_qp_result == -1) {
221           break;
222         }
223         else if (decode_qp_result >= 0) {
224           data[(*num_decoded)] = decode_qp_result;
225           (*num_decoded)++;
226         };
227       }
228       else {
229         data[(*num_decoded)] = *p;
230         (*num_decoded)++;
231         p++;
232       };
233     };
234     return data;
235     /* ----------------------------------------------------------------- */
236   }
237   /* unknown encoding type, just dump as-is */
238   else goto NO_DECODING;
239 }
240
241
242 FILE *mime_get_decode_file(uschar *pname, uschar *fname) {
243   FILE *f;
244   uschar *filename;
245   
246   filename = (uschar *)malloc(2048);
247   
248   if ((pname != NULL) && (fname != NULL)) {
249     snprintf(CS filename, 2048, "%s/%s", pname, fname);
250     f = fopen(CS filename,"w+");
251   }
252   else if (pname == NULL) {
253     f = fopen(CS fname,"w+");
254   }
255   else if (fname == NULL) {
256     int file_nr = 0;
257     int result = 0;
258
259     /* must find first free sequential filename */
260     do {
261       struct stat mystat;
262       snprintf(CS filename,2048,"%s/%s-%05u", pname, message_id, file_nr);
263       file_nr++;
264       /* security break */
265       if (file_nr >= 1024)
266         break;
267       result = stat(CS filename,&mystat);
268     }
269     while(result != -1);
270     f = fopen(CS filename,"w+");
271   };
272   
273   /* set expansion variable */
274   mime_decoded_filename = filename;
275   
276   return f;
277 }
278
279
280 int mime_decode(uschar **listptr) {
281   int sep = 0;
282   uschar *list = *listptr;
283   uschar *option;
284   uschar option_buffer[1024];
285   uschar decode_path[1024];
286   FILE *decode_file = NULL;
287   uschar *buffer = NULL;
288   long f_pos = 0;
289   unsigned int size_counter = 0;
290
291   if (mime_stream == NULL)
292     return FAIL;
293   
294   f_pos = ftell(mime_stream);
295   
296   /* build default decode path (will exist since MBOX must be spooled up) */
297   snprintf(CS decode_path,1024,"%s/scan/%s",spool_directory,message_id);
298   
299   /* reserve a line buffer to work in */
300   buffer = (uschar *)malloc(MIME_MAX_LINE_LENGTH+1);
301   if (buffer == NULL) {
302     log_write(0, LOG_PANIC,
303                  "decode ACL condition: can't allocate %d bytes of memory.", MIME_MAX_LINE_LENGTH+1);
304     return DEFER;
305   };
306   
307   /* try to find 1st option */
308   if ((option = string_nextinlist(&list, &sep,
309                                   option_buffer,
310                                   sizeof(option_buffer))) != NULL) {
311     
312     /* parse 1st option */
313     if ( (Ustrcmp(option,"false") == 0) || (Ustrcmp(option,"0") == 0) ) {
314       /* explicitly no decoding */
315       return FAIL;
316     };
317     
318     if (Ustrcmp(option,"default") == 0) {
319       /* explicit default path + file names */
320       goto DEFAULT_PATH;
321     };
322     
323     if (option[0] == '/') {
324       struct stat statbuf;
325
326       memset(&statbuf,0,sizeof(statbuf));
327       
328       /* assume either path or path+file name */
329       if ( (stat(CS option, &statbuf) == 0) && S_ISDIR(statbuf.st_mode) )
330         /* is directory, use it as decode_path */
331         decode_file = mime_get_decode_file(option, NULL);
332       else
333         /* does not exist or is a file, use as full file name */
334         decode_file = mime_get_decode_file(NULL, option);
335     }
336     else
337       /* assume file name only, use default path */
338       decode_file = mime_get_decode_file(decode_path, option);
339   }
340   else
341     /* no option? patch default path */
342     DEFAULT_PATH: decode_file = mime_get_decode_file(decode_path, NULL);
343   
344   if (decode_file == NULL)
345     return DEFER;
346   
347   /* read data linewise and dump it to the file,
348      while looking for the current boundary */
349   while(fgets(CS buffer, MIME_MAX_LINE_LENGTH, mime_stream) != NULL) {
350     uschar *decoded_line = NULL;
351     int decoded_line_length = 0;
352     
353     if (mime_current_boundary != NULL) {
354       /* boundary line must start with 2 dashes */
355       if (Ustrncmp(buffer,"--",2) == 0) {
356         if (Ustrncmp((buffer+2),mime_current_boundary,Ustrlen(mime_current_boundary)) == 0)
357           break;
358       };
359     };
360   
361     decoded_line = mime_parse_line(buffer, mime_content_transfer_encoding, &decoded_line_length);
362     /* write line to decode file */
363     if (fwrite(decoded_line, 1, decoded_line_length, decode_file) < decoded_line_length) {
364       /* error/short write */
365       clearerr(mime_stream);
366       fseek(mime_stream,f_pos,SEEK_SET);
367       return DEFER;
368     };
369     size_counter += decoded_line_length;
370     
371     if (size_counter > 1023) { 
372       if ((mime_content_size + (size_counter / 1024)) < 65535)
373         mime_content_size += (size_counter / 1024);
374       else 
375         mime_content_size = 65535;
376       size_counter = (size_counter % 1024);
377     };
378     
379     free(decoded_line);
380   }
381   
382   fclose(decode_file);
383   
384   clearerr(mime_stream);
385   fseek(mime_stream,f_pos,SEEK_SET);
386   
387   /* round up remaining size bytes to one k */
388   if (size_counter) {
389     mime_content_size++;
390   };
391   
392   return OK;
393 }
394
395 int mime_get_header(FILE *f, uschar *header) {
396   int c = EOF;
397   int done = 0;
398   int header_value_mode = 0;
399   int header_open_brackets = 0;
400   int num_copied = 0;
401   
402   while(!done) {
403     
404     c = fgetc(f);
405     if (c == EOF) break;
406    
407     /* always skip CRs */
408     if (c == '\r') continue;
409     
410     if (c == '\n') {
411       if (num_copied > 0) {
412         /* look if next char is '\t' or ' ' */
413         c = fgetc(f);
414         if (c == EOF) break;
415         if ( (c == '\t') || (c == ' ') ) continue;
416         ungetc(c,f);
417       };
418       /* end of the header, terminate with ';' */
419       c = ';';
420       done = 1;
421     };
422   
423     /* skip control characters */
424     if (c < 32) continue;
425
426     if (header_value_mode) {
427       /* --------- value mode ----------- */
428       /* skip leading whitespace */
429       if ( ((c == '\t') || (c == ' ')) && (header_value_mode == 1) )
430         continue;
431       
432       /* we have hit a non-whitespace char, start copying value data */
433       header_value_mode = 2;
434       
435       /* skip quotes */
436       if (c == '"') continue;
437       
438       /* leave value mode on ';' */
439       if (c == ';') {
440         header_value_mode = 0;
441       };
442       /* -------------------------------- */
443     }
444     else {
445       /* -------- non-value mode -------- */
446       /* skip whitespace + tabs */
447       if ( (c == ' ') || (c == '\t') )
448         continue;
449       if (c == '\\') {
450         /* quote next char. can be used
451         to escape brackets. */
452         c = fgetc(f);
453         if (c == EOF) break;
454       }
455       else if (c == '(') {
456         header_open_brackets++;
457         continue;
458       }
459       else if ((c == ')') && header_open_brackets) {
460         header_open_brackets--;
461         continue;
462       }
463       else if ( (c == '=') && !header_open_brackets ) {
464         /* enter value mode */
465         header_value_mode = 1;
466       };
467       
468       /* skip chars while we are in a comment */
469       if (header_open_brackets > 0)
470         continue;
471       /* -------------------------------- */
472     };
473     
474     /* copy the char to the buffer */
475     header[num_copied] = (uschar)c;
476     /* raise counter */
477     num_copied++;
478     
479     /* break if header buffer is full */
480     if (num_copied > MIME_MAX_HEADER_SIZE-1) {
481       done = 1;
482     };
483   };
484
485   if (header[num_copied-1] != ';') {
486     header[num_copied-1] = ';';
487   };
488
489   /* 0-terminate */
490   header[num_copied] = '\0';
491   
492   /* return 0 for EOF or empty line */
493   if ((c == EOF) || (num_copied == 1))
494     return 0;
495   else
496     return 1;
497 }
498
499
500 int mime_acl_check(FILE *f, struct mime_boundary_context *context, uschar 
501                    **user_msgptr, uschar **log_msgptr) {
502   int rc = OK;
503   uschar *header = NULL;
504   struct mime_boundary_context nested_context;
505
506   /* reserve a line buffer to work in */
507   header = (uschar *)malloc(MIME_MAX_HEADER_SIZE+1);
508   if (header == NULL) {
509     log_write(0, LOG_PANIC,
510                  "acl_smtp_mime: can't allocate %d bytes of memory.", MIME_MAX_HEADER_SIZE+1);
511     return DEFER;
512   };
513
514   /* Not actually used at the moment, but will be vital to fixing
515    * some RFC 2046 nonconformance later... */
516   nested_context.parent = context;
517
518   /* loop through parts */
519   while(1) {
520   
521     /* reset all per-part mime variables */
522     mime_anomaly_level     = NULL;
523     mime_anomaly_text      = NULL;
524     mime_boundary          = NULL;
525     mime_charset           = NULL;
526     mime_decoded_filename  = NULL;
527     mime_filename          = NULL;
528     mime_content_description = NULL;
529     mime_content_disposition = NULL;
530     mime_content_id        = NULL;
531     mime_content_transfer_encoding = NULL;
532     mime_content_type      = NULL;
533     mime_is_multipart      = 0;
534     mime_content_size      = 0;
535   
536     /*
537     If boundary is null, we assume that *f is positioned on the start of headers (for example,
538     at the very beginning of a message.
539     If a boundary is given, we must first advance to it to reach the start of the next header
540     block.
541     */
542     
543     /* NOTE -- there's an error here -- RFC2046 specifically says to
544      * check for outer boundaries.  This code doesn't do that, and
545      * I haven't fixed this.
546      *
547      * (I have moved partway towards adding support, however, by adding 
548      * a "parent" field to my new boundary-context structure.)
549      */
550     if (context != NULL) {
551       while(fgets(CS header, MIME_MAX_HEADER_SIZE, f) != NULL) {
552         /* boundary line must start with 2 dashes */
553         if (Ustrncmp(header,"--",2) == 0) {
554           if (Ustrncmp((header+2),context->boundary,Ustrlen(context->boundary)) == 0) {
555             /* found boundary */
556             if (Ustrncmp((header+2+Ustrlen(context->boundary)),"--",2) == 0) {
557               /* END boundary found */
558               debug_printf("End boundary found %s\n", context->boundary);
559               return rc;
560             }
561             else {
562               debug_printf("Next part with boundary %s\n", context->boundary);
563             };
564             /* can't use break here */
565             goto DECODE_HEADERS;
566           }
567         };
568       }
569       /* Hit EOF or read error. Ugh. */
570       debug_printf("Hit EOF ...\n");
571       return rc;
572     };
573   
574     DECODE_HEADERS:
575     /* parse headers, set up expansion variables */
576     while(mime_get_header(f,header)) {
577       int i;
578       /* loop through header list */
579       for (i = 0; i < mime_header_list_size; i++) {
580         uschar *header_value = NULL;
581         int header_value_len = 0;
582         
583         /* found an interesting header? */
584         if (strncmpic(mime_header_list[i].name,header,mime_header_list[i].namelen) == 0) {
585           uschar *p = header + mime_header_list[i].namelen;
586           /* yes, grab the value (normalize to lower case)
587              and copy to its corresponding expansion variable */
588           while(*p != ';') {
589             *p = tolower(*p);
590             p++;
591           };
592           header_value_len = (p - (header + mime_header_list[i].namelen));
593           header_value = (uschar *)malloc(header_value_len+1);
594           memset(header_value,0,header_value_len+1);
595           p = header + mime_header_list[i].namelen;
596           Ustrncpy(header_value, p, header_value_len);
597           debug_printf("Found %s MIME header, value is '%s'\n", mime_header_list[i].name, header_value);
598           *((uschar **)(mime_header_list[i].value)) = header_value;
599           
600           /* make p point to the next character after the closing ';' */
601           p += (header_value_len+1);
602           
603           /* grab all param=value tags on the remaining line, check if they are interesting */
604           NEXT_PARAM_SEARCH: while (*p != 0) {
605             int j;
606             for (j = 0; j < mime_parameter_list_size; j++) {
607               uschar *param_value = NULL;
608               int param_value_len = 0;
609               
610               /* found an interesting parameter? */
611               if (strncmpic(mime_parameter_list[j].name,p,mime_parameter_list[j].namelen) == 0) {
612                 uschar *q = p + mime_parameter_list[j].namelen;
613                 /* yes, grab the value and copy to its corresponding expansion variable */
614                 while(*q != ';') q++;
615                 param_value_len = (q - (p + mime_parameter_list[j].namelen));
616                 param_value = (uschar *)malloc(param_value_len+1);
617                 memset(param_value,0,param_value_len+1);
618                 q = p + mime_parameter_list[j].namelen;
619                 Ustrncpy(param_value, q, param_value_len);
620                 param_value = rfc2047_decode(param_value, TRUE, NULL, 32, &param_value_len, &q);
621                 debug_printf("Found %s MIME parameter in %s header, value is '%s'\n", mime_parameter_list[j].name, mime_header_list[i].name, param_value);
622                 *((uschar **)(mime_parameter_list[j].value)) = param_value;
623                 p += (mime_parameter_list[j].namelen + param_value_len + 1);
624                 goto NEXT_PARAM_SEARCH;
625               };
626             }
627             /* There is something, but not one of our interesting parameters.
628                Advance to the next semicolon */
629             while(*p != ';') p++;
630             p++;
631           };
632         };
633       };
634     };
635     
636     /* set additional flag variables (easier access) */
637     if ( (mime_content_type != NULL) &&
638          (Ustrncmp(mime_content_type,"multipart",9) == 0) )
639       mime_is_multipart = 1;
640     
641     /* Make a copy of the boundary pointer.
642        Required since mime_boundary is global
643        and can be overwritten further down in recursion */
644     nested_context.boundary = mime_boundary;
645     
646     /* raise global counter */
647     mime_part_count++;
648     
649     /* copy current file handle to global variable */
650     mime_stream = f;
651     mime_current_boundary = context ? context->boundary : 0;
652
653     /* Note the context */
654     mime_is_coverletter = !(context && context->context == MBC_ATTACHMENT);
655     
656     /* call ACL handling function */
657     rc = acl_check(ACL_WHERE_MIME, NULL, acl_smtp_mime, user_msgptr, log_msgptr);
658     
659     mime_stream = NULL;
660     mime_current_boundary = NULL;
661     
662     if (rc != OK) break;
663     
664     /* If we have a multipart entity and a boundary, go recursive */
665     if ( (mime_content_type != NULL) &&
666          (nested_context.boundary != NULL) &&
667          (Ustrncmp(mime_content_type,"multipart",9) == 0) ) {
668       debug_printf("Entering multipart recursion, boundary '%s'\n", nested_context.boundary);
669
670       if (context && context->context == MBC_ATTACHMENT)
671         nested_context.context = MBC_ATTACHMENT;
672       else if (!Ustrcmp(mime_content_type,"multipart/alternative")
673             || !Ustrcmp(mime_content_type,"multipart/related"))
674         nested_context.context = MBC_COVERLETTER_ALL;
675       else
676         nested_context.context = MBC_COVERLETTER_ONESHOT;
677
678       rc = mime_acl_check(f, &nested_context, user_msgptr, log_msgptr);
679       if (rc != OK) break;
680     }
681     else if ( (mime_content_type != NULL) &&
682             (Ustrncmp(mime_content_type,"message/rfc822",14) == 0) ) {
683       uschar *rfc822name = NULL;
684       uschar filename[2048];
685       int file_nr = 0;
686       int result = 0;
687       
688       /* must find first free sequential filename */
689       do {
690         struct stat mystat;
691         snprintf(CS filename,2048,"%s/scan/%s/__rfc822_%05u", spool_directory, message_id, file_nr);
692         file_nr++;
693         /* security break */
694         if (file_nr >= 128)
695           goto NO_RFC822;
696         result = stat(CS filename,&mystat);
697       }
698       while(result != -1);
699       
700       rfc822name = filename;
701       
702       /* decode RFC822 attachment */
703       mime_decoded_filename = NULL;
704       mime_stream = f;
705       mime_current_boundary = context ? context->boundary : NULL;
706       mime_decode(&rfc822name);
707       mime_stream = NULL;
708       mime_current_boundary = NULL;
709       if (mime_decoded_filename == NULL) {
710         /* decoding failed */
711         log_write(0, LOG_MAIN,
712              "mime_regex acl condition warning - could not decode RFC822 MIME part to file.");
713         return DEFER;
714       };
715       mime_decoded_filename = NULL;
716     };
717     
718     NO_RFC822:
719     /* If the boundary of this instance is NULL, we are finished here */
720     if (context == NULL) break;
721
722     if (context->context == MBC_COVERLETTER_ONESHOT)
723       context->context = MBC_ATTACHMENT;
724   
725   };
726
727   return rc;
728 }
729
730 #endif