chiark / gitweb /
Include a Date: header in mail sent by the CGI. We always use UTC
[disorder] / lib / mime.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2005, 2007 Richard Kettlewell
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18  * USA
19  */
20 /** @file lib/mime.c
21  * @brief Support for MIME and allied protocols
22  */
23
24 #include <config.h>
25 #include "types.h"
26
27 #include <string.h>
28 #include <ctype.h>
29
30 #include <stdio.h>
31
32 #include "mem.h"
33 #include "mime.h"
34 #include "vector.h"
35 #include "hex.h"
36 #include "log.h"
37 #include "base64.h"
38
39 /** @brief Match whitespace characters */
40 static int whitespace(int c) {
41   switch(c) {
42   case ' ':
43   case '\t':
44   case '\r':
45   case '\n':
46     return 1;
47   default:
48     return 0;
49   }
50 }
51
52 /** @brief Match RFC2045 tspecial characters */
53 static int tspecial(int c) {
54   switch(c) {
55   case '(':
56   case ')':
57   case '<':
58   case '>':
59   case '@':
60   case ',':
61   case ';':
62   case ':':
63   case '\\':
64   case '"':
65   case '/':
66   case '[':
67   case ']':
68   case '?':
69   case '=':
70     return 1;
71   default:
72     return 0;
73   }
74 }
75
76 /** @brief Match RFC2616 seprator characters */
77 static int http_separator(int c) {
78   switch(c) {
79   case '(':
80   case ')':
81   case '<':
82   case '>':
83   case '@':
84   case ',':
85   case ';':
86   case ':':
87   case '\\':
88   case '"':
89   case '/':
90   case '[':
91   case ']':
92   case '?':
93   case '=':
94   case '{':
95   case '}':
96   case ' ':
97   case '\t':
98     return 1;
99   default:
100     return 0;
101   }
102 }
103
104 /** @brief Match CRLF */
105 static int iscrlf(const char *ptr) {
106   return ptr[0] == '\r' && ptr[1] == '\n';
107 }
108
109 /** @brief Skip whitespace
110  * @param s Pointer into string
111  * @param rfc822_comments If true, skip RFC822 nested comments
112  * @return Pointer into string after whitespace
113  */
114 static const char *skipwhite(const char *s, int rfc822_comments) {
115   int c, depth;
116   
117   for(;;) {
118     switch(c = *s) {
119     case ' ':
120     case '\t':
121     case '\r':
122     case '\n':
123       ++s;
124       break;
125     case '(':
126       if(!rfc822_comments)
127         return s;
128       ++s;
129       depth = 1;
130       while(*s && depth) {
131         c = *s++;
132         switch(c) {
133         case '(': ++depth; break;
134         case ')': --depth; break;
135         case '\\':
136           if(!*s) return 0;
137           ++s;
138           break;
139         }
140       }
141       if(depth) return 0;
142       break;
143     default:
144       return s;
145     }
146   }
147 }
148
149 /** @brief Test for a word character
150  * @param c Character to test
151  * @param special tspecial() (MIME/RFC2405) or http_separator() (HTTP/RFC2616)
152  * @return 1 if @p c is a word character, else 0
153  */
154 static int iswordchar(int c, int (*special)(int)) {
155   return !(c <= ' ' || c > '~' || special(c));
156 }
157
158 /** @brief Parse an RFC1521/RFC2616 word
159  * @param s Pointer to start of word
160  * @param valuep Where to store value
161  * @param special tspecial() (MIME/RFC2405) or http_separator() (HTTP/RFC2616)
162  * @return Pointer just after end of word or NULL if there's no word
163  *
164  * A word is a token or a quoted-string.
165  */
166 static const char *parseword(const char *s, char **valuep,
167                              int (*special)(int)) {
168   struct dynstr value[1];
169   int c;
170
171   dynstr_init(value);
172   if(*s == '"') {
173     ++s;
174     while((c = *s++) != '"') {
175       switch(c) {
176       case '\\':
177         if(!(c = *s++)) return 0;
178       default:
179         dynstr_append(value, c);
180         break;
181       }
182     }
183     if(!c) return 0;
184   } else {
185     if(!iswordchar((unsigned char)*s, special))
186       return NULL;
187     dynstr_init(value);
188     while(iswordchar((unsigned char)*s, special))
189       dynstr_append(value, *s++);
190   }
191   dynstr_terminate(value);
192   *valuep = value->vec;
193   return s;
194 }
195
196 /** @brief Parse an RFC1521/RFC2616 token
197  * @param s Pointer to start of token
198  * @param valuep Where to store value
199  * @param special tspecial() (MIME/RFC2405) or http_separator() (HTTP/RFC2616)
200  * @return Pointer just after end of token or NULL if there's no token
201  */
202 static const char *parsetoken(const char *s, char **valuep,
203                               int (*special)(int)) {
204   if(*s == '"') return 0;
205   return parseword(s, valuep, special);
206 }
207
208 /** @brief Parse a MIME content-type field
209  * @param s Start of field
210  * @param typep Where to store type
211  * @param parameternamep Where to store parameter name
212  * @param parametervaluep Wher to store parameter value
213  * @return 0 on success, non-0 on error
214  *
215  * See <a href="http://tools.ietf.org/html/rfc2045#section-5">RFC 2045 s5</a>.
216  */
217 int mime_content_type(const char *s,
218                       char **typep,
219                       char **parameternamep,
220                       char **parametervaluep) {
221   struct dynstr type, parametername;
222
223   dynstr_init(&type);
224   if(!(s = skipwhite(s, 1))) return -1;
225   if(!*s) return -1;
226   while(*s && !tspecial(*s) && !whitespace(*s))
227     dynstr_append(&type, tolower((unsigned char)*s++));
228   if(!(s = skipwhite(s, 1))) return -1;
229   if(*s++ != '/') return -1;
230   dynstr_append(&type, '/');
231   if(!(s = skipwhite(s, 1))) return -1;
232   while(*s && !tspecial(*s) && !whitespace(*s))
233     dynstr_append(&type, tolower((unsigned char)*s++));
234   if(!(s = skipwhite(s, 1))) return -1;
235
236   if(*s == ';') {
237     dynstr_init(&parametername);
238     ++s;
239     if(!(s = skipwhite(s, 1))) return -1;
240     if(!*s) return -1;
241     while(*s && !tspecial(*s) && !whitespace(*s))
242       dynstr_append(&parametername, tolower((unsigned char)*s++));
243     if(!(s = skipwhite(s, 1))) return -1;
244     if(*s++ != '=') return -1;
245     if(!(s = skipwhite(s, 1))) return -1;
246     if(!(s = parseword(s, parametervaluep, tspecial))) return -1;
247     if(!(s = skipwhite(s, 1))) return -1;
248     dynstr_terminate(&parametername);
249     *parameternamep = parametername.vec;
250   } else
251     *parametervaluep = *parameternamep = 0;
252   dynstr_terminate(&type);
253   *typep = type.vec;
254   return 0;
255 }
256
257 /** @brief Parse a MIME message
258  * @param s Start of message
259  * @param callback Called for each header field
260  * @param u Passed to callback
261  * @return Pointer to decoded body (might be in original string), or NULL on error
262  *
263  * This does an RFC 822-style parse and honors Content-Transfer-Encoding as
264  * described in <a href="http://tools.ietf.org/html/rfc2045#section-6">RFC 2045
265  * s6</a>.  @p callback is called for each header field encountered, in order,
266  * with ASCII characters in the header name forced to lower case.
267  */
268 const char *mime_parse(const char *s,
269                        int (*callback)(const char *name, const char *value,
270                                        void *u),
271                        void *u) {
272   struct dynstr name, value;
273   char *cte = 0, *p;
274   
275   while(*s && !iscrlf(s)) {
276     dynstr_init(&name);
277     dynstr_init(&value);
278     while(*s && !tspecial(*s) && !whitespace(*s))
279       dynstr_append(&name, tolower((unsigned char)*s++));
280     if(!(s = skipwhite(s, 1))) return 0;
281     if(*s != ':') return 0;
282     ++s;
283     while(*s && !(*s == '\n' && !(s[1] == ' ' || s[1] == '\t')))
284       dynstr_append(&value, *s++);
285     if(*s) ++s;
286     dynstr_terminate(&name);
287     dynstr_terminate(&value);
288     if(!strcmp(name.vec, "content-transfer-encoding")) {
289       cte = xstrdup(value.vec);
290       for(p = cte; *p; p++)
291         *p = tolower((unsigned char)*p);
292     }
293     if(callback(name.vec, value.vec, u)) return 0;
294   }
295   if(*s) s += 2;
296   if(cte) {
297     if(!strcmp(cte, "base64")) return mime_base64(s, 0);
298     if(!strcmp(cte, "quoted-printable")) return mime_qp(s);
299   }
300   return s;
301 }
302
303 /** @brief Match the boundary string */
304 static int isboundary(const char *ptr, const char *boundary, size_t bl) {
305   return (ptr[0] == '-'
306           && ptr[1] == '-'
307           && !strncmp(ptr + 2, boundary, bl)
308           && (iscrlf(ptr + bl + 2)
309               || (ptr[bl + 2] == '-'
310                   && ptr[bl + 3] == '-'
311                   && (iscrlf(ptr + bl + 4) || *(ptr + bl + 4) == 0))));
312 }
313
314 /** @brief Match the final boundary string */
315 static int isfinal(const char *ptr, const char *boundary, size_t bl) {
316   return (ptr[0] == '-'
317           && ptr[1] == '-'
318           && !strncmp(ptr + 2, boundary, bl)
319           && ptr[bl + 2] == '-'
320           && ptr[bl + 3] == '-'
321           && (iscrlf(ptr + bl + 4) || *(ptr + bl + 4) == 0));
322 }
323
324 /** @brief Parse a multipart MIME body
325  * @param s Start of message
326  * @param callback Callback for each part
327  * @param boundary Boundary string
328  * @param u Passed to callback
329  * @return 0 on success, non-0 on error
330  * 
331  * See <a href="http://tools.ietf.org/html/rfc2046#section-5.1">RFC 2046
332  * s5.1</a>.  @p callback is called for each part (not yet decoded in any way)
333  * in succession; you should probably call mime_parse() for each part.
334  */
335 int mime_multipart(const char *s,
336                    int (*callback)(const char *s, void *u),
337                    const char *boundary,
338                    void *u) {
339   size_t bl = strlen(boundary);
340   const char *start, *e;
341   int ret;
342
343   /* We must start with a boundary string */
344   if(!isboundary(s, boundary, bl))
345     return -1;
346   /* Keep going until we hit a final boundary */
347   while(!isfinal(s, boundary, bl)) {
348     s = strstr(s, "\r\n") + 2;
349     start = s;
350     while(!isboundary(s, boundary, bl)) {
351       if(!(e = strstr(s, "\r\n")))
352         return -1;
353       s = e + 2;
354     }
355     if((ret = callback(xstrndup(start,
356                                 s == start ? 0 : s - start - 2),
357                        u)))
358       return ret;
359   }
360   return 0;
361 }
362
363 /** @brief Parse an RFC2388-style content-disposition field
364  * @param s Start of field
365  * @param dispositionp Where to store disposition
366  * @param parameternamep Where to store parameter name
367  * @param parametervaluep Wher to store parameter value
368  * @return 0 on success, non-0 on error
369  *
370  * See <a href="http://tools.ietf.org/html/rfc2388#section-3">RFC 2388 s3</a>
371  * and <a href="http://tools.ietf.org/html/rfc2183">RFC 2183</a>.
372  */
373 int mime_rfc2388_content_disposition(const char *s,
374                                      char **dispositionp,
375                                      char **parameternamep,
376                                      char **parametervaluep) {
377   struct dynstr disposition, parametername;
378
379   dynstr_init(&disposition);
380   if(!(s = skipwhite(s, 1))) return -1;
381   if(!*s) return -1;
382   while(*s && !tspecial(*s) && !whitespace(*s))
383     dynstr_append(&disposition, tolower((unsigned char)*s++));
384   if(!(s = skipwhite(s, 1))) return -1;
385
386   if(*s == ';') {
387     dynstr_init(&parametername);
388     ++s;
389     if(!(s = skipwhite(s, 1))) return -1;
390     if(!*s) return -1;
391     while(*s && !tspecial(*s) && !whitespace(*s))
392       dynstr_append(&parametername, tolower((unsigned char)*s++));
393     if(!(s = skipwhite(s, 1))) return -1;
394     if(*s++ != '=') return -1;
395     if(!(s = skipwhite(s, 1))) return -1;
396     if(!(s = parseword(s, parametervaluep, tspecial))) return -1;
397     if(!(s = skipwhite(s, 1))) return -1;
398     dynstr_terminate(&parametername);
399     *parameternamep = parametername.vec;
400   } else
401     *parametervaluep = *parameternamep = 0;
402   dynstr_terminate(&disposition);
403   *dispositionp = disposition.vec;
404   return 0;
405 }
406
407 /** @brief Convert MIME quoted-printable
408  * @param s Quoted-printable data
409  * @return Decoded data
410  *
411  * See <a href="http://tools.ietf.org/html/rfc2045#section-6.7">RFC 2045
412  * s6.7</a>.
413  */
414 char *mime_qp(const char *s) {
415   struct dynstr d;
416   int c, a, b;
417   const char *t;
418
419   dynstr_init(&d);
420   while((c = *s++)) {
421     switch(c) {
422     case '=':
423       if((a = unhexdigitq(s[0])) != -1
424          && (b = unhexdigitq(s[1])) != -1) {
425         dynstr_append(&d, a * 16 + b);
426         s += 2;
427       } else {
428         t = s;
429         while(*t == ' ' || *t == '\t') ++t;
430         if(iscrlf(t)) {
431           /* soft line break */
432           s = t + 2;
433         } else
434           return 0;
435       }
436       break;
437     case ' ':
438     case '\t':
439       t = s;
440       while(*t == ' ' || *t == '\t') ++t;
441       if(iscrlf(t))
442         /* trailing space is always eliminated */
443         s = t;
444       else
445         dynstr_append(&d, c);
446       break;
447     default:
448       dynstr_append(&d, c);
449       break;
450     }
451   }
452   dynstr_terminate(&d);
453   return d.vec;
454 }
455
456 /** @brief Parse a RFC2109 Cookie: header
457  * @param s Header field value
458  * @param cd Where to store result
459  * @return 0 on success, non-0 on error
460  *
461  * See <a href="http://tools.ietf.org/html/rfc2109">RFC 2109</a>.
462  */
463 int parse_cookie(const char *s,
464                  struct cookiedata *cd) {
465   char *n = 0, *v = 0;
466
467   memset(cd, 0, sizeof *cd);
468   s = skipwhite(s, 0);
469   while(*s) {
470     /* Skip separators */
471     if(*s == ';' || *s == ',') {
472       ++s;
473       s = skipwhite(s, 0);
474       continue;
475     }
476     if(!(s = parsetoken(s, &n, http_separator))) return -1;
477     s = skipwhite(s, 0);
478     if(*s++ != '=') return -1;
479     s = skipwhite(s, 0);
480     if(!(s = parseword(s, &v, http_separator))) return -1;
481     if(n[0] == '$') {
482       /* Some bit of meta-information */
483       if(!strcmp(n, "$Version"))
484         cd->version = v;
485       else if(!strcmp(n, "$Path")) {
486         if(cd->ncookies > 0 && cd->cookies[cd->ncookies-1].path == 0)
487           cd->cookies[cd->ncookies-1].path = v;
488         else {
489           error(0, "redundant $Path in Cookie: header");
490           return -1;
491         }
492       } else if(!strcmp(n, "$Domain")) {
493         if(cd->ncookies > 0 && cd->cookies[cd->ncookies-1].domain == 0)
494           cd->cookies[cd->ncookies-1].domain = v;
495         else {
496           error(0, "redundant $Domain in Cookie: header");
497           return -1;
498         }
499       }
500     } else {
501       /* It's a new cookie */
502       cd->cookies = xrealloc(cd->cookies,
503                              (cd->ncookies + 1) * sizeof (struct cookie));
504       cd->cookies[cd->ncookies].name = n;
505       cd->cookies[cd->ncookies].value = v;
506       cd->cookies[cd->ncookies].path = 0;
507       cd->cookies[cd->ncookies].domain = 0;
508       ++cd->ncookies;
509     }
510     s = skipwhite(s, 0);
511     if(*s && (*s != ',' && *s != ';')) {
512       error(0, "missing separator in Cookie: header");
513       return -1;
514     }
515   }
516   return 0;
517 }
518
519 /** @brief Find a named cookie
520  * @param cd Parse cookie data
521  * @param name Name of cookie
522  * @return Cookie structure or NULL if not found
523  */
524 const struct cookie *find_cookie(const struct cookiedata *cd,
525                                  const char *name) {
526   int n;
527
528   for(n = 0; n < cd->ncookies; ++n)
529     if(!strcmp(cd->cookies[n].name, name))
530       return &cd->cookies[n];
531   return 0;
532 }
533
534 /** @brief RFC822 quoting
535  * @param s String to quote
536  * @param force If non-0, always quote
537  * @return Possibly quoted string
538  */
539 char *quote822(const char *s, int force) {
540   const char *t;
541   struct dynstr d[1];
542   int c;
543
544   if(!force) {
545     /* See if we need to quote */
546     for(t = s; (c = (unsigned char)*t); ++t) {
547       if(tspecial(c) || http_separator(c) || whitespace(c))
548         break;
549     }
550     if(*t)
551       force = 1;
552   }
553
554   if(!force)
555     return xstrdup(s);
556
557   dynstr_init(d);
558   dynstr_append(d, '"');
559   for(t = s; (c = (unsigned char)*t); ++t) {
560     if(c == '"' || c == '\\')
561       dynstr_append(d, '\\');
562     dynstr_append(d, c);
563   }
564   dynstr_append(d, '"');
565   dynstr_terminate(d);
566   return d->vec;
567 }
568
569 /** @brief Return true if @p ptr points at trailing space */
570 static int is_trailing_space(const char *ptr) {
571   if(*ptr == ' ' || *ptr == '\t') {
572     while(*ptr == ' ' || *ptr == '\t')
573       ++ptr;
574     return *ptr == '\n' || *ptr == 0;
575   } else
576     return 0;
577 }
578
579 /** @brief Encoding text as quoted-printable
580  * @param text String to encode
581  * @return Encoded string
582  *
583  * See <a href="http://tools.ietf.org/html/rfc2045#section-6.7">RFC2045
584  * s6.7</a>.
585  */
586 char *mime_to_qp(const char *text) {
587   struct dynstr d[1];
588   int linelength = 0;                   /* length of current line */
589   char buffer[10];
590
591   dynstr_init(d);
592   /* The rules are:
593    * 1. Anything except newline can be replaced with =%02X
594    * 2. Newline, 33-60 and 62-126 stand for themselves (i.e. not '=')
595    * 3. Non-trailing space/tab stand for themselves.
596    * 4. Output lines are limited to 76 chars, with =<newline> being used
597    *    as a soft line break
598    * 5. Newlines aren't counted towards the 76 char limit.
599    */
600   while(*text) {
601     const int c = (unsigned char)*text;
602     if(c == '\n') {
603       /* Newline stands as itself */
604       dynstr_append(d, '\n');
605       linelength = 0;
606     } else if((c >= 33 && c <= 126 && c != '=')
607               || ((c == ' ' || c == '\t')
608                   && !is_trailing_space(text))) {
609       /* Things that can stand for themselves  */
610       dynstr_append(d, c);
611       ++linelength;
612     } else {
613       /* Anything else that needs encoding */
614       snprintf(buffer, sizeof buffer, "=%02X", c);
615       dynstr_append_string(d, buffer);
616       linelength += 3;
617     }
618     ++text;
619     if(linelength > 73 && *text && *text != '\n') {
620       /* Next character might overflow 76 character limit if encoded, so we
621        * insert a soft break */
622       dynstr_append_string(d, "=\n");
623       linelength = 0;
624     }
625   }
626   /* Ensure there is a final newline */
627   if(linelength)
628     dynstr_append(d, '\n');
629   /* That's all */
630   dynstr_terminate(d);
631   return d->vec;
632 }
633
634 /** @brief Encode text
635  * @param text Underlying UTF-8 text
636  * @param charsetp Where to store charset string
637  * @param encodingp Where to store encoding string
638  * @return Encoded text (might be @ref text)
639  */
640 const char *mime_encode_text(const char *text,
641                              const char **charsetp,
642                              const char **encodingp) {
643   const char *ptr;
644
645   /* See if there are in fact any non-ASCII characters */
646   for(ptr = text; *ptr; ++ptr)
647     if((unsigned char)*ptr >= 128)
648       break;
649   if(!*ptr) {
650     /* Plain old ASCII, no encoding required */
651     *charsetp = "us-ascii";
652     *encodingp = "7bit";
653     return text;
654   }
655   *charsetp = "utf-8";
656   *encodingp = "quoted-printable";
657   return mime_to_qp(text);
658 }
659
660 /*
661 Local Variables:
662 c-basic-offset:2
663 comment-column:40
664 fill-column:79
665 End:
666 */