(no commit message)
[citadel.git] / libcitadel / lib / mime_parser.c
1 /*
2  * $Id$
3  *
4  * This is the MIME parser for Citadel.
5  *
6  * Copyright (c) 1998-2007 by the citadel.org development team.
7  * This code is distributed under the GNU General Public License v3.
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 "libcitadel.h"
22
23 void extract_key(char *target, char *source, char *key)
24 {
25         char *ptr;
26         char looking_for[256];
27         int double_quotes = 0;
28
29         snprintf(looking_for, sizeof looking_for, "%s=", key);
30
31         ptr = bmstrcasestr(source, looking_for);
32         if (ptr == NULL) {
33                 strcpy(target, "");
34                 return;
35         }
36         strcpy(target, (ptr + strlen(looking_for)));
37
38         for (ptr=target; (*ptr != 0); ++ptr) {
39
40                 /* A semicolon means we've hit the end of the key, unless we're inside double quotes */
41                 if ( (double_quotes != 1) && (*ptr == ';')) {
42                         *ptr = 0;
43                 }
44
45                 /* if we find double quotes, we've got a great set of string boundaries */
46                 if (*ptr == '\"') {
47                         ++double_quotes;
48                         if (double_quotes == 1) {
49                                 strcpy(ptr, ptr+1);
50                         }
51                         else {
52                                 *ptr = 0;
53                         }
54                 }
55         }
56 }
57
58
59 /*
60  * For non-multipart messages, we need to generate a quickie partnum of "1"
61  * to return to callback functions.  Some callbacks demand it.
62  */
63 char *fixed_partnum(char *supplied_partnum) {
64         if (supplied_partnum == NULL) return "1";
65         if (strlen(supplied_partnum)==0) return "1";
66         return supplied_partnum;
67 }
68
69
70
71 /*
72  * Convert "quoted-printable" to binary.  Returns number of bytes decoded.
73  * according to RFC2045 section 6.7
74  */
75 int CtdlDecodeQuotedPrintable(char *decoded, char *encoded, int sourcelen) {
76         unsigned int ch;
77         int decoded_length = 0;
78         int pos = 0;
79
80         while (pos < sourcelen)
81         {
82                 if (!strncmp(&encoded[pos], "=\r\n", 3))
83                 {
84                         pos += 3;
85                 }
86                 else if (!strncmp(&encoded[pos], "=\n", 2))
87                 {
88                         pos += 2;
89                 }
90                 else if (encoded[pos] == '=')
91                 {
92                         ch = 0;
93                         sscanf(&encoded[pos+1], "%02x", &ch);
94                         pos += 3;
95                         decoded[decoded_length++] = ch;
96                 }
97                 else
98                 {
99                         decoded[decoded_length++] = encoded[pos];
100                         pos += 1;
101                 }
102         }
103         decoded[decoded_length] = 0;
104         return(decoded_length);
105 }
106
107
108 /*
109  * Given a message or message-part body and a length, handle any necessary
110  * decoding and pass the request up the stack.
111  */
112 void mime_decode(char *partnum,
113                  char *part_start, size_t length,
114                  char *content_type, char *charset, char *encoding,
115                  char *disposition,
116                  char *name, char *filename,
117                  void (*CallBack)
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 (*PreMultiPartCallBack)
129                   (char *cbname,
130                    char *cbfilename,
131                    char *cbpartnum,
132                    char *cbdisp,
133                    void *cbcontent,
134                    char *cbtype,
135                    char *cbcharset,
136                    size_t cblength,
137                    char *cbencoding,
138                    void *cbuserdata),
139                  void (*PostMultiPartCallBack)
140                   (char *cbname,
141                    char *cbfilename,
142                    char *cbpartnum,
143                    char *cbdisp,
144                    void *cbcontent,
145                    char *cbtype,
146                    char *cbcharset,
147                    size_t cblength,
148                    char *cbencoding,
149                    void *cbuserdata),
150                   void *userdata,
151                   int dont_decode
152 )
153 {
154
155         char *decoded;
156         size_t bytes_decoded = 0;
157
158         /* Some encodings aren't really encodings */
159         if (!strcasecmp(encoding, "7bit"))
160                 strcpy(encoding, "");
161         if (!strcasecmp(encoding, "8bit"))
162                 strcpy(encoding, "");
163         if (!strcasecmp(encoding, "binary"))
164                 strcpy(encoding, "");
165
166         /* If this part is not encoded, send as-is */
167         if ( (strlen(encoding) == 0) || (dont_decode)) {
168                 if (CallBack != NULL) {
169                         CallBack(name, filename, fixed_partnum(partnum),
170                                 disposition, part_start,
171                                 content_type, charset, length, encoding, userdata);
172                         }
173                 return;
174         }
175         
176         /* Fail silently if we hit an unknown encoding. */
177         if ((strcasecmp(encoding, "base64"))
178             && (strcasecmp(encoding, "quoted-printable"))) {
179                 return;
180         }
181
182         /*
183          * Allocate a buffer for the decoded data.  The output buffer is slightly
184          * larger than the input buffer; this assumes that the decoded data
185          * will never be significantly larger than the encoded data.  This is a
186          * safe assumption with base64, uuencode, and quoted-printable.
187          */
188         decoded = malloc(length + 32768);
189         if (decoded == NULL) {
190                 return;
191         }
192
193         if (!strcasecmp(encoding, "base64")) {
194                 bytes_decoded = CtdlDecodeBase64(decoded, part_start, length);
195         }
196         else if (!strcasecmp(encoding, "quoted-printable")) {
197                 bytes_decoded = CtdlDecodeQuotedPrintable(decoded, part_start, length);
198         }
199
200         if (bytes_decoded > 0) if (CallBack != NULL) {
201                 CallBack(name, filename, fixed_partnum(partnum),
202                         disposition, decoded,
203                         content_type, charset, bytes_decoded, "binary", userdata);
204         }
205
206         free(decoded);
207 }
208
209 /*
210  * Break out the components of a multipart message
211  * (This function expects to be fed HEADERS + CONTENT)
212  * Note: NULL can be supplied as content_end; in this case, the message is
213  * considered to have ended when the parser encounters a 0x00 byte.
214  */
215 void the_mime_parser(char *partnum,
216                      char *content_start, char *content_end,
217                      void (*CallBack)
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 (*PreMultiPartCallBack)
229                       (char *cbname,
230                        char *cbfilename,
231                        char *cbpartnum,
232                        char *cbdisp,
233                        void *cbcontent,
234                        char *cbtype,
235                        char *cbcharset,
236                        size_t cblength,
237                        char *cbencoding,
238                        void *cbuserdata),
239                      void (*PostMultiPartCallBack)
240                       (char *cbname,
241                        char *cbfilename,
242                        char *cbpartnum,
243                        char *cbdisp,
244                        void *cbcontent,
245                        char *cbtype,
246                        char *cbcharset,
247                        size_t cblength,
248                        char *cbencoding,
249                        void *cbuserdata),
250                       void *userdata,
251                       int dont_decode
252 )
253 {
254
255         char *ptr;
256         char *srch = NULL;
257         char *part_start, *part_end = NULL;
258         char buf[SIZ];
259         char *header;
260         char *boundary;
261         char *startary;
262         size_t startary_len = 0;
263         char *endary;
264         char *next_boundary;
265         char *content_type;
266         char *charset;
267         size_t content_length;
268         char *encoding;
269         char *disposition;
270         char *name = NULL;
271         char *content_type_name;
272         char *content_disposition_name;
273         char *filename;
274         int is_multipart;
275         int part_seq = 0;
276         int i;
277         size_t length;
278         char nested_partnum[256];
279         int crlf_in_use = 0;
280         char *evaluate_crlf_ptr = NULL;
281         int buflen = 0;
282         int headerlen = 0;
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         headerlen = 0;
328         do {
329                 ptr = memreadlinelen(ptr, buf, SIZ, &buflen);
330                 if (ptr >= content_end) {
331                         goto end_parser;
332                 }
333
334                 for (i = 0; i < buflen; ++i) {
335                         if (isspace(buf[i])) {
336                                 buf[i] = ' ';
337                         }
338                 }
339
340                 if (!isspace(buf[0])) {
341                         if (!strncasecmp(header, "Content-type:", 13)) {
342                                 strcpy(content_type, &header[13]);
343                                 striplt(content_type);
344                                 extract_key(content_type_name, content_type, "name");
345                                 extract_key(charset, content_type, "charset");
346                                 extract_key(boundary, header, "boundary");
347                                 /* Deal with weird headers */
348                                 if (strchr(content_type, ' '))
349                                         *(strchr(content_type, ' ')) = '\0';
350                                 if (strchr(content_type, ';'))
351                                         *(strchr(content_type, ';')) = '\0';
352                         }
353                         if (!strncasecmp(header, "Content-Disposition:", 20)) {
354                                 strcpy(disposition, &header[20]);
355                                 striplt(disposition);
356                                 extract_key(content_disposition_name, disposition, "name");
357                                 extract_key(filename, disposition, "filename");
358                         }
359                         if (!strncasecmp(header, "Content-length: ", 15)) {
360                                 char clbuf[10];
361                                 safestrncpy(clbuf, &header[15], sizeof clbuf);
362                                 striplt(clbuf);
363                                 content_length = (size_t) atol(clbuf);
364                         }
365                         if (!strncasecmp(header, "Content-transfer-encoding: ", 26)) {
366                                 strcpy(encoding, &header[26]);
367                                 striplt(encoding);
368                         }
369                         strcpy(header, "");
370                         headerlen = 0;
371                 }
372                 if ((headerlen + buflen + 2) < SIZ) {
373                         memcpy(&header[headerlen], buf, buflen);
374                         headerlen += buflen;
375                         header[headerlen] = '\0';
376                 }
377         } while ((!IsEmptyStr(buf)) && (*ptr != 0));
378
379         if (strchr(disposition, ';'))
380                 *(strchr(disposition, ';')) = '\0';
381         striplt(disposition);
382         if (strchr(content_type, ';'))
383                 *(strchr(content_type, ';')) = '\0';
384         striplt(content_type);
385
386         if (!IsEmptyStr(boundary)) {
387                 is_multipart = 1;
388         } else {
389                 is_multipart = 0;
390         }
391
392         /* If this is a multipart message, then recursively process it */
393         part_start = NULL;
394         if (is_multipart) {
395
396                 /* Tell the client about this message's multipartedness */
397                 if (PreMultiPartCallBack != NULL) {
398                         PreMultiPartCallBack("", "", partnum, "",
399                                 NULL, content_type, charset,
400                                 0, encoding, userdata);
401                 }
402
403                 /* Figure out where the boundaries are */
404                 snprintf(startary, SIZ, "--%s", boundary);
405                 snprintf(endary, SIZ, "--%s--", boundary);
406                 startary_len = strlen(startary);
407
408                 part_start = NULL;
409                 do {
410                         next_boundary = NULL;
411                         for (srch=ptr; srch<content_end; ++srch) {
412                                 if (!memcmp(srch, startary, startary_len)) {
413                                         next_boundary = srch;
414                                         srch = content_end;
415                                 }
416                         }
417
418                         if ( (part_start != NULL) && (next_boundary != NULL) ) {
419                                 part_end = next_boundary;
420                                 --part_end;             /* omit the trailing LF */
421                                 if (crlf_in_use) {
422                                         --part_end;     /* omit the trailing CR */
423                                 }
424
425                                 if (!IsEmptyStr(partnum)) {
426                                         snprintf(nested_partnum,
427                                                  sizeof nested_partnum,
428                                                  "%s.%d", partnum,
429                                                  ++part_seq);
430                                 }
431                                 else {
432                                         snprintf(nested_partnum,
433                                                  sizeof nested_partnum,
434                                                  "%d", ++part_seq);
435                                 }
436                                 the_mime_parser(nested_partnum,
437                                             part_start, part_end,
438                                                 CallBack,
439                                                 PreMultiPartCallBack,
440                                                 PostMultiPartCallBack,
441                                                 userdata,
442                                                 dont_decode);
443                         }
444
445                         if (next_boundary != NULL) {
446                                 /* If we pass out of scope, don't attempt to
447                                  * read past the end boundary. */
448                                 if (!strcmp(next_boundary, endary)) {
449                                         ptr = content_end;
450                                 }
451                                 else {
452                                         /* Set up for the next part. */
453                                         part_start = strstr(next_boundary, "\n");
454                                         
455                                         /* Determine whether newlines are LF or CRLF */
456                                         evaluate_crlf_ptr = part_start;
457                                         --evaluate_crlf_ptr;
458                                         if (!memcmp(evaluate_crlf_ptr, "\r\n", 2)) {
459                                                 crlf_in_use = 1;
460                                         }
461                                         else {
462                                                 crlf_in_use = 0;
463                                         }
464
465                                         /* Advance past the LF ... now we're in the next part */
466                                         ++part_start;
467                                         ptr = part_start;
468                                 }
469                         }
470                         else {
471                                 /* Invalid end of multipart.  Bail out! */
472                                 ptr = content_end;
473                         }
474                 } while ( (ptr < content_end) && (next_boundary != NULL) );
475
476                 if (PostMultiPartCallBack != NULL) {
477                         PostMultiPartCallBack("", "", partnum, "", NULL,
478                                 content_type, charset, 0, encoding, userdata);
479                 }
480                 goto end_parser;
481         }
482
483         /* If it's not a multipart message, then do something with it */
484         if (!is_multipart) {
485                 part_start = ptr;
486                 length = 0;
487                 while (ptr < content_end) {
488                         ++ptr;
489                         ++length;
490                 }
491                 part_end = content_end;
492
493                 /******
494                  * I thought there was an off-by-one error here, but there isn't.
495                  * This probably means that there's an off-by-one error somewhere
496                  * else ... or maybe only in certain messages?
497                 --part_end;
498                 --length;
499                 ******/
500                 
501                 /* Truncate if the header told us to */
502                 if ( (content_length > 0) && (length > content_length) ) {
503                         length = content_length;
504                 }
505
506                 /* Sometimes the "name" field is tacked on to Content-type,
507                  * and sometimes it's tacked on to Content-disposition.  Use
508                  * whichever one we have.
509                  */
510                 if (strlen(content_disposition_name) > strlen(content_type_name)) {
511                         name = content_disposition_name;
512                 }
513                 else {
514                         name = content_type_name;
515                 }
516         
517                 /* Ok, we've got a non-multipart part here, so do something with it.
518                  */
519                 mime_decode(partnum,
520                         part_start, length,
521                         content_type, charset, encoding, disposition,
522                         name, filename,
523                         CallBack, NULL, NULL,
524                         userdata, dont_decode
525                 );
526
527                 /*
528                  * Now if it's an encapsulated message/rfc822 then we have to recurse into it
529                  */
530                 if (!strcasecmp(content_type, "message/rfc822")) {
531
532                         if (PreMultiPartCallBack != NULL) {
533                                 PreMultiPartCallBack("", "", partnum, "",
534                                         NULL, content_type, charset,
535                                         0, encoding, userdata);
536                         }
537                         if (CallBack != NULL) {
538                                 if (strlen(partnum) > 0) {
539                                         snprintf(nested_partnum,
540                                                  sizeof nested_partnum,
541                                                  "%s.%d", partnum,
542                                                  ++part_seq);
543                                 }
544                                 else {
545                                         snprintf(nested_partnum,
546                                                  sizeof nested_partnum,
547                                                  "%d", ++part_seq);
548                                 }
549                                 the_mime_parser(nested_partnum,
550                                         part_start, part_end,
551                                         CallBack,
552                                         PreMultiPartCallBack,
553                                         PostMultiPartCallBack,
554                                         userdata,
555                                         dont_decode
556                                 );
557                         }
558                         if (PostMultiPartCallBack != NULL) {
559                                 PostMultiPartCallBack("", "", partnum, "", NULL,
560                                         content_type, charset, 0, encoding, userdata);
561                         }
562
563
564                 }
565
566         }
567
568 end_parser:     /* free the buffers!  end the oppression!! */
569         free(boundary);
570         free(startary);
571         free(endary);   
572         free(header);
573         free(content_type);
574         free(charset);
575         free(encoding);
576         free(content_type_name);
577         free(content_disposition_name);
578         free(filename);
579         free(disposition);
580 }
581
582
583
584 /*
585  * Entry point for the MIME parser.
586  * (This function expects to be fed HEADERS + CONTENT)
587  * Note: NULL can be supplied as content_end; in this case, the message is
588  * considered to have ended when the parser encounters a 0x00 byte.
589  */
590 void mime_parser(char *content_start,
591                 char *content_end,
592
593                  void (*CallBack)
594                   (char *cbname,
595                    char *cbfilename,
596                    char *cbpartnum,
597                    char *cbdisp,
598                    void *cbcontent,
599                    char *cbtype,
600                    char *cbcharset,
601                    size_t cblength,
602                    char *cbencoding,
603                    void *cbuserdata),
604
605                  void (*PreMultiPartCallBack)
606                   (char *cbname,
607                    char *cbfilename,
608                    char *cbpartnum,
609                    char *cbdisp,
610                    void *cbcontent,
611                    char *cbtype,
612                    char *cbcharset,
613                    size_t cblength,
614                    char *cbencoding,
615                    void *cbuserdata),
616
617                  void (*PostMultiPartCallBack)
618                   (char *cbname,
619                    char *cbfilename,
620                    char *cbpartnum,
621                    char *cbdisp,
622                    void *cbcontent,
623                    char *cbtype,
624                    char *cbcharset,
625                    size_t cblength,
626                    char *cbencoding,
627                    void *cbuserdata),
628
629                   void *userdata,
630                   int dont_decode
631 )
632 {
633
634         the_mime_parser("", content_start, content_end,
635                         CallBack,
636                         PreMultiPartCallBack,
637                         PostMultiPartCallBack,
638                         userdata, dont_decode);
639 }
640
641
642
643
644
645
646 typedef struct _MimeGuess {
647         const char *Pattern;
648         size_t PatternLen;
649         long PatternOffset;
650         const char *MimeString;
651 } MimeGuess;
652
653 MimeGuess MyMimes [] = {
654         {
655                 "GIF",
656                 3,
657                 0,
658                 "image/gif"
659         },
660         {
661                 "\xff\xd8",
662                 2,
663                 0,
664                 "image/jpeg"
665         },
666         {
667                 "\x89PNG",
668                 4,
669                 0,
670                 "image/png"
671         },
672         { // last...
673                 "",
674                 0,
675                 0,
676                 ""
677         }
678 };
679
680
681 const char *GuessMimeType(char *data, size_t dlen)
682 {
683         int MimeIndex = 0;
684
685         while (MyMimes[MimeIndex].PatternLen != 0)
686         {
687                 if ((MyMimes[MimeIndex].PatternLen + 
688                      MyMimes[MimeIndex].PatternOffset < dlen) &&
689                     strncmp(MyMimes[MimeIndex].Pattern, 
690                             &data[MyMimes[MimeIndex].PatternOffset], 
691                             MyMimes[MimeIndex].PatternLen) == 0)
692                 {
693                         break;
694                 }
695                 MimeIndex ++;
696         }
697         return MyMimes[MimeIndex].MimeString;
698
699 }