chiark / gitweb /
Leave a bit of headroom above test port number, since we go at least
[disorder] / libtests / t-mime.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2005, 2007, 2008 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 3 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,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU 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, see <http://www.gnu.org/licenses/>.
17  */
18 #include "test.h"
19
20 static int test_multipart_callback(const char *s, void *u) {
21   struct vector *parts = u;
22
23   vector_append(parts, (char *)s);
24   return 0;
25 }
26
27 static int header_callback(const char *name, const char *value,
28                            void *u) {
29   hash *const h = u;
30
31   hash_add(h, name, &value, HASH_INSERT);
32   return 0;
33 }
34
35 static void test_mime(void) {
36   char *t, *n, *v;
37   struct vector parts[1];
38   struct kvp *k;
39   const char *s, *cs, *enc;
40   hash *h;
41
42   t = 0;
43   k = 0;
44   insist(!mime_content_type("text/plain", &t, &k));
45   check_string(t, "text/plain");
46   insist(k == 0);
47
48   insist(mime_content_type("TEXT ((broken) comment", &t, &k) < 0);
49   insist(mime_content_type("TEXT ((broken) comment\\", &t, &k) < 0);
50   
51   t = 0;
52   k = 0;
53   insist(!mime_content_type("TEXT ((nested)\\ comment) /plain", &t, &k));
54   check_string(t, "text/plain");
55   insist(k == 0);
56
57   t = 0;
58   k = 0;
59   insist(!mime_content_type(" text/plain ; Charset=\"utf-\\8\"", &t, &k));
60   check_string(t, "text/plain");
61   insist(k != 0);
62   insist(k->next == 0);
63   check_string(k->name, "charset");
64   check_string(k->value, "utf-8");
65
66   t = 0;
67   k = 0;
68   insist(!mime_content_type("text/plain;charset = ISO-8859-1 ", &t, &k));
69   insist(k != 0);
70   insist(k->next == 0);
71   check_string(t, "text/plain");
72   check_string(k->name, "charset");
73   check_string(k->value, "ISO-8859-1");
74
75   t = n = v = 0;
76   insist(!mime_rfc2388_content_disposition("form-data; name=\"field1\"", &t, &n, &v));
77   check_string(t, "form-data");
78   check_string(n, "name");
79   check_string(v, "field1");
80
81   insist(!mime_rfc2388_content_disposition("inline", &t, &n, &v));
82   check_string(t, "inline");
83   insist(n == 0);
84   insist(v == 0);
85
86   /* Current versions of the code only understand a single arg to these
87    * headers.  This is a bug at the level they work at but suffices for
88    * DisOrder's current purposes. */
89
90   insist(!mime_rfc2388_content_disposition(
91               "attachment; filename=genome.jpeg;\n"
92               "modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"",
93          &t, &n, &v));
94   check_string(t, "attachment");
95   check_string(n, "filename");
96   check_string(v, "genome.jpeg");
97
98   vector_init(parts);
99   insist(mime_multipart("--outer\r\n"
100                         "Content-Type: text/plain\r\n"
101                         "Content-Disposition: inline\r\n"
102                         "Content-Description: text-part-1\r\n"
103                         "\r\n"
104                         "Some text goes here\r\n"
105                         "\r\n"
106                         "--outer\r\n"
107                         "Content-Type: multipart/mixed; boundary=inner\r\n"
108                         "Content-Disposition: attachment\r\n"
109                         "Content-Description: multipart-2\r\n"
110                         "\r\n"
111                         "--inner\r\n"
112                         "Content-Type: text/plain\r\n"
113                         "Content-Disposition: inline\r\n"
114                         "Content-Description: text-part-2\r\n"
115                         "\r\n"
116                         "Some more text here.\r\n"
117                         "\r\n"
118                         "--inner\r\n"
119                         "Content-Type: image/jpeg\r\n"
120                         "Content-Disposition: attachment\r\n"
121                         "Content-Description: jpeg-1\r\n"
122                         "\r\n"
123                         "<jpeg data>\r\n"
124                         "--inner--\r\n"
125                         "--outer--\r\n",
126                         test_multipart_callback,
127                         "outer",
128                         parts) == 0);
129   check_integer(parts->nvec, 2);
130   check_string(parts->vec[0],
131                "Content-Type: text/plain\r\n"
132                "Content-Disposition: inline\r\n"
133                "Content-Description: text-part-1\r\n"
134                "\r\n"
135                "Some text goes here\r\n");
136   check_string(parts->vec[1],
137                "Content-Type: multipart/mixed; boundary=inner\r\n"
138                "Content-Disposition: attachment\r\n"
139                "Content-Description: multipart-2\r\n"
140                "\r\n"
141                "--inner\r\n"
142                "Content-Type: text/plain\r\n"
143                "Content-Disposition: inline\r\n"
144                "Content-Description: text-part-2\r\n"
145                "\r\n"
146                "Some more text here.\r\n"
147                "\r\n"
148                "--inner\r\n"
149                "Content-Type: image/jpeg\r\n"
150                "Content-Disposition: attachment\r\n"
151                "Content-Description: jpeg-1\r\n"
152                "\r\n"
153                "<jpeg data>\r\n"
154                "--inner--");
155   /* No trailing CRLF is _correct_ - see RFC2046 5.1.1 note regarding CRLF
156    * preceding the boundary delimiter line.  An implication of this is that we
157    * must cope with partial lines at the end of the input when recursively
158    * decomposing a multipart message. */
159   vector_init(parts);
160   insist(mime_multipart("--inner\r\n"
161                         "Content-Type: text/plain\r\n"
162                         "Content-Disposition: inline\r\n"
163                         "Content-Description: text-part-2\r\n"
164                         "\r\n"
165                         "Some more text here.\r\n"
166                         "\r\n"
167                         "--inner\r\n"
168                         "Content-Type: image/jpeg\r\n"
169                         "Content-Disposition: attachment\r\n"
170                         "Content-Description: jpeg-1\r\n"
171                         "\r\n"
172                         "<jpeg data>\r\n"
173                         "--inner--",
174                         test_multipart_callback,
175                         "inner",
176                         parts) == 0);
177   check_integer(parts->nvec, 2);
178   check_string(parts->vec[0],
179                "Content-Type: text/plain\r\n"
180                "Content-Disposition: inline\r\n"
181                "Content-Description: text-part-2\r\n"
182                "\r\n"
183                "Some more text here.\r\n");
184   check_string(parts->vec[1],
185                "Content-Type: image/jpeg\r\n"
186                "Content-Disposition: attachment\r\n"
187                "Content-Description: jpeg-1\r\n"
188                "\r\n"
189                "<jpeg data>");
190
191   /* Bogus inputs to mime_multipart() */
192   fprintf(stderr, "expect two mime_multipart errors:\n");
193   insist(mime_multipart("--inner\r\n"
194                         "Content-Type: text/plain\r\n"
195                         "Content-Disposition: inline\r\n"
196                         "Content-Description: text-part-2\r\n"
197                         "\r\n"
198                         "Some more text here.\r\n"
199                         "\r\n"
200                         "--inner\r\n"
201                         "Content-Type: image/jpeg\r\n"
202                         "Content-Disposition: attachment\r\n"
203                         "Content-Description: jpeg-1\r\n"
204                         "\r\n"
205                         "<jpeg data>\r\n",
206                         test_multipart_callback,
207                         "inner",
208                         parts) == -1);
209   insist(mime_multipart("--wrong\r\n"
210                         "Content-Type: text/plain\r\n"
211                         "Content-Disposition: inline\r\n"
212                         "Content-Description: text-part-2\r\n"
213                         "\r\n"
214                         "Some more text here.\r\n"
215                         "\r\n"
216                         "--inner--\r\n",
217                         test_multipart_callback,
218                         "inner",
219                         parts) == -1);
220     
221   /* XXX mime_parse */
222
223   check_string(mime_qp(""), "");
224   check_string(mime_qp("foobar"), "foobar");
225   check_string(mime_qp("foo=20bar"), "foo bar");
226   check_string(mime_qp("x \r\ny"), "x\r\ny");
227   check_string(mime_qp("x=\r\ny"), "xy");
228   check_string(mime_qp("x= \r\ny"), "xy");
229   check_string(mime_qp("x =\r\ny"), "x y");
230   check_string(mime_qp("x = \r\ny"), "x y");
231
232   check_string(mime_to_qp(""), "");
233   check_string(mime_to_qp("foobar\n"), "foobar\n");
234   check_string(mime_to_qp("foobar \n"), "foobar=20\n");
235   check_string(mime_to_qp("foobar\t\n"), "foobar=09\n"); 
236   check_string(mime_to_qp("foobar \t \n"), "foobar=20=09=20\n");
237   check_string(mime_to_qp(" foo=bar"), " foo=3Dbar\n");
238   check_string(mime_to_qp("copyright \xC2\xA9"), "copyright =C2=A9\n");
239   check_string(mime_to_qp("foo\nbar\nbaz\n"), "foo\nbar\nbaz\n");
240   check_string(mime_to_qp("wibble wobble wibble wobble wibble wobble wibble wobble wibble wobble wibble"), "wibble wobble wibble wobble wibble wobble wibble wobble wibble wobble wibb=\nle\n");
241  
242   /* from RFC2045 */
243   check_string(mime_qp("Now's the time =\r\n"
244 "for all folk to come=\r\n"
245 " to the aid of their country."),
246                "Now's the time for all folk to come to the aid of their country.");
247
248 #define check_base64(encoded, decoded) do {                     \
249     size_t ns;                                                  \
250     check_string(mime_base64(encoded, &ns), decoded);           \
251     insist(ns == (sizeof decoded) - 1);                         \
252     check_string(mime_to_base64((const uint8_t *)decoded,       \
253                                          (sizeof decoded) - 1), \
254                  encoded);                                      \
255   } while(0)
256     
257   
258   check_base64("",  "");
259   check_base64("BBBB", "\x04\x10\x41");
260   check_base64("////", "\xFF\xFF\xFF");
261   check_base64("//BB", "\xFF\xF0\x41");
262   check_base64("BBBB//BB////",
263              "\x04\x10\x41" "\xFF\xF0\x41" "\xFF\xFF\xFF");
264   check_base64("BBBBBA==",
265                "\x04\x10\x41" "\x04");
266   check_base64("BBBBBBA=",
267                "\x04\x10\x41" "\x04\x10");
268
269   /* Check that decoding handles various kinds of rubbish OK */
270   check_string(mime_base64("B B B B  / / B B / / / /", 0),
271              "\x04\x10\x41" "\xFF\xF0\x41" "\xFF\xFF\xFF");
272   check_string(mime_base64("B\r\nBBB.// B-B//~//", 0),
273                "\x04\x10\x41" "\xFF\xF0\x41" "\xFF\xFF\xFF");
274   check_string(mime_base64("BBBB BB==", 0),
275                "\x04\x10\x41" "\x04");
276   check_string(mime_base64("BBBB BB = =", 0),
277                "\x04\x10\x41" "\x04");
278   check_string(mime_base64("BBBB BBB=", 0),
279                "\x04\x10\x41" "\x04\x10");
280   check_string(mime_base64("BBBB BBB = ", 0),
281                "\x04\x10\x41" "\x04\x10");
282   check_string(mime_base64("BBBB=", 0),
283                "\x04\x10\x41");
284   check_string(mime_base64("BBBBBB==", 0),
285                "\x04\x10\x41" "\x04");
286   check_string(mime_base64("BBBBBBB=", 0),
287                "\x04\x10\x41" "\x04\x10");
288   /* Not actually valid base64 */
289   check_string(mime_base64("BBBBx=", 0),
290                "\x04\x10\x41");
291
292   h = hash_new(sizeof (char *));
293   s = mime_parse("From: sender@example.com\r\n"
294                  "To: rcpt@example.com\r\n"
295                  "Subject: test #1\r\n"
296                  "\r\n"
297                  "body\r\n",
298                  header_callback, h);
299   insist(s != 0);
300   check_string(*(char **)hash_find(h, "from"), "sender@example.com");
301   check_string(*(char **)hash_find(h, "to"), "rcpt@example.com");
302   check_string(*(char **)hash_find(h, "subject"), "test #1");
303   check_string(s, "body\r\n");
304
305   h = hash_new(sizeof (char *));
306   s = mime_parse("FROM: sender@example.com\r\n"
307                  "TO: rcpt@example.com\r\n"
308                  "SUBJECT: test #1\r\n"
309                  "CONTENT-TRANSFER-ENCODING: 7bit\r\n"
310                  "\r\n"
311                  "body\r\n",
312                  header_callback, h);
313   insist(s != 0);
314   check_string(*(char **)hash_find(h, "from"), "sender@example.com");
315   check_string(*(char **)hash_find(h, "to"), "rcpt@example.com");
316   check_string(*(char **)hash_find(h, "subject"), "test #1");
317   check_string(*(char **)hash_find(h, "content-transfer-encoding"), "7bit");
318   check_string(s, "body\r\n");
319
320   h = hash_new(sizeof (char *));
321   s = mime_parse("From: sender@example.com\r\n"
322                  "To:    \r\n"
323                  "     rcpt@example.com\r\n"
324                  "Subject: test #1\r\n"
325                  "MIME-Version: 1.0\r\n"
326                  "Content-Type: text/plain\r\n"
327                  "Content-Transfer-Encoding: BASE64\r\n"
328                  "\r\n"
329                  "d2liYmxlDQo=\r\n",
330                  header_callback, h);
331   insist(s != 0); 
332   check_string(*(char **)hash_find(h, "from"), "sender@example.com");
333   check_string(*(char **)hash_find(h, "to"), "rcpt@example.com");
334   check_string(*(char **)hash_find(h, "subject"), "test #1");
335   check_string(*(char **)hash_find(h, "mime-version"), "1.0");
336   check_string(*(char **)hash_find(h, "content-type"), "text/plain");
337   check_string(*(char **)hash_find(h, "content-transfer-encoding"), "BASE64");
338   check_string(s, "wibble\r\n");
339
340 #define CHECK_QUOTE(INPUT, EXPECT) do {                 \
341   s = quote822(INPUT, 0);                               \
342   insist(s != 0);                                       \
343   check_string(s, EXPECT);                              \
344   s = mime_parse_word(s, &t, mime_http_separator);      \
345   check_string(t, INPUT);                               \
346 } while(0)
347   CHECK_QUOTE("wibble", "wibble");
348   CHECK_QUOTE("wibble spong", "\"wibble spong\"");
349   CHECK_QUOTE("wibble\\spong", "\"wibble\\\\spong\"");
350   CHECK_QUOTE("wibble\"spong", "\"wibble\\\"spong\"");
351   CHECK_QUOTE("(wibble)", "\"(wibble)\"");
352
353   s = mime_encode_text("wibble\n", &cs, &enc);
354   insist(s != 0);
355   check_string(s, "wibble\n");
356   check_string(cs, "us-ascii");
357   check_string(enc, "7bit");
358
359   s = mime_encode_text("wibble\xC3\xB7\n", &cs, &enc);
360   insist(s != 0);
361   check_string(s, "wibble=C3=B7\n");
362   check_string(cs, "utf-8");
363   check_string(enc, "quoted-printable");
364 }
365
366 TEST(mime);
367
368 /*
369 Local Variables:
370 c-basic-offset:2
371 comment-column:40
372 fill-column:79
373 indent-tabs-mode:nil
374 End:
375 */