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