869327ecbc0a475f754753ea2f1129fc06915d58
[citadel.git] / citadel / mime_parser.c
1 /*
2  * $Id$
3  *
4  * This is the MIME parser for Citadel.
5  *
6  * Copyright (c) 1998-2006 by Art Cancro
7  * This code is distributed under the GNU General Public License v2.
8  *
9  */
10
11 #include <stdlib.h>
12 #include <unistd.h>
13 #include <stdio.h>
14 #include <signal.h>
15 #include <sys/types.h>
16 #include <ctype.h>
17 #include <string.h>
18 #include <sys/stat.h>
19 #include <errno.h>
20
21 #include "citadel.h"
22 #include "server.h"
23 #include "serv_extensions.h"
24 #include "sysdep_decls.h"
25 #include "tools.h"
26
27 #include "mime_parser.h"
28
29
30 void extract_key(char *target, char *source, char *key)
31 {
32         int a, b;
33
34         strcpy(target, source);
35         for (a = 0; a < strlen(target); ++a) {
36                 if ((!strncasecmp(&target[a], key, strlen(key)))
37                     && (target[a + strlen(key)] == '=')) {
38                         strcpy(target, &target[a + strlen(key) + 1]);
39                         if (target[0] == 34)
40                                 strcpy(target, &target[1]);
41                         for (b = 0; b < strlen(target); ++b)
42                                 if (target[b] == 34)
43                                         target[b] = 0;
44                         return;
45                 }
46         }
47         strcpy(target, "");
48 }
49
50
51 /*
52  * For non-multipart messages, we need to generate a quickie partnum of "1"
53  * to return to callback functions.  Some callbacks demand it.
54  */
55 char *fixed_partnum(char *supplied_partnum) {
56         if (supplied_partnum == NULL) return "1";
57         if (strlen(supplied_partnum)==0) return "1";
58         return supplied_partnum;
59 }
60
61
62
63 /*
64  * Convert "quoted-printable" to binary.  Returns number of bytes decoded.
65  * according to RFC2045 section 6.7
66  */
67 int CtdlDecodeQuotedPrintable(char *decoded, char *encoded, int sourcelen) {
68         unsigned int ch;
69         int decoded_length = 0;
70         int pos = 0;
71
72         while (pos < sourcelen)
73         {
74                 if (!strncmp(&encoded[pos], "=\r\n", 3))
75                 {
76                         pos += 3;
77                 }
78                 else if (!strncmp(&encoded[pos], "=\n", 2))
79                 {
80                         pos += 2;
81                 }
82                 else if (encoded[pos] == '=')
83                 {
84                         ch = 0;
85                         sscanf(&encoded[pos+1], "%02x", &ch);
86                         pos += 3;
87                         decoded[decoded_length++] = ch;
88                 }
89                 else
90                 {
91                         decoded[decoded_length++] = encoded[pos];
92                         pos += 1;
93                 }
94         }
95         decoded[decoded_length] = 0;
96         return(decoded_length);
97 }
98
99
100 /*
101  * Given a message or message-part body and a length, handle any necessary
102  * decoding and pass the request up the stack.
103  */
104 void mime_decode(char *partnum,
105                  char *part_start, size_t length,
106                  char *content_type, char *charset, char *encoding,
107                  char *disposition,
108                  char *name, char *filename,
109                  void (*CallBack)
110                   (char *cbname,
111                    char *cbfilename,
112                    char *cbpartnum,
113                    char *cbdisp,
114                    void *cbcontent,
115                    char *cbtype,
116                    char *cbcharset,
117                    size_t cblength,
118                    char *cbencoding,
119                    void *cbuserdata),
120                  void (*PreMultiPartCallBack)
121                   (char *cbname,
122                    char *cbfilename,
123                    char *cbpartnum,
124                    char *cbdisp,
125                    void *cbcontent,
126                    char *cbtype,
127                    char *cbcharset,
128                    size_t cblength,
129                    char *cbencoding,
130                    void *cbuserdata),
131                  void (*PostMultiPartCallBack)
132                   (char *cbname,
133                    char *cbfilename,
134                    char *cbpartnum,
135                    char *cbdisp,
136                    void *cbcontent,
137                    char *cbtype,
138                    char *cbcharset,
139                    size_t cblength,
140                    char *cbencoding,
141                    void *cbuserdata),
142                   void *userdata,
143                   int dont_decode
144 )
145 {
146
147         char *decoded;
148         size_t bytes_decoded = 0;
149
150         /* Some encodings aren't really encodings */
151         if (!strcasecmp(encoding, "7bit"))
152                 strcpy(encoding, "");
153         if (!strcasecmp(encoding, "8bit"))
154                 strcpy(encoding, "");
155         if (!strcasecmp(encoding, "binary"))
156                 strcpy(encoding, "");
157
158         /* If this part is not encoded, send as-is */
159         if ( (strlen(encoding) == 0) || (dont_decode)) {
160                 if (CallBack != NULL) {
161                         CallBack(name, filename, fixed_partnum(partnum),
162                                 disposition, part_start,
163                                 content_type, charset, length, encoding, userdata);
164                         }
165                 return;
166         }
167         
168         /* Fail silently if we hit an unknown encoding. */
169         if ((strcasecmp(encoding, "base64"))
170             && (strcasecmp(encoding, "quoted-printable"))) {
171                 return;
172         }
173
174         /*
175          * Allocate a buffer for the decoded data.  The output buffer is slightly
176          * larger than the input buffer; this assumes that the decoded data
177          * will never be significantly larger than the encoded data.  This is a
178          * safe assumption with base64, uuencode, and quoted-printable.
179          */
180         decoded = malloc(length + 32768);
181         if (decoded == NULL) {
182                 return;
183         }
184
185         if (!strcasecmp(encoding, "base64")) {
186                 bytes_decoded = CtdlDecodeBase64(decoded, part_start, length);
187         }
188         else if (!strcasecmp(encoding, "quoted-printable")) {
189                 bytes_decoded = CtdlDecodeQuotedPrintable(decoded, part_start, length);
190         }
191
192         if (bytes_decoded > 0) if (CallBack != NULL) {
193                 CallBack(name, filename, fixed_partnum(partnum),
194                         disposition, decoded,
195                         content_type, charset, bytes_decoded, "binary", userdata);
196         }
197
198         free(decoded);
199 }
200
201 /*
202  * Break out the components of a multipart message
203  * (This function expects to be fed HEADERS + CONTENT)
204  * Note: NULL can be supplied as content_end; in this case, the message is
205  * considered to have ended when the parser encounters a 0x00 byte.
206  */
207 void the_mime_parser(char *partnum,
208                      char *content_start, char *content_end,
209                      void (*CallBack)
210                       (char *cbname,
211                        char *cbfilename,
212                        char *cbpartnum,
213                        char *cbdisp,
214                        void *cbcontent,
215                        char *cbtype,
216                        char *cbcharset,
217                        size_t cblength,
218                        char *cbencoding,
219                        void *cbuserdata),
220                      void (*PreMultiPartCallBack)
221                       (char *cbname,
222                        char *cbfilename,
223                        char *cbpartnum,
224                        char *cbdisp,
225                        void *cbcontent,
226                        char *cbtype,
227                        char *cbcharset,
228                        size_t cblength,
229                        char *cbencoding,
230                        void *cbuserdata),
231                      void (*PostMultiPartCallBack)
232                       (char *cbname,
233                        char *cbfilename,
234                        char *cbpartnum,
235                        char *cbdisp,
236                        void *cbcontent,
237                        char *cbtype,
238                        char *cbcharset,
239                        size_t cblength,
240                        char *cbencoding,
241                        void *cbuserdata),
242                       void *userdata,
243                       int dont_decode
244 )
245 {
246
247         char *ptr;
248         char *srch = NULL;
249         char *part_start, *part_end = NULL;
250         char buf[SIZ];
251         char *header;
252         char *boundary;
253         char *startary;
254         size_t startary_len = 0;
255         char *endary;
256         char *next_boundary;
257         char *content_type;
258         char *charset;
259         size_t content_length;
260         char *encoding;
261         char *disposition;
262         char *name = NULL;
263         char *content_type_name;
264         char *content_disposition_name;
265         char *filename;
266         int is_multipart;
267         int part_seq = 0;
268         int i;
269         size_t length;
270         char nested_partnum[256];
271         int crlf_in_use = 0;
272         char *evaluate_crlf_ptr = NULL;
273
274         ptr = content_start;
275         content_length = 0;
276
277         boundary = malloc(SIZ);
278         memset(boundary, 0, SIZ);
279
280         startary = malloc(SIZ);
281         memset(startary, 0, SIZ);
282
283         endary = malloc(SIZ);
284         memset(endary, 0, SIZ);
285
286         header = malloc(SIZ);
287         memset(header, 0, SIZ);
288
289         content_type = malloc(SIZ);
290         memset(content_type, 0, SIZ);
291
292         charset = malloc(SIZ);
293         memset(charset, 0, SIZ);
294
295         encoding = malloc(SIZ);
296         memset(encoding, 0, SIZ);
297
298         content_type_name = malloc(SIZ);
299         memset(content_type_name, 0, SIZ);
300
301         content_disposition_name = malloc(SIZ);
302         memset(content_disposition_name, 0, SIZ);
303
304         filename = malloc(SIZ);
305         memset(filename, 0, SIZ);
306
307         disposition = malloc(SIZ);
308         memset(disposition, 0, SIZ);
309
310         /* If the caller didn't supply an endpointer, generate one by measure */
311         if (content_end == NULL) {
312                 content_end = &content_start[strlen(content_start)];
313         }
314
315         /* Learn interesting things from the headers */
316         strcpy(header, "");
317         do {
318                 ptr = memreadline(ptr, buf, SIZ);
319                 if (ptr >= content_end) {
320                         goto end_parser;
321                 }
322
323                 for (i = 0; i < strlen(buf); ++i) {
324                         if (isspace(buf[i])) {
325                                 buf[i] = ' ';
326                         }
327                 }
328
329                 if (!isspace(buf[0])) {
330                         if (!strncasecmp(header, "Content-type:", 13)) {
331                                 strcpy(content_type, &header[13]);
332                                 striplt(content_type);
333                                 extract_key(content_type_name, content_type, "name");
334                                 extract_key(charset, content_type, "charset");
335                                 /* Deal with weird headers */
336                                 if (strchr(content_type, ' '))
337                                         *(strchr(content_type, ' ')) = '\0';
338                                 if (strchr(content_type, ';'))
339                                         *(strchr(content_type, ';')) = '\0';
340                         }
341                         if (!strncasecmp(header, "Content-Disposition:", 20)) {
342                                 strcpy(disposition, &header[20]);
343                                 striplt(disposition);
344                                 extract_key(content_disposition_name, disposition, "name");
345                                 extract_key(filename, disposition, "filename");
346                         }
347                         if (!strncasecmp(header, "Content-length: ", 15)) {
348                                 char clbuf[10];
349                                 safestrncpy(clbuf, &header[15], sizeof clbuf);
350                                 striplt(clbuf);
351                                 content_length = (size_t) atol(clbuf);
352                         }
353                         if (!strncasecmp(header, "Content-transfer-encoding: ", 26)) {
354                                 strcpy(encoding, &header[26]);
355                                 striplt(encoding);
356                         }
357                         if (strlen(boundary) == 0)
358                                 extract_key(boundary, header, "boundary");
359                         strcpy(header, "");
360                 }
361                 if ((strlen(header) + strlen(buf) + 2) < SIZ) {
362                         strcat(header, buf);
363                 }
364         } while ((strlen(buf) > 0) && (*ptr != 0));
365
366         if (strchr(disposition, ';'))
367                 *(strchr(disposition, ';')) = '\0';
368         striplt(disposition);
369         if (strchr(content_type, ';'))
370                 *(strchr(content_type, ';')) = '\0';
371         striplt(content_type);
372
373         if (strlen(boundary) > 0) {
374                 is_multipart = 1;
375         } else {
376                 is_multipart = 0;
377         }
378
379         /* If this is a multipart message, then recursively process it */
380         part_start = NULL;
381         if (is_multipart) {
382
383                 /* Tell the client about this message's multipartedness */
384                 if (PreMultiPartCallBack != NULL) {
385                         PreMultiPartCallBack("", "", partnum, "",
386                                 NULL, content_type, charset,
387                                 0, encoding, userdata);
388                 }
389
390                 /* Figure out where the boundaries are */
391                 snprintf(startary, SIZ, "--%s", boundary);
392                 snprintf(endary, SIZ, "--%s--", boundary);
393                 startary_len = strlen(startary);
394
395                 part_start = NULL;
396                 do {
397                         next_boundary = NULL;
398                         for (srch=ptr; srch<content_end; ++srch) {
399                                 if (!memcmp(srch, startary, startary_len)) {
400                                         next_boundary = srch;
401                                         srch = content_end;
402                                 }
403                         }
404
405                         if ( (part_start != NULL) && (next_boundary != NULL) ) {
406                                 part_end = next_boundary;
407                                 --part_end;             /* omit the trailing LF */
408                                 if (crlf_in_use) {
409                                         --part_end;     /* omit the trailing CR */
410                                 }
411
412                                 if (strlen(partnum) > 0) {
413                                         snprintf(nested_partnum,
414                                                  sizeof nested_partnum,
415                                                  "%s.%d", partnum,
416                                                  ++part_seq);
417                                 }
418                                 else {
419                                         snprintf(nested_partnum,
420                                                  sizeof nested_partnum,
421                                                  "%d", ++part_seq);
422                                 }
423                                 the_mime_parser(nested_partnum,
424                                             part_start, part_end,
425                                                 CallBack,
426                                                 PreMultiPartCallBack,
427                                                 PostMultiPartCallBack,
428                                                 userdata,
429                                                 dont_decode);
430                         }
431
432                         if (next_boundary != NULL) {
433                                 /* If we pass out of scope, don't attempt to
434                                  * read past the end boundary. */
435                                 if (!strcmp(next_boundary, endary)) {
436                                         ptr = content_end;
437                                 }
438                                 else {
439                                         /* Set up for the next part. */
440                                         part_start = strstr(next_boundary, "\n");
441                                         
442                                         /* Determine whether newlines are LF or CRLF */
443                                         evaluate_crlf_ptr = part_start;
444                                         --evaluate_crlf_ptr;
445                                         if (!memcmp(evaluate_crlf_ptr, "\r\n", 2)) {
446                                                 crlf_in_use = 1;
447                                         }
448                                         else {
449                                                 crlf_in_use = 0;
450                                         }
451
452                                         /* Advance past the LF ... now we're in the next part */
453                                         ++part_start;
454                                         ptr = part_start;
455                                 }
456                         }
457                         else {
458                                 /* Invalid end of multipart.  Bail out! */
459                                 ptr = content_end;
460                         }
461                 } while ( (ptr < content_end) && (next_boundary != NULL) );
462
463                 if (PostMultiPartCallBack != NULL) {
464                         PostMultiPartCallBack("", "", partnum, "", NULL,
465                                 content_type, charset, 0, encoding, userdata);
466                 }
467                 goto end_parser;
468         }
469
470         /* If it's not a multipart message, then do something with it */
471         if (!is_multipart) {
472                 part_start = ptr;
473                 length = 0;
474                 while (ptr < content_end) {
475                         ++ptr;
476                         ++length;
477                 }
478                 part_end = content_end;
479
480                 /******
481                  * I thought there was an off-by-one error here, but there isn't.
482                  * This probably means that there's an off-by-one error somewhere
483                  * else ... or maybe only in certain messages?
484                 --part_end;
485                 --length;
486                 ******/
487                 
488                 /* Truncate if the header told us to */
489                 if ( (content_length > 0) && (length > content_length) ) {
490                         length = content_length;
491                 }
492
493                 /* Sometimes the "name" field is tacked on to Content-type,
494                  * and sometimes it's tacked on to Content-disposition.  Use
495                  * whichever one we have.
496                  */
497                 if (strlen(content_disposition_name) > strlen(content_type_name)) {
498                         name = content_disposition_name;
499                 }
500                 else {
501                         name = content_type_name;
502                 }
503         
504                 /* lprintf(CTDL_DEBUG, "mime_decode part=%s, len=%d, type=%s, charset=%s, encoding=%s\n",
505                         partnum, length, content_type, charset, encoding); */
506
507                 /* Ok, we've got a non-multipart part here, so do something with it.
508                  */
509                 mime_decode(partnum,
510                         part_start, length,
511                         content_type, charset, encoding, disposition,
512                         name, filename,
513                         CallBack, NULL, NULL,
514                         userdata, dont_decode
515                 );
516
517                 /*
518                  * Now if it's an encapsulated message/rfc822 then we have to recurse into it
519                  */
520                 if (!strcasecmp(content_type, "message/rfc822")) {
521
522                         if (PreMultiPartCallBack != NULL) {
523                                 PreMultiPartCallBack("", "", partnum, "",
524                                         NULL, content_type, charset,
525                                         0, encoding, userdata);
526                         }
527                         if (CallBack != NULL) {
528                                 if (strlen(partnum) > 0) {
529                                         snprintf(nested_partnum,
530                                                  sizeof nested_partnum,
531                                                  "%s.%d", partnum,
532                                                  ++part_seq);
533                                 }
534                                 else {
535                                         snprintf(nested_partnum,
536                                                  sizeof nested_partnum,
537                                                  "%d", ++part_seq);
538                                 }
539                                 the_mime_parser(nested_partnum,
540                                         part_start, part_end,
541                                         CallBack,
542                                         PreMultiPartCallBack,
543                                         PostMultiPartCallBack,
544                                         userdata,
545                                         dont_decode
546                                 );
547                         }
548                         if (PostMultiPartCallBack != NULL) {
549                                 PostMultiPartCallBack("", "", partnum, "", NULL,
550                                         content_type, charset, 0, encoding, userdata);
551                         }
552
553
554                 }
555
556         }
557
558 end_parser:     /* free the buffers!  end the oppression!! */
559         free(boundary);
560         free(startary);
561         free(endary);   
562         free(header);
563         free(content_type);
564         free(charset);
565         free(encoding);
566         free(content_type_name);
567         free(content_disposition_name);
568         free(filename);
569         free(disposition);
570 }
571
572
573
574 /*
575  * Entry point for the MIME parser.
576  * (This function expects to be fed HEADERS + CONTENT)
577  * Note: NULL can be supplied as content_end; in this case, the message is
578  * considered to have ended when the parser encounters a 0x00 byte.
579  */
580 void mime_parser(char *content_start,
581                 char *content_end,
582
583                  void (*CallBack)
584                   (char *cbname,
585                    char *cbfilename,
586                    char *cbpartnum,
587                    char *cbdisp,
588                    void *cbcontent,
589                    char *cbtype,
590                    char *cbcharset,
591                    size_t cblength,
592                    char *cbencoding,
593                    void *cbuserdata),
594
595                  void (*PreMultiPartCallBack)
596                   (char *cbname,
597                    char *cbfilename,
598                    char *cbpartnum,
599                    char *cbdisp,
600                    void *cbcontent,
601                    char *cbtype,
602                    char *cbcharset,
603                    size_t cblength,
604                    char *cbencoding,
605                    void *cbuserdata),
606
607                  void (*PostMultiPartCallBack)
608                   (char *cbname,
609                    char *cbfilename,
610                    char *cbpartnum,
611                    char *cbdisp,
612                    void *cbcontent,
613                    char *cbtype,
614                    char *cbcharset,
615                    size_t cblength,
616                    char *cbencoding,
617                    void *cbuserdata),
618
619                   void *userdata,
620                   int dont_decode
621 )
622 {
623
624         the_mime_parser("", content_start, content_end,
625                         CallBack,
626                         PreMultiPartCallBack,
627                         PostMultiPartCallBack,
628                         userdata, dont_decode);
629 }