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