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