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