4 * This module contains decoding routines for converting
5 * quoted-printable data into pure 8-bit data, in MIME
8 * By Henrik Storner <storner@image.dk>
10 * Configuration file support for fetchmail 4.3.8 by
11 * Frank Damgaard <frda@post3.tele.dk>
13 * For license terms, see the file COPYING in this directory.
21 #include "fetchmail.h"
24 static unsigned char unhex(unsigned char c)
26 if ((c >= '0') && (c <= '9'))
28 else if ((c >= 'A') && (c <= 'F'))
29 return (c - 'A' + 10);
30 else if ((c >= 'a') && (c <= 'f'))
31 return (c - 'a' + 10);
33 return 16; /* invalid hex character */
36 static int qp_char(unsigned char c1, unsigned char c2, unsigned char *c_out)
41 if ((c1 > 15) || (c2 > 15))
51 * Routines to decode MIME QP-encoded headers, as per RFC 2047.
54 /* States of the decoding state machine */
55 #define S_COPY_PLAIN 0 /* Just copy, but watch for the QP flag */
56 #define S_SKIP_MIMEINIT 1 /* Get the encoding, and skip header */
57 #define S_COPY_MIME 2 /* Decode a sequence of coded characters */
59 static const char MIMEHDR_INIT[] = "=?"; /* Start of coded sequence */
60 static const char MIMEHDR_END[] = "?="; /* End of coded sequence */
62 void UnMimeHeader(unsigned char *hdr)
64 /* Decode a buffer containing data encoded according to RFC
65 * 2047. This only handles content-transfer-encoding; conversion
66 * between character sets is not implemented. In other words: We
67 * assume the charsets used can be displayed by your mail program
71 /* Note: Decoding is done "in-situ", i.e. without using an
72 * additional buffer for temp. storage. This is possible, since the
73 * decoded string will always be shorter than the encoded string,
74 * due to the encoding scheme.
77 int state = S_COPY_PLAIN;
78 unsigned char *p_in, *p_out, *p;
79 unsigned char enc = '\0'; /* initialization pacifies -Wall */
82 /* Speed up in case this is not a MIME-encoded header */
83 p = strstr(hdr, MIMEHDR_INIT);
85 return; /* No MIME header */
87 /* Loop through the buffer.
88 * p_in : Next char to be processed.
89 * p_out: Where to put the next processed char
90 * enc : Encoding used (usually, 'q' = quoted-printable)
92 for (p_out = p_in = hdr; (*p_in); ) {
95 p = strstr(p_in, MIMEHDR_INIT);
98 * No more coded data in buffer,
99 * just move remainder into place.
101 i = strlen(p_in); /* How much left */
102 memmove(p_out, p_in, i);
103 p_in += i; p_out += i;
106 /* MIME header init found at location p */
108 /* There are some uncoded chars at the beginning. */
110 memmove(p_out, p_in, i);
114 state = S_SKIP_MIMEINIT;
118 case S_SKIP_MIMEINIT:
119 /* Mime type definition: "charset?encoding?" */
120 p = strchr(p_in, '?');
122 /* p_in .. (p-1) holds the charset */
124 /* *(p+1) is the transfer encoding, *(p+2) must be a '?' */
126 enc = tolower(*(p+1));
131 state = S_COPY_PLAIN;
134 state = S_COPY_PLAIN; /* Invalid data */
138 p = strstr(p_in, MIMEHDR_END); /* Find end of coded data */
139 if (p == NULL) p = p_in + strlen(p_in);
140 for (; (p_in < p); ) {
141 /* Decode all encoded data */
144 /* Decode one char qp-coded at (p_in+1) and (p_in+2) */
145 if (qp_char(*(p_in+1), *(p_in+2), p_out) == 0)
148 /* Invalid QP data - pass through unchanged. */
153 else if (*p_in == '_') {
155 * RFC 2047: '_' inside encoded word represents 0x20.
156 * NOT a space - always the value 0x20.
168 else if (enc == 'b') {
169 /* Decode base64 encoded data */
173 delimsave = *p; *p = '\r';
174 decoded_count = from64tobits(p_out, p_in, 0);
176 if (decoded_count > 0)
177 p_out += decoded_count;
188 p_in += 2; /* Skip the MIMEHDR_END delimiter */
191 * We've completed decoding one encoded sequence. But another
192 * may follow immediately, in which case whitespace before the
193 * new MIMEHDR_INIT delimiter must be discarded.
194 * See if that is the case
196 p = strstr(p_in, MIMEHDR_INIT);
197 state = S_COPY_PLAIN;
200 * There is more MIME data later on. Is there
201 * whitespace only before the delimiter?
206 for (q=p_in; (wsp_only && (q < p)); q++)
207 wsp_only = isspace(*q);
211 * Whitespace-only before the MIME delimiter. OK,
212 * just advance p_in to past the new MIMEHDR_INIT,
213 * and prepare to process the new MIME charset/encoding
216 p_in = p + sizeof(MIMEHDR_INIT) - 1;
217 state = S_SKIP_MIMEINIT;
230 * Routines for decoding body-parts of a message.
232 * Since the "fetch" part of fetchmail gets a message body
233 * one line at a time, we need to maintain some state variables
234 * across multiple invokations of the UnMimeBodyline() routine.
235 * The driver routine should call MimeBodyType() when all
236 * headers have been received, and then UnMimeBodyline() for
237 * every line in the message body.
240 #define S_BODY_DATA 0
244 * Flag indicating if we are currently processing
245 * the headers or the body of a (multipart) message.
247 static int BodyState = S_BODY_DATA;
250 * Flag indicating if we are in the process of decoding
251 * a quoted-printable body part.
253 static int CurrEncodingIsQP = 0;
254 static int CurrTypeNeedsDecode = 0;
257 * Delimiter for multipart messages. RFC 2046 states that this must
258 * NEVER be longer than 70 characters. Add 3 for the two hyphens
259 * at the beginning, and a terminating null.
261 #define MAX_DELIM_LEN 70
262 static unsigned char MultipartDelimiter[MAX_DELIM_LEN+3];
265 /* This string replaces the "Content-Transfer-Encoding: quoted-printable"
266 * string in all headers, including those in body-parts. The replacement
267 * must be no longer than the original string.
269 static const char ENC8BIT[] = "Content-Transfer-Encoding: 8bit";
270 static void SetEncoding8bit(unsigned char *XferEncOfs)
274 if (XferEncOfs != NULL) {
275 memcpy(XferEncOfs, ENC8BIT, sizeof(ENC8BIT) - 1);
277 /* If anything left, in this header, replace with whitespace */
278 for (p=XferEncOfs+sizeof(ENC8BIT)-1; (*p >= ' '); p++) *p=' ';
282 static char *GetBoundary(char *CntType)
287 /* Find the "boundary" delimiter. It must be preceded with a ';'
288 * and optionally some whitespace.
292 p2 = strchr(p1, ';');
294 for (p2++; isspace(*p2); p2++);
297 } while ((p1) && (strncasecmp(p1, "boundary", 8) != 0));
300 /* No boundary delimiter */
303 /* Skip "boundary", whitespace and '='; check that we do have a '=' */
304 for (p1+=8, flag=0; (isspace(*p1) || (*p1 == '=')); p1++)
305 flag |= (*p1 == '=');
309 /* Find end of boundary delimiter string */
311 /* The delimiter is inside quotes */
313 p2 = strchr(p1, '\"');
315 return NULL; /* No closing '"' !?! */
318 /* There might be more text after the "boundary" string. */
319 p2 = strchr(p1, ';'); /* Safe - delimiter with ';' must be in quotes */
322 /* Zero-terminate the boundary string */
326 return (p1 && strlen(p1)) ? p1 : NULL;
330 int CheckContentType(char *CntType)
333 * Static array of Content-Type's for which we will do
334 * quoted-printable decoding, if requested.
335 * It is probably wise to do this only on known text-only types;
336 * be really careful if you change this.
339 static char *DecodedTypes[] = {
340 "text/", /* Will match ALL content-type's starting with 'text/' */
348 /* If no Content-Type header, it isn't MIME - don't touch it */
349 if (CntType == NULL) return 0;
351 /* Skip whitespace, if any */
352 for (; isspace(*p); p++) ;
356 (strncasecmp(p, DecodedTypes[i], strlen(DecodedTypes[i]))));
359 return (DecodedTypes[i] != NULL);
364 * This routine does three things:
365 * 1) It determines - based on the message headers - whether the
366 * message body is a MIME message that may hold 8 bit data.
367 * - A message that has a "quoted-printable" or "8bit" transfer
368 * encoding is assumed to contain 8-bit data (when decoded).
369 * - A multipart message is assumed to contain 8-bit data
370 * when decoded (there might be quoted-printable body-parts).
371 * - All other messages are assumed NOT to include 8-bit data.
372 * 2) It determines the delimiter-string used in multi-part message
374 * 3) It sets the initial values of the CurrEncodingIsQP,
375 * CurrTypeNeedsDecode, and BodyState variables, from the header
378 * The return value is a bitmask.
380 int MimeBodyType(unsigned char *hdrs, int WantDecode)
382 unsigned char *NxtHdr = hdrs;
383 unsigned char *XferEnc, *XferEncOfs, *CntType, *MimeVer, *p;
384 int HdrsFound = 0; /* We only look for three headers */
385 int BodyType; /* Return value */
387 /* Setup for a standard (no MIME, no QP, 7-bit US-ASCII) message */
388 MultipartDelimiter[0] = '\0';
389 CurrEncodingIsQP = CurrTypeNeedsDecode = 0;
390 BodyState = S_BODY_DATA;
393 /* Just in case ... */
397 XferEnc = XferEncOfs = CntType = MimeVer = NULL;
400 if (strncasecmp("Content-Transfer-Encoding:", NxtHdr, 26) == 0) {
404 xalloca(XferEnc, char *, strlen(p) + 1);
409 else if (strncasecmp("Content-Type:", NxtHdr, 13) == 0) {
411 * This one is difficult. We cannot use the standard
412 * nxtaddr() routine, since the boundary-delimiter is
413 * (probably) enclosed in quotes - and thus appears
414 * as an rfc822 comment, and nxtaddr() "eats" up any
415 * spaces in the delimiter. So, we have to do this
419 /* Skip the "Content-Type:" part and whitespace after it */
420 for (NxtHdr += 13; ((*NxtHdr == ' ') || (*NxtHdr == '\t')); NxtHdr++);
423 * Get the full value of the Content-Type header;
424 * it might span multiple lines. So search for
425 * a newline char, but ignore those that have a
426 * have a TAB or space just after the NL (continued
431 p=strchr((p+1),'\n');
432 } while ( (p != NULL) && ((*(p+1) == '\t') || (*(p+1) == ' ')) );
433 if (p == NULL) p = NxtHdr + strlen(NxtHdr);
435 xalloca(CntType, char *, p-NxtHdr+2);
436 strncpy(CntType, NxtHdr, (p-NxtHdr));
437 *(CntType+(p-NxtHdr)) = '\0';
440 else if (strncasecmp("MIME-Version:", NxtHdr, 13) == 0) {
443 xalloca(MimeVer, char *, strlen(p) + 1);
449 NxtHdr = (strchr(NxtHdr, '\n'));
450 if (NxtHdr != NULL) NxtHdr++;
451 } while ((NxtHdr != NULL) && (*NxtHdr) && (HdrsFound != 3));
454 /* Done looking through the headers, now check what they say */
455 if ((MimeVer != NULL) && (strcmp(MimeVer, "1.0") == 0)) {
457 CurrTypeNeedsDecode = CheckContentType(CntType);
459 /* Check Content-Type to see if this is a multipart message */
460 if ( (CntType != NULL) &&
461 ((strncasecmp(CntType, "multipart/mixed", 16) == 0) ||
462 (strncasecmp(CntType, "message/", 8) == 0)) ) {
464 char *p1 = GetBoundary(CntType);
467 /* The actual delimiter is "--" followed by
468 the boundary string */
469 strcpy(MultipartDelimiter, "--");
470 strncat(MultipartDelimiter, p1, MAX_DELIM_LEN);
471 BodyType = (MSG_IS_8BIT | MSG_NEEDS_DECODE);
476 * Check Content-Transfer-Encoding, but
477 * ONLY for non-multipart messages (BodyType == 0).
479 if ((XferEnc != NULL) && (BodyType == 0)) {
480 if (strcasecmp(XferEnc, "quoted-printable") == 0) {
481 CurrEncodingIsQP = 1;
482 BodyType = (MSG_IS_8BIT | MSG_NEEDS_DECODE);
483 if (WantDecode && CurrTypeNeedsDecode) {
484 SetEncoding8bit(XferEncOfs);
487 else if (strcasecmp(XferEnc, "7bit") == 0) {
488 CurrEncodingIsQP = 0;
489 BodyType = (MSG_IS_7BIT);
491 else if (strcasecmp(XferEnc, "8bit") == 0) {
492 CurrEncodingIsQP = 0;
493 BodyType = (MSG_IS_8BIT);
504 * Decode one line of data containing QP data.
505 * Return flag set if this line ends with a soft line-break.
506 * 'bufp' is modified to point to the end of the output buffer.
508 static int DoOneQPLine(unsigned char **bufp, flag delimited, flag issoftline)
510 unsigned char *buf = *bufp;
511 unsigned char *p_in, *p_out, *p;
516 * Special case: line consists of a single =2E and messages are
517 * dot-terminated. Line has to be dot-stuffed after decoding.
519 if (delimited && !issoftline && buf[0]=='=' && !strncmp(*bufp, "=2E\r\n", 5))
521 strcpy(buf, "..\r\n");
527 if (delimited && issoftline && (strncmp(buf, "..", 2) == 0))
530 for (p_out = buf; (*p_in); ) {
531 p = strchr(p_in, '=');
533 /* No more QP data, just move remainder into place */
535 memmove(p_out, p_in, n);
536 p_in += n; p_out += n;
540 /* There are some uncoded chars at the beginning. */
542 memmove(p_out, p_in, n);
547 case '\0': case '\r': case '\n':
548 /* Soft line break, skip '=' */
550 if (*p_in == '\r') p_in++;
551 if (*p_in == '\n') p_in++;
556 /* There is a QP encoded byte */
557 if (qp_char(*(p+1), *(p+2), p_out) == 0) {
561 /* Invalid QP data - pass through unchanged. */
577 /* This is called once per line in the message body. We need to scan
578 * all lines in the message body for the multipart delimiter string,
579 * and handle any body-part headers in such messages (these can toggle
580 * qp-decoding on and off).
582 * Note: Messages that are NOT multipart-messages go through this
583 * routine quickly, since BodyState will always be S_BODY_DATA,
584 * and MultipartDelimiter is NULL.
586 * Return flag set if this line ends with a soft line-break.
587 * 'bufp' is modified to point to the end of the output buffer.
590 int UnMimeBodyline(unsigned char **bufp, flag delimited, flag softline)
592 unsigned char *buf = *bufp;
597 UnMimeHeader(buf); /* Headers in body-parts can be encoded, too! */
598 if ((*buf == '\0') || (*buf == '\n') || (strcmp(buf, "\r\n") == 0)) {
599 BodyState = S_BODY_DATA;
601 else if (strncasecmp("Content-Transfer-Encoding:", buf, 26) == 0) {
604 XferEnc = nxtaddr(buf);
605 if ((XferEnc != NULL) && (strcasecmp(XferEnc, "quoted-printable") == 0)) {
606 CurrEncodingIsQP = 1;
609 * Hmm ... we cannot be really sure that CurrTypeNeedsDecode
610 * has been set - we may not have seen the Content-Type header
611 * yet. But *usually* the Content-Type header comes first, so
612 * this will work. And there is really no way of doing it
613 * "right" as long as we stick with the line-by-line processing.
615 if (CurrTypeNeedsDecode)
616 SetEncoding8bit(buf);
619 else if (strncasecmp("Content-Type:", buf, 13) == 0) {
620 CurrTypeNeedsDecode = CheckContentType(nxtaddr(buf));
623 *bufp = (buf + strlen(buf));
627 if ((*MultipartDelimiter) &&
628 (strncmp(buf, MultipartDelimiter, strlen(MultipartDelimiter)) == 0)) {
629 BodyState = S_BODY_HDR;
630 CurrEncodingIsQP = CurrTypeNeedsDecode = 0;
633 if (CurrEncodingIsQP && CurrTypeNeedsDecode)
634 ret = DoOneQPLine(bufp, delimited, softline);
636 *bufp = (buf + strlen(buf));
648 char *program_name = "unmime";
651 #define BUFSIZE_INCREMENT 4096
654 #define DBG_FWRITE(B,L,BS,FD) fwrite(B, L, BS, FD)
656 #define DBG_FWRITE(B,L,BS,FD)
659 int main(int argc, char *argv[])
661 unsigned int BufSize;
662 unsigned char *buffer, *buf_p;
663 int nl_count, i, bodytype;
667 FILE *fd_orig, *fd_conv;
671 sprintf(fnam, "/tmp/i_unmime.%x", pid);
672 fd_orig = fopen(fnam, "w");
673 sprintf(fnam, "/tmp/o_unmime.%x", pid);
674 fd_conv = fopen(fnam, "w");
677 BufSize = BUFSIZE_INCREMENT; /* Initial size of buffer */
678 buf_p = buffer = (unsigned char *) xmalloc(BufSize);
682 i = fread(buf_p, 1, 1, stdin);
697 if ((buf_p - buffer) == BufSize) {
698 /* Buffer is full! Get more room. */
699 buffer = xrealloc(buffer, BufSize+BUFSIZE_INCREMENT);
700 buf_p = buffer + BufSize;
701 BufSize += BUFSIZE_INCREMENT;
703 } while ((i > 0) && (nl_count < 2));
706 DBG_FWRITE(buffer, strlen(buffer), 1, fd_orig);
708 UnMimeHeader(buffer);
709 bodytype = MimeBodyType(buffer, 1);
712 fwrite(buffer, i, 1, stdout);
713 DBG_FWRITE(buffer, i, 1, fd_conv);
716 buf_p = (buffer - 1);
719 i = fread(buf_p, 1, 1, stdin);
720 } while ((i == 1) && (*buf_p != '\n'));
723 DBG_FWRITE(buf, (buf_p - buffer), 1, fd_orig);
725 if (buf_p > buffer) {
726 if (bodytype & MSG_NEEDS_DECODE) {
728 UnMimeBodyline(&buf_p, 0);
730 fwrite(buffer, (buf_p - buffer), 1, stdout);
731 DBG_FWRITE(buffer, (buf_p - buffer), 1, fd_conv);
733 } while (buf_p > buffer);