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