]> Pileus Git - ~andy/fetchmail/blob - unmime.c
753c9aa19210a5db06ba7ef7ee567cee5bc97191
[~andy/fetchmail] / unmime.c
1 /*
2  * MIME mail decoding.
3  *
4  * This module contains decoding routines for converting
5  * quoted-printable data into pure 8-bit data, in MIME
6  * formatted messages.
7  *
8  * By Henrik Storner <storner@image.dk>
9  *
10  * Configuration file support for fetchmail 4.3.8 by 
11  * Frank Damgaard <frda@post3.tele.dk>
12  * 
13  */
14
15 #include "config.h"
16 #include <string.h>
17 #include <stdlib.h>
18 #include <stdio.h>
19 #include <ctype.h>
20 #include "fetchmail.h"
21
22 static unsigned char unhex(unsigned char c)
23 {
24   if ((c >= '0') && (c <= '9'))
25     return (c - '0');
26   else if ((c >= 'A') && (c <= 'F'))
27     return (c - 'A' + 10);
28   else if ((c >= 'a') && (c <= 'f'))
29     return (c - 'a' + 10);
30   else
31     return c;
32 }
33
34 static int qp_char(unsigned char c1, unsigned char c2, unsigned char *c_out)
35 {
36   c1 = unhex(c1);
37   c2 = unhex(c2);
38
39   if ((c1 > 15) || (c2 > 15)) 
40     return 1;
41   else {
42     *c_out = 16*c1+c2;
43     return 0;
44   }
45 }
46
47
48
49 /*
50  * Routines to decode MIME QP-encoded headers, as per RFC 2047.
51  */
52
53 /* States of the decoding state machine */
54 #define S_COPY_PLAIN        0   /* Just copy, but watch for the QP flag */
55 #define S_SKIP_MIMEINIT     1   /* Get the encoding, and skip header */
56 #define S_COPY_MIME         2   /* Decode a sequence of coded characters */
57
58 static const char MIMEHDR_INIT[]  = "=?";       /* Start of coded sequence */
59 static const char MIMEHDR_END[]   = "?=";       /* End of coded sequence */
60
61 void UnMimeHeader(unsigned char *hdr)
62 {
63   /* Decode a buffer containing data encoded according to RFC
64    * 2047. This only handles content-transfer-encoding; conversion
65    * between character sets is not implemented.  In other words: We
66    * assume the charsets used can be displayed by your mail program
67    * without problems. 
68    */
69
70   /* Note: Decoding is done "in-situ", i.e. without using an
71    * additional buffer for temp. storage. This is possible, since the
72    * decoded string will always be shorter than the encoded string,
73    * due to the en- coding scheme.
74    */
75
76   int  state = S_COPY_PLAIN;
77   unsigned char *p_in, *p_out, *p;
78   unsigned char enc = '\0';             /* initialization pacifies -Wall */
79   int  i;
80
81   /* Speed up in case this is not a MIME-encoded header */
82   p = strstr(hdr, MIMEHDR_INIT);
83   if (p == NULL)
84     return;   /* No MIME header */
85
86   /* Loop through the buffer.
87    *  p_in : Next char to be processed.
88    *  p_out: Where to put the next processed char
89    *  enc  : Encoding used (usually, 'q' = quoted-printable)
90    */
91   for (p_out = p_in = hdr; (*p_in); ) {
92     switch (state) {
93     case S_COPY_PLAIN:
94       p = strstr(p_in, MIMEHDR_INIT);
95       if (p == NULL) {
96         /* 
97          * No more coded data in buffer, 
98          * just move remainder into place. 
99          */
100         i = strlen(p_in);   /* How much left */
101         memmove(p_out, p_in, i);
102         p_in += i; p_out += i;
103       }
104       else {
105         /* MIME header init found at location p */
106         if (p > p_in) {
107           /* There are some uncoded chars at the beginning. */
108           i = (p - p_in);
109           memmove(p_out, p_in, i);
110           p_out += i;
111         }
112         p_in = (p + 2);
113         state = S_SKIP_MIMEINIT;
114       }
115       break;
116
117     case S_SKIP_MIMEINIT:
118       /* Mime type definition: "charset?encoding?" */
119       p = strchr(p_in, '?');
120       if (p != NULL) {
121         /* p_in .. (p-1) holds the charset */
122
123         /* *(p+1) is the transfer encoding, *(p+2) must be a '?' */
124         if (*(p+2) == '?') {
125           enc = tolower(*(p+1));
126           p_in = p+3;
127           state = S_COPY_MIME;
128         }
129         else
130           state = S_COPY_PLAIN;
131       }
132       else
133         state = S_COPY_PLAIN;   /* Invalid data */
134       break;
135
136     case S_COPY_MIME:
137       p = strstr(p_in, MIMEHDR_END);  /* Find end of coded data */
138       if (p == NULL) p = p_in + strlen(p_in);
139       for (; (p_in < p); ) {
140         /* Decode all encoded data */
141         if (enc == 'q') {
142           if (*p_in == '=') {
143             /* Decode one char qp-coded at (p_in+1) and (p_in+2) */
144             if (qp_char(*(p_in+1), *(p_in+2), p_out) == 0)
145               p_in += 3;
146             else {
147               /* Invalid QP data - pass through unchanged. */
148               *p_out = *p_in;
149               p_in++;
150             }
151           }
152           else if (*p_in == '_') {
153             /* 
154              * RFC 2047: '_' inside encoded word represents 0x20.
155              * NOT a space - always the value 0x20.
156              */
157             *p_out = 0x20;
158             p_in++;
159           }
160           else {
161             /* Copy unchanged */
162             *p_out = *p_in;
163             p_in++;
164           }
165           p_out++;
166         }
167         else if (enc == 'b') {
168           /* Decode base64 encoded data */
169           char delimsave;
170           int decoded_count;
171
172           delimsave = *p; *p = '\r';
173           decoded_count = from64tobits(p_out, p_in);
174           *p = delimsave;
175           if (decoded_count > 0) 
176             p_out += decoded_count;            
177           p_in = p;
178         }
179         else {
180           /* Copy unchanged */
181           *p_out = *p_in;
182           p_in++;
183           p_out++;
184         }
185       }
186       if (*p_in)
187         p_in += 2;   /* Skip the MIMEHDR_END delimiter */
188
189       /* 
190        * We've completed decoding one encoded sequence. But another
191        * may follow immediately, in which case whitespace before the
192        * new MIMEHDR_INIT delimiter must be discarded.
193        * See if that is the case 
194        */
195       p = strstr(p_in, MIMEHDR_INIT);
196       state = S_COPY_PLAIN;
197       if (p != NULL) {
198         /*
199          * There is more MIME data later on. Is there
200          * whitespace  only before the delimiter? 
201          */
202         unsigned char *q;
203         int  wsp_only = 1;
204
205         for (q=p_in; (wsp_only && (q < p)); q++)
206           wsp_only = isspace(*q);
207
208         if (wsp_only) {
209           /* 
210            * Whitespace-only before the MIME delimiter. OK,
211            * just advance p_in to past the new MIMEHDR_INIT,
212            * and prepare to process the new MIME charset/encoding
213            * header.
214            */
215           p_in = p + strlen(MIMEHDR_INIT);
216           state = S_SKIP_MIMEINIT;
217         }
218       }
219       break;
220     }
221   }
222
223   *p_out = '\0';
224 }
225
226
227
228 /*
229  * Routines for decoding body-parts of a message.
230  *
231  * Since the "fetch" part of fetchmail gets a message body
232  * one line at a time, we need to maintain some state variables
233  * across multiple invokations of the UnMimeBodyline() routine.
234  * The driver routine should call MimeBodyType() when all
235  * headers have been received, and then UnMimeBodyline() for
236  * every line in the message body.
237  *
238  */
239 #define S_BODY_DATA 0
240 #define S_BODY_HDR  1
241
242 /* 
243  * Flag indicating if we are currently processing 
244  * the headers or the body of a (multipart) message.
245  */
246 static int  BodyState = S_BODY_DATA;
247
248 /* 
249  * Flag indicating if we are in the process of decoding
250  * a quoted-printable body part.
251  */
252 static int  CurrEncodingIsQP = 0;
253 static int  CurrTypeNeedsDecode = 0;
254
255 /* 
256  * Delimiter for multipart messages. RFC 2046 states that this must
257  * NEVER be longer than 70 characters. Add 3 for the two hyphens
258  * at the beginning, and a terminating null.
259  */
260 #define MAX_DELIM_LEN 70
261 static unsigned char MultipartDelimiter[MAX_DELIM_LEN+3];
262
263
264 /* This string replaces the "Content-Transfer-Encoding: quoted-printable"
265  * string in all headers, including those in body-parts. The replacement
266  * must be no longer than the original string.
267  */
268 static const char ENC8BIT[] = "Content-Transfer-Encoding: 8bit";
269 static void SetEncoding8bit(unsigned char *XferEncOfs)
270 {
271   unsigned char *p;
272
273   if (XferEncOfs != NULL) {
274      memcpy(XferEncOfs, ENC8BIT, strlen(ENC8BIT));
275
276      /* If anything left, in this header, replace with whitespace */
277      for (p=XferEncOfs+strlen(ENC8BIT); (*p >= ' '); p++) *p=' ';
278   }
279 }
280
281 static char *GetBoundary(char *CntType)
282 {
283   char *p1, *p2;
284   int flag;
285
286   /* Find the "boundary" delimiter. It must be preceded with a ';'
287    * and optionally some whitespace.
288    */
289   p1 = CntType;
290   do {
291     p2 = strchr(p1, ';'); 
292     if (p2)
293       for (p2++; isspace(*p2); p2++);
294
295     p1 = p2;
296   } while ((p1) && (strncasecmp(p1, "boundary", 8) != 0));
297
298   if (p1 == NULL)
299     /* No boundary delimiter */
300     return NULL;
301
302   /* Skip "boundary", whitespace and '='; check that we do have a '=' */
303   for (p1+=8, flag=0; (isspace(*p1) || (*p1 == '=')); p1++)
304     flag |= (*p1 == '=');
305   if (!flag)
306     return NULL;
307
308   /* Find end of boundary delimiter string */
309   if (*p1 == '\"') {
310     /* The delimiter is inside quotes */
311     p1++;
312     p2 = strchr(p1, '\"');
313     if (p2 == NULL)
314       return NULL;  /* No closing '"' !?! */
315   }
316   else {
317     /* There might be more text after the "boundary" string. */
318     p2 = strchr(p1, ';');  /* Safe - delimiter with ';' must be in quotes */
319   }
320
321   /* Zero-terminate the boundary string */
322   if (p2 != NULL)
323     *p2 = '\0';
324
325   return (p1 && strlen(p1)) ? p1 : NULL;
326 }
327
328
329 int CheckContentType(char *CntType)
330 {
331   /*
332    * Static array of Content-Type's for which we will do
333    * quoted-printable decoding, if requested. 
334    * It is probably wise to do this only on known text-only types;
335    * be really careful if you change this.
336    */
337
338   static char *DecodedTypes[] = {
339     "text/",        /* Will match ALL content-type's starting with 'text/' */
340     "message/rfc822", 
341     NULL
342   };
343
344   char *p = CntType;
345   int i;
346
347   /* Skip whitespace, if any */
348   for (; isspace(*p); p++) ;
349
350   for (i=0; 
351        (DecodedTypes[i] && 
352         (strncasecmp(p, DecodedTypes[i], strlen(DecodedTypes[i])))); 
353        i++) ;
354
355   return (DecodedTypes[i] != NULL);
356 }
357
358
359 /*
360  * This routine does three things:
361  * 1) It determines - based on the message headers - whether the
362  *    message body is a MIME message that may hold 8 bit data.
363  *    - A message that has a "quoted-printable" or "8bit" transfer 
364  *      encoding is assumed to contain 8-bit data (when decoded).
365  *    - A multipart message is assumed to contain 8-bit data
366  *      when decoded (there might be quoted-printable body-parts).
367  *    - All other messages are assumed NOT to include 8-bit data.
368  * 2) It determines the delimiter-string used in multi-part message
369  *    bodies.
370  * 3) It sets the initial values of the CurrEncodingIsQP, 
371  *    CurrTypeNeedsDecode, and BodyState variables, from the header 
372  *    contents.
373  *
374  * The return value is a bitmask.
375  */
376 int MimeBodyType(unsigned char *hdrs, int WantDecode)
377 {
378   unsigned char *NxtHdr = hdrs;
379   unsigned char *XferEnc, *XferEncOfs, *CntType, *MimeVer, *p;
380   int  HdrsFound = 0;     /* We only look for three headers */
381   int  BodyType;          /* Return value */ 
382
383   /* Setup for a standard (no MIME, no QP, 7-bit US-ASCII) message */
384   MultipartDelimiter[0] = '\0';
385   CurrEncodingIsQP = CurrTypeNeedsDecode = 0;
386   BodyState = S_BODY_DATA;
387   BodyType = 0;
388
389   /* Just in case ... */
390   if (hdrs == NULL)
391     return BodyType;
392
393   XferEnc = XferEncOfs = CntType = MimeVer = NULL;
394
395   do {
396     if (strncasecmp("Content-Transfer-Encoding:", NxtHdr, 26) == 0) {
397       XferEncOfs = NxtHdr;
398       p = nxtaddr(NxtHdr);
399       if (p != NULL) {
400         xalloca(XferEnc, char *, strlen(p) + 1);
401         strcpy(XferEnc, p);
402         HdrsFound++;
403       }
404     }
405     else if (strncasecmp("Content-Type:", NxtHdr, 13) == 0) {
406       /*
407        * This one is difficult. We cannot use the standard
408        * nxtaddr() routine, since the boundary-delimiter is
409        * (probably) enclosed in quotes - and thus appears
410        * as an rfc822 comment, and nxtaddr() "eats" up any
411        * spaces in the delimiter. So, we have to do this
412        * by hand.
413        */
414
415       /* Skip the "Content-Type:" part and whitespace after it */
416       for (NxtHdr += 13; ((*NxtHdr == ' ') || (*NxtHdr == '\t')); NxtHdr++);
417
418       /* 
419        * Get the full value of the Content-Type header;
420        * it might span multiple lines. So search for
421        * a newline char, but ignore those that have a
422        * have a TAB or space just after the NL (continued
423        * lines).
424        */
425       p = NxtHdr-1;
426       do {
427         p=strchr((p+1),'\n'); 
428       } while ( (p != NULL) && ((*(p+1) == '\t') || (*(p+1) == ' ')) );
429       if (p == NULL) p = NxtHdr + strlen(NxtHdr);
430
431       xalloca(CntType, char *, p-NxtHdr+2);
432       strncpy(CntType, NxtHdr, (p-NxtHdr));
433       *(CntType+(p-NxtHdr)) = '\0';
434       HdrsFound++;
435     }
436     else if (strncasecmp("MIME-Version:", NxtHdr, 13) == 0) {
437       p = nxtaddr(NxtHdr);
438       if (p != NULL) {
439         xalloca(MimeVer, char *, strlen(p) + 1);
440         strcpy(MimeVer, p);
441         HdrsFound++;
442       }
443     }
444
445     NxtHdr = (strchr(NxtHdr, '\n'));
446     if (NxtHdr != NULL) NxtHdr++;
447   } while ((NxtHdr != NULL) && (*NxtHdr) && (HdrsFound != 3));
448
449
450   /* Done looking through the headers, now check what they say */
451   if ((MimeVer != NULL) && (strcmp(MimeVer, "1.0") == 0)) {
452
453     CurrTypeNeedsDecode = CheckContentType(CntType);
454
455     /* Check Content-Type to see if this is a multipart message */
456     if ( (CntType != NULL) &&
457          ((strncasecmp(CntType, "multipart/", 10) == 0) ||
458           (strncasecmp(CntType, "message/", 8) == 0)) ) {
459
460       char *p1 = GetBoundary(CntType);
461
462       if (p1 != NULL) {
463         /* The actual delimiter is "--" followed by 
464            the boundary string */
465         strcpy(MultipartDelimiter, "--");
466         strncat(MultipartDelimiter, p1, MAX_DELIM_LEN);
467         BodyType = (MSG_IS_8BIT | MSG_NEEDS_DECODE);
468       }
469     }
470
471     /* 
472      * Check Content-Transfer-Encoding, but
473      * ONLY for non-multipart messages (BodyType == 0).
474      */
475     if ((XferEnc != NULL) && (BodyType == 0)) {
476       if (strcasecmp(XferEnc, "quoted-printable") == 0) {
477         CurrEncodingIsQP = 1;
478         BodyType = (MSG_IS_8BIT | MSG_NEEDS_DECODE);
479         if (WantDecode && CurrTypeNeedsDecode) {
480            SetEncoding8bit(XferEncOfs);
481         }
482       }
483       else if (strcasecmp(XferEnc, "7bit") == 0) {
484         CurrEncodingIsQP = 0;
485         BodyType = (MSG_IS_7BIT);
486       }
487       else if (strcasecmp(XferEnc, "8bit") == 0) {
488         CurrEncodingIsQP = 0;
489         BodyType = (MSG_IS_8BIT);
490       }
491     }
492
493   }
494
495   return BodyType;
496 }
497
498
499 /*
500  * Decode one line of data containing QP data.
501  * Return flag set if this line ends with a soft line-break.
502  * 'bufp' is modified to point to the end of the output buffer.
503  */
504 static int DoOneQPLine(unsigned char **bufp, int collapsedoubledot)
505 {
506   unsigned char *buf = *bufp;
507   unsigned char *p_in, *p_out, *p;
508   int n;
509   int ret = 0;
510
511   p_in = buf;
512   if (collapsedoubledot && (strncmp(buf, "..", 2) == 0))
513     p_in++;
514
515   for (p_out = buf; (*p_in); ) {
516     p = strchr(p_in, '=');
517     if (p == NULL) {
518       /* No more QP data, just move remainder into place */
519       n = strlen(p_in);
520       memmove(p_out, p_in, n);
521       p_in += n; p_out += n;
522     }
523     else {
524       if (p > p_in) {
525         /* There are some uncoded chars at the beginning. */
526         n = (p - p_in);
527         memmove(p_out, p_in, n);
528         p_out += n;
529       }
530               
531       switch (*(p+1)) {
532       case '\0': case '\r': case '\n':
533         /* Soft line break, skip '=' */
534         p_in = p+1; 
535         if (*p_in == '\r') p_in++;
536         if (*p_in == '\n') p_in++;
537         ret = 1;
538         break;
539
540       default:
541         /* There is a QP encoded byte */
542         if (qp_char(*(p+1), *(p+2), p_out) == 0) {
543           p_in = p+3;
544         }
545         else {
546           /* Invalid QP data - pass through unchanged. */
547           *p_out = '=';
548           p_in = p+1;
549         }
550         p_out++;
551         break;
552       }
553     }
554   }
555
556   *p_out = '\0';
557   *bufp = p_out;
558   return ret;
559 }
560
561
562 /* This is called once per line in the message body.  We need to scan
563  * all lines in the message body for the multipart delimiter string,
564  * and handle any body-part headers in such messages (these can toggle
565  * qp-decoding on and off).
566  *
567  * Note: Messages that are NOT multipart-messages go through this
568  * routine quickly, since BodyState will always be S_BODY_DATA,
569  * and MultipartDelimiter is NULL.
570  *
571  * Return flag set if this line ends with a soft line-break.
572  * 'bufp' is modified to point to the end of the output buffer.
573  */
574
575 int UnMimeBodyline(unsigned char **bufp, int collapsedoubledot)
576 {
577   unsigned char *buf = *bufp;
578   int ret = 0;
579
580   switch (BodyState) {
581   case S_BODY_HDR:
582     UnMimeHeader(buf);   /* Headers in body-parts can be encoded, too! */
583     if ((*buf == '\0') || (*buf == '\n') || (strcmp(buf, "\r\n") == 0)) {
584       BodyState = S_BODY_DATA;
585     } 
586     else if (strncasecmp("Content-Transfer-Encoding:", buf, 26) == 0) {
587       char *XferEnc;
588
589       XferEnc = nxtaddr(buf);
590       if ((XferEnc != NULL) && (strcasecmp(XferEnc, "quoted-printable") == 0)) {
591         CurrEncodingIsQP = 1;
592
593         /* Hmm ... we cannot be really sure that CurrTypeNeedsDecode
594            has been set - we may not have seen the Content-Type header
595            yet. But *usually* the Content-Type header comes first, so
596            this will work. And there is really no way of doing it 
597            "right" as long as we stick with the line-by-line processing. */
598         if (CurrTypeNeedsDecode) SetEncoding8bit(buf);
599       }
600     }
601     else if (strncasecmp("Content-Type:", buf, 13) == 0) {
602       CurrTypeNeedsDecode = CheckContentType(nxtaddr(buf));
603     }
604
605     *bufp = (buf + strlen(buf));
606     break;
607
608   case S_BODY_DATA:
609     if ((*MultipartDelimiter) && 
610         (strncmp(buf, MultipartDelimiter, strlen(MultipartDelimiter)) == 0)) {
611       BodyState = S_BODY_HDR;
612       CurrEncodingIsQP = CurrTypeNeedsDecode = 0;
613     }
614
615     if (CurrEncodingIsQP && CurrTypeNeedsDecode) 
616       ret = DoOneQPLine(bufp, collapsedoubledot);
617     else
618      *bufp = (buf + strlen(buf));
619     break;
620   }
621
622   return ret;
623 }
624
625
626 #ifdef STANDALONE
627 #include <stdio.h>
628 #include <unistd.h>
629
630 char *program_name = "unmime";
631 int outlevel = 0;
632
633 #define BUFSIZE_INCREMENT 4096
634
635 #ifdef DEBUG
636 #define DBG_FWRITE(B,L,BS,FD) fwrite(B, L, BS, FD)
637 #else
638 #define DBG_FWRITE(B,L,BS,FD)
639 #endif
640
641 int main(int argc, char *argv[])
642 {
643   unsigned int BufSize;
644   unsigned char *buffer, *buf_p;
645   int nl_count, i, bodytype;
646
647 #ifdef DEBUG
648   pid_t pid;
649   FILE *fd_orig, *fd_conv;
650   char fnam[100];
651
652   pid = getpid();
653   sprintf(fnam, "/tmp/i_unmime.%x", pid);
654   fd_orig = fopen(fnam, "w");
655   sprintf(fnam, "/tmp/o_unmime.%x", pid);
656   fd_conv = fopen(fnam, "w");
657 #endif
658
659   BufSize = BUFSIZE_INCREMENT;    /* Initial size of buffer */
660   buf_p = buffer = (unsigned char *) xmalloc(BufSize);
661   nl_count = 0;
662
663   do {
664     i = fread(buf_p, 1, 1, stdin);
665     switch (*buf_p) {
666      case '\n':
667        nl_count++;
668        break;
669
670      case '\r':
671        break;
672
673      default:
674        nl_count = 0;
675        break;
676     }
677
678     buf_p++;
679     if ((buf_p - buffer) == BufSize) {
680        /* Buffer is full! Get more room. */
681        buffer = xrealloc(buffer, BufSize+BUFSIZE_INCREMENT);
682        buf_p = buffer + BufSize;
683        BufSize += BUFSIZE_INCREMENT;
684     }
685   } while ((i > 0) && (nl_count < 2));
686
687   *buf_p = '\0';
688   DBG_FWRITE(buffer, strlen(buffer), 1, fd_orig);
689
690   UnMimeHeader(buffer);
691   bodytype = MimeBodyType(buffer, 1);
692
693   i = strlen(buffer);
694   fwrite(buffer, i, 1, stdout);
695   DBG_FWRITE(buffer, i, 1, fd_conv);
696   
697   do {
698      buf_p = (buffer - 1);
699      do {
700         buf_p++;
701         i = fread(buf_p, 1, 1, stdin);
702      } while ((i == 1) && (*buf_p != '\n'));
703      if (i == 1) buf_p++;
704      *buf_p = '\0';
705      DBG_FWRITE(buf, (buf_p - buffer), 1, fd_orig);
706
707      if (buf_p > buffer) {
708         if (bodytype & MSG_NEEDS_DECODE) {
709            buf_p = buffer;
710            UnMimeBodyline(&buf_p, 0);
711         }
712         fwrite(buffer, (buf_p - buffer), 1, stdout);
713         DBG_FWRITE(buffer, (buf_p - buffer), 1, fd_conv);
714      }
715   } while (buf_p > buffer);
716
717   free(buffer);
718   fflush(stdout);
719
720 #ifdef DEBUG
721   fclose(fd_orig);
722   fclose(fd_conv);
723 #endif
724
725   return 0;
726 }
727 #endif
728