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