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