simplify rfc822 parsing *A LOT*
[apps/madmutt.git] / lib-mime / rfc822.c
1 /*
2  *  This program is free software; you can redistribute it and/or modify
3  *  it under the terms of the GNU General Public License as published by
4  *  the Free Software Foundation; either version 2 of the License, or (at
5  *  your option) any later version.
6  *
7  *  This program is distributed in the hope that it will be useful, but
8  *  WITHOUT ANY WARRANTY; without even the implied warranty of
9  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
10  *  General Public License for more details.
11  *
12  *  You should have received a copy of the GNU General Public License
13  *  along with this program; if not, write to the Free Software
14  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
15  *  MA 02110-1301, USA.
16  *
17  *  Copyright © 2006 Pierre Habouzit
18  */
19
20 /*
21  * Copyright notice from original mutt:
22  * Copyright (C) 1996-2000 Michael R. Elkins <me@mutt.org>
23  *
24  * This file is part of mutt-ng, see http://www.muttng.org/.
25  * It's licensed under the GNU General Public License,
26  * please see the file GPL in the top level source directory.
27  */
28
29 #include <string.h>
30 #include <ctype.h>
31 #include <stdlib.h>
32
33 #include <lib-lib/mem.h>
34 #include <lib-lib/str.h>
35 #include <lib-lib/ascii.h>
36 #include <lib-lib/macros.h>
37
38 #include "mutt_idna.h"
39
40 const char RFC822Specials[] = "@.,:;<>[]\\\"()";
41
42 void address_wipe(address_t *addr)
43 {
44     p_delete(&addr->personal);
45     p_delete(&addr->mailbox);
46     address_delete(&addr->next);
47 }
48
49
50 void rfc822_qualify(address_t *addr, const char *host)
51 {
52     char *p;
53
54     for (; addr; addr = addr->next) {
55         if (!addr->group && addr->mailbox && !strchr(addr->mailbox, '@')) {
56             p = p_new(char, m_strlen(addr->mailbox) + m_strlen(host) + 2);
57             sprintf(p, "%s@%s", addr->mailbox, host);        /* __SPRINTF_CHECKED__ */
58             p_delete(&addr->mailbox);
59             addr->mailbox = p;
60         }
61     }
62 }
63
64 address_t *address_dup(address_t *addr)
65 {
66     address_t *res = address_new();
67
68     res->personal = m_strdup(addr->personal);
69     res->mailbox  = m_strdup(addr->mailbox);
70     res->group    = addr->group;
71     return res;
72 }
73
74 address_t *address_list_dup(address_t *addr)
75 {
76     address_t *res = NULL, **resp = &res;
77
78     for (; addr; addr = addr->next) {
79         *resp = address_dup(addr);
80         resp = &(*resp)->next;
81     }
82
83     return res;
84 }
85
86
87 static void rfc822_dequote_comment(char *s)
88 {
89     char *w = s;
90
91     for (; *s; s++) {
92         if (*s == '\\') {
93             /* if *++s is NUL that's an error, but we don't care */
94             *w++ = *++s;
95         } else
96         if (*s != '\"') {
97             *w++ = *s;
98         }
99     }
100     *w = 0;
101 }
102
103
104 /****************************************************************************/
105 /* Parsing functions                                                        */
106 /****************************************************************************/
107
108 struct rfc822_parse_ctx {
109     address_t *cur;
110
111     char comment[STRING];
112     size_t commentlen;
113
114     char phrase[STRING];
115     size_t phraselen;
116 };
117
118 #define is_special(x) strchr(RFC822Specials,x)
119 #define terminate_string(a, b, c)  (a[MIN(b, c)] = 0)
120 #define terminate_buffer(a)        terminate_string(a, a##len, sizeof (a) - 1)
121
122
123 static const char *
124 parse_comment(const char *s, char *comment, size_t *commentlen,
125               size_t commentmax)
126 {
127     int level = 1;
128
129     for (; *s; s++) {
130         switch (*s) {
131           case '(':
132             level++;
133             break;
134
135           case ')':
136             level--;
137             if (!level)
138                 return s;
139             break;
140
141           case '\\':
142             s++; /* if *++s is NUL it will be an error anyway */
143             break;
144
145           default:
146             break;
147         }
148
149         if (*commentlen < commentmax)
150             comment[(*commentlen)++] = *s;
151     }
152
153     return NULL;
154 }
155
156 static const char *
157 parse_quote(const char *s, char *token, size_t *tokenlen, size_t tokenmax)
158 {
159     if (*tokenlen < tokenmax)
160         token[(*tokenlen)++] = '"';
161
162     for (; *s; s++) {
163         if (*tokenlen < tokenmax)
164             token[*tokenlen] = *s;
165
166         if (*s == '"') {
167             (*tokenlen)++;
168             return s + 1;
169         }
170
171         if (*s == '\\') {
172             if (!*++s)
173                 break;
174
175             if (*tokenlen < tokenmax)
176                 token[*tokenlen] = *s;
177         }
178         (*tokenlen)++;
179     }
180
181     return NULL;
182 }
183
184 static const char *
185 next_token(const char *s, char *token, size_t *tokenlen, size_t tokenmax)
186 {
187     if (*s == '(')
188         return parse_comment(s + 1, token, tokenlen, tokenmax);
189
190     if (*s == '"')
191         return parse_quote(s + 1, token, tokenlen, tokenmax);
192
193     if (is_special(*s)) {
194         if (*tokenlen < tokenmax)
195             token[(*tokenlen)++] = *s;
196         return s + 1;
197     }
198
199     while (*s) {
200         if (ISSPACE(*s) || is_special(*s))
201             break;
202         if (*tokenlen < tokenmax)
203             token[(*tokenlen)++] = *s;
204         s++;
205     }
206     return s;
207 }
208
209 static const char *
210 parse_mailboxdomain(const char *s, const char *nonspecial,
211                     char *mailbox, size_t *mailboxlen, size_t mailboxmax,
212                     struct rfc822_parse_ctx *ctx)
213 {
214     while (*s) {
215         s = skipspaces(s);
216
217         if (!strchr(nonspecial, *s) && is_special(*s))
218             return s;
219
220         if (*s == '(') {
221             if (ctx->commentlen && ctx->commentlen < sizeof(ctx->comment) - 1)
222                 ctx->comment[ctx->commentlen++] = ' ';
223             s = next_token(s, ctx->comment, &ctx->commentlen, sizeof(ctx->comment) - 1);
224         } else {
225             s = next_token(s, mailbox, mailboxlen, mailboxmax);
226         }
227
228         if (!s)
229             return NULL;
230     }
231
232     return s;
233 }
234
235 static const char *
236 parse_address(const char *s, struct rfc822_parse_ctx *ctx)
237 {
238     char token[STRING];
239     size_t tokenlen = 0;
240
241     s = parse_mailboxdomain(s, ".\"(\\",
242                             token, &tokenlen, sizeof(token) - 1, ctx);
243     if (!s)
244         return NULL;
245
246     if (*s == '@') {
247         if (tokenlen < sizeof(token) - 1)
248             token[tokenlen++] = '@';
249         s = parse_mailboxdomain(s + 1, ".([]\\",
250                                 token, &tokenlen, sizeof(token) - 1, ctx);
251         if (!s)
252             return NULL;
253     }
254
255     terminate_buffer(token);
256     ctx->cur->mailbox = m_strdup(token);
257
258     if (ctx->commentlen && !ctx->cur->personal) {
259         terminate_buffer(ctx->comment);
260         ctx->cur->personal = m_strdup(ctx->comment);
261     }
262
263     return s;
264 }
265
266 address_t **add_addrspec(address_t **last, struct rfc822_parse_ctx *ctx)
267 {
268     const char *s;
269
270     ctx->cur = address_new();
271     s = parse_address(ctx->phrase, ctx);
272     if (s && *s && *s != ',' && *s != ';') {
273         address_delete(&ctx->cur);
274         return last;
275     }
276
277     fprintf(stderr, "ADD [%s]\n", ctx->cur->mailbox);
278     *last = ctx->cur;
279     return &(*last)->next;
280 }
281
282 address_t *rfc822_parse_adrlist(address_t *top, const char *s)
283 {
284     struct rfc822_parse_ctx ctx = { NULL, "", 0, "", 0 };
285     int ws_pending = 0;
286     address_t **last;
287
288     last = address_list_last(&top);
289
290     for (;;) {
291         ws_pending = ISSPACE(*s);
292         s = skipspaces(s);
293
294         switch (*s) {
295           case '\0':
296             if (ctx.phraselen) {
297                 terminate_buffer(ctx.phrase);
298                 terminate_buffer(ctx.comment);
299                 last = add_addrspec(last, &ctx);
300             } else
301             if (ctx.commentlen && ctx.cur && !ctx.cur->personal) {
302                 terminate_buffer(ctx.comment);
303                 ctx.cur->personal = m_strdup(ctx.comment);
304             }
305             return top;
306
307           default:
308             if (ctx.phraselen && ctx.phraselen < sizeof(ctx.phrase) - 1 && ws_pending)
309                 ctx.phrase[ctx.phraselen++] = ' ';
310             s = next_token(s, ctx.phrase, &ctx.phraselen, sizeof(ctx.phrase) - 1);
311             if (!s) {
312                 address_delete(&top);
313                 return NULL;
314             }
315             continue;
316
317           case '(':
318             if (ctx.commentlen && ctx.commentlen < sizeof(ctx.comment) - 1)
319                 ctx.comment[ctx.commentlen++] = ' ';
320             s = next_token(s, ctx.comment, &ctx.commentlen, sizeof(ctx.comment) - 1);
321             if (!s) {
322                 address_delete (&top);
323                 return NULL;
324             }
325             continue;
326
327           case ',':
328             if (ctx.phraselen) {
329                 terminate_buffer(ctx.phrase);
330                 last = add_addrspec(last, &ctx);
331             } else
332             if (ctx.commentlen && ctx.cur && !ctx.cur->personal) {
333                 terminate_buffer(ctx.comment);
334                 ctx.cur->personal = m_strdup(ctx.comment);
335             }
336             break;
337
338           case ':':
339             terminate_buffer(ctx.phrase);
340             *last = address_new();
341             (*last)->mailbox = m_strdup(ctx.phrase);
342             (*last)->group = 1;
343             last = &(*last)->next;
344             break;
345
346           case ';':
347             if (ctx.phraselen) {
348                 terminate_buffer(ctx.phrase);
349                 last = add_addrspec(last, &ctx);
350             } else
351             if (ctx.commentlen && ctx.cur && !ctx.cur->personal) {
352                 terminate_buffer(ctx.comment);
353                 ctx.cur->personal = m_strdup(ctx.comment);
354             }
355
356             /* add group terminator */
357             *last = address_new();
358             return top;
359
360           case '<':
361             terminate_buffer(ctx.phrase);
362             ctx.cur = address_new ();
363             if (ctx.phraselen) {
364                 /* if we get something like "Michael R. Elkins" remove the quotes */
365                 rfc822_dequote_comment(ctx.phrase);
366                 ctx.cur->personal = m_strdup(ctx.phrase);
367             }
368
369             s = parse_address(skipspaces(s + 1), &ctx);
370             if (!s || *s != '>' || !ctx.cur->mailbox) {
371                 address_delete(&top);
372                 address_delete(&ctx.cur);
373                 return NULL;
374             }
375
376             *last = ctx.cur;
377             break;
378         }
379
380         ctx.commentlen = 0;
381         ctx.phraselen = 0;
382         s++;
383     }
384
385     return NULL;
386 }
387
388
389 /****************************************************************************/
390 /* Output functions                                                         */
391 /****************************************************************************/
392
393 void
394 rfc822_cat(char *buf, size_t buflen, const char *value, const char *specials)
395 {
396     if (strpbrk(value, specials)) {
397         char tmp[256], *pc = tmp;
398         size_t tmplen = sizeof (tmp) - 3;
399
400         *pc++ = '"';
401         for (; *value && tmplen > 1; value++) {
402             if (*value == '\\' || *value == '"') {
403                 *pc++ = '\\';
404                 tmplen--;
405             }
406             *pc++ = *value;
407             tmplen--;
408         }
409         *pc++ = '"';
410         *pc = 0;
411         m_strcpy(buf, buflen, tmp);
412     } else {
413         m_strcpy(buf, buflen, value);
414     }
415 }
416
417 void rfc822_write_address_single(char *buf, size_t buflen, address_t * addr,
418                                  int display)
419 {
420     size_t len;
421     char *pbuf = buf;
422     char *pc;
423
424     if (!addr)
425         return;
426
427     buflen--;                     /* save room for the terminal nul */
428
429     if (addr->personal) {
430         if (strpbrk (addr->personal, RFC822Specials)) {
431             if (!buflen)
432                 goto done;
433             *pbuf++ = '"';
434             buflen--;
435             for (pc = addr->personal; *pc && buflen > 0; pc++) {
436                 if (*pc == '"' || *pc == '\\') {
437                     if (!buflen)
438                         goto done;
439                     *pbuf++ = '\\';
440                     buflen--;
441                 }
442                 if (!buflen)
443                     goto done;
444                 *pbuf++ = *pc;
445                 buflen--;
446             }
447             if (!buflen)
448                 goto done;
449             *pbuf++ = '"';
450             buflen--;
451         }
452         else {
453             if (!buflen)
454                 goto done;
455             m_strcpy(pbuf, buflen, addr->personal);
456             len = m_strlen(pbuf);
457             pbuf += len;
458             buflen -= len;
459         }
460
461         if (!buflen)
462             goto done;
463         *pbuf++ = ' ';
464         buflen--;
465     }
466
467     if (addr->personal || (addr->mailbox && *addr->mailbox == '@')) {
468         if (!buflen)
469             goto done;
470         *pbuf++ = '<';
471         buflen--;
472     }
473
474     if (addr->mailbox) {
475         if (!buflen)
476             goto done;
477         if (ascii_strcmp (addr->mailbox, "@") && !display) {
478             m_strcpy(pbuf, buflen, addr->mailbox);
479             len = m_strlen(pbuf);
480         }
481         else if (ascii_strcmp (addr->mailbox, "@") && display) {
482             m_strcpy(pbuf, buflen, mutt_addr_for_display(addr));
483             len = m_strlen(pbuf);
484         }
485         else {
486             *pbuf = '\0';
487             len = 0;
488         }
489         pbuf += len;
490         buflen -= len;
491
492         if (addr->personal || (addr->mailbox && *addr->mailbox == '@')) {
493             if (!buflen)
494                 goto done;
495             *pbuf++ = '>';
496             buflen--;
497         }
498
499         if (addr->group) {
500             if (!buflen)
501                 goto done;
502             *pbuf++ = ':';
503             buflen--;
504             if (!buflen)
505                 goto done;
506             *pbuf++ = ' ';
507             buflen--;
508         }
509     }
510     else {
511         if (!buflen)
512             goto done;
513         *pbuf++ = ';';
514         buflen--;
515     }
516 done:
517     /* no need to check for length here since we already save space at the
518        beginning of this routine */
519     *pbuf = 0;
520 }
521
522 /* note: it is assumed that `buf' is nul terminated! */
523 void rfc822_write_address (char *buf, size_t buflen, address_t * addr,
524                            int display)
525 {
526     char *pbuf = buf;
527     size_t len = m_strlen(buf);
528
529     buflen--;                     /* save room for the terminal nul */
530
531     if (len > 0) {
532         if (len > buflen)
533             return;                   /* safety check for bogus arguments */
534
535         pbuf += len;
536         buflen -= len;
537         if (!buflen)
538             goto done;
539         *pbuf++ = ',';
540         buflen--;
541         if (!buflen)
542             goto done;
543         *pbuf++ = ' ';
544         buflen--;
545     }
546
547     for (; addr && buflen > 0; addr = addr->next) {
548         /* use buflen+1 here because we already saved space for the trailing
549            nul char, and the subroutine can make use of it */
550         rfc822_write_address_single (pbuf, buflen + 1, addr, display);
551
552         /* this should be safe since we always have at least 1 char passed into
553            the above call, which means `pbuf' should always be nul terminated */
554         len = m_strlen(pbuf);
555         pbuf += len;
556         buflen -= len;
557
558         /* if there is another address, and its not a group mailbox name or
559            group terminator, add a comma to separate the addresses */
560         if (addr->next && addr->next->mailbox && !addr->group) {
561             if (!buflen)
562                 goto done;
563             *pbuf++ = ',';
564             buflen--;
565             if (!buflen)
566                 goto done;
567             *pbuf++ = ' ';
568             buflen--;
569         }
570     }
571 done:
572     *pbuf = 0;
573 }
574