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