Line data Source code
1 : #include "test_helpers.h"
2 : #include "mime_util.h"
3 : #include "raii.h"
4 : #include <string.h>
5 : #include <stdlib.h>
6 : #include <time.h>
7 :
8 1 : void test_mime_util(void) {
9 :
10 : /* ── mime_get_header ───────────────────────────────────────────── */
11 :
12 : /* Basic header extraction */
13 1 : const char *msg1 =
14 : "From: Alice <alice@example.com>\r\n"
15 : "Subject: Hello World\r\n"
16 : "Date: Mon, 25 Mar 2026 10:00:00 +0000\r\n"
17 : "\r\n"
18 : "Body text here.";
19 :
20 : {
21 2 : RAII_STRING char *from = mime_get_header(msg1, "From");
22 1 : ASSERT(from != NULL, "mime_get_header: From should not be NULL");
23 1 : ASSERT(strcmp(from, "Alice <alice@example.com>") == 0, "From header value mismatch");
24 : }
25 :
26 : {
27 2 : RAII_STRING char *subject = mime_get_header(msg1, "Subject");
28 1 : ASSERT(subject != NULL, "mime_get_header: Subject should not be NULL");
29 1 : ASSERT(strcmp(subject, "Hello World") == 0, "Subject header value mismatch");
30 : }
31 :
32 : /* Header folding */
33 : {
34 1 : const char *folded =
35 : "Subject: This is a very\r\n"
36 : " long subject\r\n"
37 : "\r\n";
38 2 : RAII_STRING char *subj = mime_get_header(folded, "Subject");
39 1 : ASSERT(subj != NULL, "Folded subject should not be NULL");
40 1 : ASSERT(strcmp(subj, "This is a very long subject") == 0, "Folded subject mismatch");
41 : }
42 :
43 : /* Case-insensitive lookup */
44 : {
45 2 : RAII_STRING char *date = mime_get_header(msg1, "date");
46 1 : ASSERT(date != NULL, "mime_get_header: case-insensitive lookup should work");
47 : }
48 :
49 : /* Missing header returns NULL */
50 : {
51 2 : RAII_STRING char *cc = mime_get_header(msg1, "Cc");
52 1 : ASSERT(cc == NULL, "mime_get_header: missing header should return NULL");
53 : }
54 :
55 : /* NULL inputs */
56 1 : ASSERT(mime_get_header(NULL, "From") == NULL, "NULL msg should return NULL");
57 1 : ASSERT(mime_get_header(msg1, NULL) == NULL, "NULL name should return NULL");
58 :
59 : /* Headers stop at blank line — body should not be searched */
60 : {
61 1 : const char *msg2 =
62 : "Subject: Real\r\n"
63 : "\r\n"
64 : "Fake: Header\r\n";
65 2 : RAII_STRING char *fake = mime_get_header(msg2, "Fake");
66 1 : ASSERT(fake == NULL, "mime_get_header: should not find headers in body");
67 : }
68 :
69 : /* ── mime_get_text_body — plain text ───────────────────────────── */
70 :
71 : {
72 1 : const char *plain =
73 : "Content-Type: text/plain\r\n"
74 : "\r\n"
75 : "Simple body.";
76 2 : RAII_STRING char *body = mime_get_text_body(plain);
77 1 : ASSERT(body != NULL, "mime_get_text_body: plain should not be NULL");
78 1 : ASSERT(strstr(body, "Simple body.") != NULL, "Plain body content mismatch");
79 : }
80 :
81 : /* No Content-Type defaults to text/plain */
82 : {
83 1 : const char *no_ct =
84 : "Subject: test\r\n"
85 : "\r\n"
86 : "Hello!";
87 2 : RAII_STRING char *body = mime_get_text_body(no_ct);
88 1 : ASSERT(body != NULL, "mime_get_text_body: no Content-Type should default to plain");
89 1 : ASSERT(strstr(body, "Hello!") != NULL, "Default plain body content mismatch");
90 : }
91 :
92 : /* ── mime_get_text_body — base64 ───────────────────────────────── */
93 :
94 : /* "Hello, base64!" base64-encoded */
95 : {
96 1 : const char *b64msg =
97 : "Content-Type: text/plain\r\n"
98 : "Content-Transfer-Encoding: base64\r\n"
99 : "\r\n"
100 : "SGVsbG8sIGJhc2U2NCE=\r\n";
101 2 : RAII_STRING char *body = mime_get_text_body(b64msg);
102 1 : ASSERT(body != NULL, "mime_get_text_body: base64 should not be NULL");
103 1 : ASSERT(strstr(body, "Hello, base64!") != NULL, "Base64 decoded content mismatch");
104 : }
105 :
106 : /* ── mime_get_text_body — quoted-printable ─────────────────────── */
107 :
108 : {
109 1 : const char *qpmsg =
110 : "Content-Type: text/plain\r\n"
111 : "Content-Transfer-Encoding: quoted-printable\r\n"
112 : "\r\n"
113 : "Hello=2C=20QP=21\r\n"; /* "Hello, QP!" */
114 2 : RAII_STRING char *body = mime_get_text_body(qpmsg);
115 1 : ASSERT(body != NULL, "mime_get_text_body: QP should not be NULL");
116 1 : ASSERT(strstr(body, "Hello, QP!") != NULL, "QP decoded content mismatch");
117 : }
118 :
119 : /* ── mime_get_text_body — HTML fallback ────────────────────────── */
120 :
121 : {
122 1 : const char *html =
123 : "Content-Type: text/html\r\n"
124 : "\r\n"
125 : "<html><body><p>HTML body</p></body></html>";
126 2 : RAII_STRING char *body = mime_get_text_body(html);
127 1 : ASSERT(body != NULL, "mime_get_text_body: HTML fallback should not be NULL");
128 1 : ASSERT(strstr(body, "HTML body") != NULL, "HTML stripped content mismatch");
129 : }
130 :
131 : /* ── mime_get_text_body — multipart ────────────────────────────── */
132 :
133 : {
134 1 : const char *mp =
135 : "Content-Type: multipart/mixed; boundary=\"BOUND\"\r\n"
136 : "\r\n"
137 : "--BOUND\r\n"
138 : "Content-Type: text/plain\r\n"
139 : "\r\n"
140 : "Multipart plain text\r\n"
141 : "--BOUND--\r\n";
142 2 : RAII_STRING char *body = mime_get_text_body(mp);
143 1 : ASSERT(body != NULL, "mime_get_text_body: multipart should not be NULL");
144 1 : ASSERT(strstr(body, "Multipart plain text") != NULL, "Multipart body content mismatch");
145 : }
146 :
147 : /* NULL input */
148 1 : ASSERT(mime_get_text_body(NULL) == NULL, "NULL msg should return NULL");
149 :
150 : /* ── mime_decode_words ──────────────────────────────────────────── */
151 :
152 : /* Plain string — no encoded words, returned verbatim */
153 : {
154 2 : RAII_STRING char *r = mime_decode_words("Hello World");
155 1 : ASSERT(r != NULL, "mime_decode_words: plain should not be NULL");
156 1 : ASSERT(strcmp(r, "Hello World") == 0, "plain string should be unchanged");
157 : }
158 :
159 : /* UTF-8 Q-encoding: Bí-Bor-Ász Kft. - Borászati Szaküzlet */
160 : {
161 2 : RAII_STRING char *r = mime_decode_words(
162 : "=?utf-8?Q?B=C3=AD-Bor-=C3=81sz_Kft=2E_-_Bor=C3=A1szati"
163 : "_Szak=C3=BCzlet?=");
164 1 : ASSERT(r != NULL, "mime_decode_words: Q UTF-8 should not be NULL");
165 1 : ASSERT(strcmp(r, "B\xc3\xad-Bor-\xc3\x81sz Kft. - Bor\xc3\xa1szati"
166 : " Szak\xc3\xbczlet") == 0,
167 : "Q UTF-8 decode mismatch");
168 : }
169 :
170 : /* UTF-8 B-encoding: "Hello" → base64 "SGVsbG8=" */
171 : {
172 2 : RAII_STRING char *r = mime_decode_words("=?utf-8?B?SGVsbG8=?=");
173 1 : ASSERT(r != NULL, "mime_decode_words: B UTF-8 should not be NULL");
174 1 : ASSERT(strcmp(r, "Hello") == 0, "B UTF-8 decode mismatch");
175 : }
176 :
177 : /* Multiple encoded words: whitespace between them must be stripped */
178 : {
179 2 : RAII_STRING char *r = mime_decode_words(
180 : "=?utf-8?Q?foo?= =?utf-8?Q?bar?=");
181 1 : ASSERT(r != NULL, "mime_decode_words: multi-word should not be NULL");
182 1 : ASSERT(strcmp(r, "foobar") == 0,
183 : "whitespace between encoded words should be stripped");
184 : }
185 :
186 : /* Mixed: encoded word followed by literal suffix */
187 : {
188 2 : RAII_STRING char *r = mime_decode_words(
189 : "=?utf-8?Q?Hello?= <user@example.com>");
190 1 : ASSERT(r != NULL, "mime_decode_words: mixed should not be NULL");
191 1 : ASSERT(strcmp(r, "Hello <user@example.com>") == 0,
192 : "mixed encoded + literal mismatch");
193 : }
194 :
195 : /* NULL input */
196 1 : ASSERT(mime_decode_words(NULL) == NULL,
197 : "mime_decode_words: NULL input should return NULL");
198 :
199 : /* ── mime_extract_imap_literal ──────────────────────────────────── */
200 :
201 : {
202 1 : const char *imap_resp =
203 : "* 1 FETCH (BODY[HEADER] {23}\r\n"
204 : "Subject: Test\r\n"
205 : "\r\n"
206 : ")\r\n"
207 : "A1 OK FETCH completed\r\n";
208 2 : RAII_STRING char *lit = mime_extract_imap_literal(imap_resp);
209 1 : ASSERT(lit != NULL, "mime_extract_imap_literal: should not be NULL");
210 1 : ASSERT(strncmp(lit, "Subject: Test", 13) == 0, "Literal content mismatch");
211 : }
212 :
213 : /* No literal in response */
214 : {
215 2 : RAII_STRING char *no_lit = mime_extract_imap_literal("* OK no literal here\r\n");
216 1 : ASSERT(no_lit == NULL, "No literal should return NULL");
217 : }
218 :
219 : /* NULL input */
220 1 : ASSERT(mime_extract_imap_literal(NULL) == NULL,
221 : "NULL response should return NULL");
222 :
223 : /* ── mime_format_date ───────────────────────────────────────────── */
224 :
225 : /* Force UTC so the expected output is timezone-independent. */
226 1 : const char *saved_tz = getenv("TZ");
227 1 : setenv("TZ", "UTC", 1);
228 1 : tzset();
229 :
230 : /* Standard RFC 2822 with weekday, UTC offset */
231 : {
232 2 : RAII_STRING char *r = mime_format_date("Tue, 10 Mar 2026 15:07:40 +0000");
233 1 : ASSERT(r != NULL, "mime_format_date: should not return NULL");
234 1 : ASSERT(strcmp(r, "2026-03-10 15:07") == 0, "UTC date format mismatch");
235 : }
236 :
237 : /* Date with +0100 offset: local (UTC) output should subtract 1 hour */
238 : {
239 2 : RAII_STRING char *r = mime_format_date("Thu, 26 Mar 2026 12:00:00 +0100");
240 1 : ASSERT(r != NULL, "mime_format_date: offset date should not return NULL");
241 1 : ASSERT(strcmp(r, "2026-03-26 11:00") == 0, "Offset date format mismatch");
242 : }
243 :
244 : /* Trailing timezone comment in parentheses */
245 : {
246 2 : RAII_STRING char *r = mime_format_date("Mon, 1 Jan 2026 00:00:00 +0000 (UTC)");
247 1 : ASSERT(r != NULL, "mime_format_date: comment date should not return NULL");
248 1 : ASSERT(strcmp(r, "2026-01-01 00:00") == 0, "Date with comment format mismatch");
249 : }
250 :
251 : /* Without day-of-week */
252 : {
253 2 : RAII_STRING char *r = mime_format_date("1 Jan 2026 10:30:00 +0000");
254 1 : ASSERT(r != NULL, "mime_format_date: no-weekday date should not return NULL");
255 1 : ASSERT(strcmp(r, "2026-01-01 10:30") == 0, "No-weekday date format mismatch");
256 : }
257 :
258 : /* Timezone name instead of numeric offset */
259 : {
260 2 : RAII_STRING char *r = mime_format_date("Tue, 24 Mar 2026 16:38:21 GMT");
261 1 : ASSERT(r != NULL, "mime_format_date: GMT date should not return NULL");
262 1 : ASSERT(strcmp(r, "2026-03-24 16:38") == 0, "GMT timezone date format mismatch");
263 : }
264 :
265 : /* Unparseable input: returns a copy of the raw string */
266 : {
267 2 : RAII_STRING char *r = mime_format_date("not a date");
268 1 : ASSERT(r != NULL, "mime_format_date: bad date should return raw copy");
269 1 : ASSERT(strcmp(r, "not a date") == 0, "Bad date should return raw input");
270 : }
271 :
272 : /* NULL input */
273 1 : ASSERT(mime_format_date(NULL) == NULL, "mime_format_date: NULL should return NULL");
274 :
275 : /* Restore original TZ */
276 1 : if (saved_tz)
277 0 : setenv("TZ", saved_tz, 1);
278 : else
279 1 : unsetenv("TZ");
280 1 : tzset();
281 :
282 : /* ── QP soft line break (=\r\n) ────────────────────────────────── */
283 :
284 : {
285 : /* "=" followed by \r\n is a soft break: the line ending is removed */
286 1 : const char *qp_soft =
287 : "Content-Type: text/plain\r\n"
288 : "Content-Transfer-Encoding: quoted-printable\r\n"
289 : "\r\n"
290 : "First=\r\n"
291 : "Second";
292 2 : RAII_STRING char *body = mime_get_text_body(qp_soft);
293 1 : ASSERT(body != NULL, "mime_get_text_body: QP soft break should not return NULL");
294 1 : ASSERT(strstr(body, "FirstSecond") != NULL,
295 : "QP soft line break should be removed");
296 : }
297 :
298 : /* ── body_start: LF-only separator ─────────────────────────────── */
299 :
300 : {
301 : /* Message with \n\n instead of \r\n\r\n as header/body separator */
302 1 : const char *lf_msg =
303 : "Content-Type: text/plain\n"
304 : "\n"
305 : "LF-only body";
306 2 : RAII_STRING char *body = mime_get_text_body(lf_msg);
307 1 : ASSERT(body != NULL, "mime_get_text_body: LF-only separator should work");
308 1 : ASSERT(strstr(body, "LF-only body") != NULL, "LF-only body content mismatch");
309 : }
310 :
311 : /* ── body_start: no separator → returns NULL ────────────────────── */
312 :
313 : {
314 : /* No blank line at all → body_start() returns NULL */
315 2 : RAII_STRING char *body = mime_get_text_body("Subject: no body");
316 1 : ASSERT(body == NULL,
317 : "mime_get_text_body: message without body separator should return NULL");
318 : }
319 :
320 : /* ── extract_charset: unquoted value ────────────────────────────── */
321 :
322 : {
323 1 : const char *ct_plain =
324 : "Content-Type: text/plain; charset=utf-8\r\n"
325 : "\r\n"
326 : "explicit UTF-8";
327 2 : RAII_STRING char *body = mime_get_text_body(ct_plain);
328 1 : ASSERT(body != NULL, "mime_get_text_body: unquoted charset=utf-8 should work");
329 1 : ASSERT(strstr(body, "explicit UTF-8") != NULL,
330 : "unquoted charset body mismatch");
331 : }
332 :
333 : /* ── extract_charset: quoted value ──────────────────────────────── */
334 :
335 : {
336 1 : const char *ct_quoted =
337 : "Content-Type: text/plain; charset=\"utf-8\"\r\n"
338 : "\r\n"
339 : "quoted charset";
340 2 : RAII_STRING char *body = mime_get_text_body(ct_quoted);
341 1 : ASSERT(body != NULL, "mime_get_text_body: quoted charset should work");
342 1 : ASSERT(strstr(body, "quoted charset") != NULL,
343 : "quoted charset body mismatch");
344 : }
345 :
346 : /* ── extract_charset: empty quoted value → NULL (p == start) ────── */
347 :
348 : {
349 : /* charset="" → extract_charset returns NULL → charset_to_utf8 is
350 : * called with NULL charset and simply returns strdup(body). */
351 1 : const char *ct_empty =
352 : "Content-Type: text/plain; charset=\"\"\r\n"
353 : "\r\n"
354 : "empty charset";
355 2 : RAII_STRING char *body = mime_get_text_body(ct_empty);
356 1 : ASSERT(body != NULL, "mime_get_text_body: empty charset should not crash");
357 1 : ASSERT(strstr(body, "empty charset") != NULL,
358 : "empty charset body mismatch");
359 : }
360 :
361 : /* ── charset_to_utf8: ISO-8859-1 body via iconv ──────────────────── */
362 :
363 : {
364 : /* \xE9 = 'é' in ISO-8859-1; UTF-8 encoding: \xC3\xA9 */
365 1 : const char *iso_msg =
366 : "Content-Type: text/plain; charset=iso-8859-1\r\n"
367 : "\r\n"
368 : "\xE9t\xE9"; /* "été" in ISO-8859-1 */
369 2 : RAII_STRING char *body = mime_get_text_body(iso_msg);
370 1 : ASSERT(body != NULL,
371 : "mime_get_text_body: iso-8859-1 should not return NULL");
372 1 : ASSERT(strstr(body, "\xC3\xA9t\xC3\xA9") != NULL,
373 : "ISO-8859-1 to UTF-8 body conversion mismatch");
374 : }
375 :
376 : /* ── text_from_multipart: unquoted boundary ──────────────────────── */
377 :
378 : {
379 1 : const char *mp_unquoted =
380 : "Content-Type: multipart/mixed; boundary=NOBOUND\r\n"
381 : "\r\n"
382 : "--NOBOUND\r\n"
383 : "Content-Type: text/plain\r\n"
384 : "\r\n"
385 : "Unquoted boundary text\r\n"
386 : "--NOBOUND--\r\n";
387 2 : RAII_STRING char *body = mime_get_text_body(mp_unquoted);
388 1 : ASSERT(body != NULL,
389 : "mime_get_text_body: unquoted boundary should work");
390 1 : ASSERT(strstr(body, "Unquoted boundary text") != NULL,
391 : "Unquoted boundary multipart content mismatch");
392 : }
393 :
394 : /* ── text_from_multipart: two non-text parts → exercises loop-continue
395 : * path (lines 256-259) and closing-boundary break (line 257 final),
396 : * then returns NULL (line 261). ──────────────────────────────────── */
397 :
398 : {
399 : /* Both parts are application/octet-stream → text_from_part returns NULL
400 : * for each. After the first part the loop continues (lines 258-259),
401 : * then the second delimiter turns out to be the closing "--B3--" →
402 : * break → return NULL. */
403 1 : const char *mp_none =
404 : "Content-Type: multipart/mixed; boundary=B3\r\n"
405 : "\r\n"
406 : "--B3\r\n"
407 : "Content-Type: application/octet-stream\r\n"
408 : "\r\n"
409 : "binary1\r\n"
410 : "--B3\r\n"
411 : "Content-Type: application/octet-stream\r\n"
412 : "\r\n"
413 : "binary2\r\n"
414 : "--B3--\r\n";
415 2 : RAII_STRING char *body = mime_get_text_body(mp_none);
416 1 : ASSERT(body == NULL,
417 : "mime_get_text_body: all-binary multipart should return NULL");
418 : }
419 :
420 : /* ── mime_decode_words: ISO-8859-1 encoded word via iconv ─────────── */
421 :
422 : {
423 : /* "été": \xE9=é, \xE9=é in ISO-8859-1 Q-encoding */
424 2 : RAII_STRING char *r = mime_decode_words("=?iso-8859-1?Q?=E9t=E9?=");
425 1 : ASSERT(r != NULL,
426 : "mime_decode_words: iso-8859-1 word should not return NULL");
427 1 : ASSERT(strcmp(r, "\xC3\xA9t\xC3\xA9") == 0,
428 : "ISO-8859-1 Q-encoded word UTF-8 decode mismatch");
429 : }
430 :
431 : /* ── mime_decode_words: unknown charset → raw bytes fallback ─────── */
432 :
433 : {
434 : /* iconv_open fails for unknown charset: raw decoded bytes returned */
435 2 : RAII_STRING char *r = mime_decode_words("=?x-unknown-charset?Q?hello?=");
436 1 : ASSERT(r != NULL,
437 : "mime_decode_words: unknown charset should not return NULL");
438 1 : ASSERT(strcmp(r, "hello") == 0,
439 : "Unknown charset encoded word should pass through raw bytes");
440 : }
441 :
442 : /* ── mime_extract_imap_literal: content shorter than claimed size ── */
443 :
444 : {
445 : /* {100} claims 100 bytes but only 5 are present */
446 1 : const char *trunc = "* FETCH {100}\r\nhello";
447 2 : RAII_STRING char *lit = mime_extract_imap_literal(trunc);
448 1 : ASSERT(lit != NULL,
449 : "mime_extract_imap_literal: truncated should not return NULL");
450 1 : ASSERT(strcmp(lit, "hello") == 0,
451 : "Truncated literal should return all available bytes");
452 : }
453 :
454 : /* ── mime_get_header: long value triggers realloc (>512 bytes) ───── */
455 :
456 : {
457 : /* 580 Z's exceed the initial 512-byte buffer → realloc required */
458 1 : char big_msg[700];
459 1 : strcpy(big_msg, "X-Big: ");
460 1 : memset(big_msg + 7, 'Z', 580);
461 1 : strcpy(big_msg + 587, "\r\n\r\n");
462 2 : RAII_STRING char *val = mime_get_header(big_msg, "X-Big");
463 1 : ASSERT(val != NULL,
464 : "mime_get_header: 580-char value should not return NULL");
465 1 : ASSERT(strlen(val) == 580, "Long header value length mismatch");
466 : }
467 :
468 : /* ── mime_get_header: folded long value triggers realloc ─────────── */
469 :
470 : {
471 : /* 511 A's fill the buffer, then a folded continuation adds space+X.
472 : * When the fold handler tries to add the separator space, n+1==512==cap
473 : * → realloc is triggered inside the fold branch. */
474 1 : char fold_msg[700];
475 1 : strcpy(fold_msg, "X-Fold: "); /* 8 chars */
476 1 : memset(fold_msg + 8, 'A', 511); /* 511 A's */
477 1 : strcpy(fold_msg + 519, "\r\n X\r\n\r\n");
478 2 : RAII_STRING char *val = mime_get_header(fold_msg, "X-Fold");
479 1 : ASSERT(val != NULL,
480 : "mime_get_header: folded long value should not return NULL");
481 1 : ASSERT(strlen(val) == 513,
482 : "Folded long header value length mismatch (511 A + space + X)");
483 : }
484 :
485 : /* ── mime_get_html_part ─────────────────────────────────────────── */
486 :
487 : /* HTML-only message */
488 : {
489 1 : const char *html_msg =
490 : "Content-Type: text/html\r\n"
491 : "\r\n"
492 : "<html><body><b>Bold</b></body></html>";
493 2 : RAII_STRING char *html = mime_get_html_part(html_msg);
494 1 : ASSERT(html != NULL, "mime_get_html_part: html-only should not be NULL");
495 1 : ASSERT(strstr(html, "<b>Bold</b>") != NULL,
496 : "mime_get_html_part: html content present");
497 : }
498 :
499 : /* Plain-only message → NULL */
500 : {
501 1 : const char *plain_msg =
502 : "Content-Type: text/plain\r\n"
503 : "\r\n"
504 : "Plain only";
505 2 : RAII_STRING char *html = mime_get_html_part(plain_msg);
506 1 : ASSERT(html == NULL, "mime_get_html_part: plain-only should return NULL");
507 : }
508 :
509 : /* NULL input → NULL */
510 1 : ASSERT(mime_get_html_part(NULL) == NULL,
511 : "mime_get_html_part: NULL should return NULL");
512 :
513 : /* multipart/alternative with html part */
514 : {
515 1 : const char *alt_msg =
516 : "Content-Type: multipart/alternative; boundary=\"ALT\"\r\n"
517 : "\r\n"
518 : "--ALT\r\n"
519 : "Content-Type: text/plain\r\n"
520 : "\r\n"
521 : "Plain fallback\r\n"
522 : "--ALT\r\n"
523 : "Content-Type: text/html\r\n"
524 : "\r\n"
525 : "<p>HTML part</p>\r\n"
526 : "--ALT--\r\n";
527 2 : RAII_STRING char *html = mime_get_html_part(alt_msg);
528 1 : ASSERT(html != NULL, "mime_get_html_part: multipart/alt html should not be NULL");
529 1 : ASSERT(strstr(html, "<p>HTML part</p>") != NULL,
530 : "mime_get_html_part: multipart html content present");
531 : }
532 :
533 : /* multipart with unquoted boundary (covers html_from_multipart unquoted path) */
534 : {
535 1 : const char *unquoted_msg =
536 : "Content-Type: multipart/alternative; boundary=UNQUOTED\r\n"
537 : "\r\n"
538 : "--UNQUOTED\r\n"
539 : "Content-Type: text/html\r\n"
540 : "\r\n"
541 : "<b>unquoted</b>\r\n"
542 : "--UNQUOTED--\r\n";
543 2 : RAII_STRING char *html = mime_get_html_part(unquoted_msg);
544 1 : ASSERT(html != NULL, "mime_get_html_part: unquoted boundary not NULL");
545 1 : ASSERT(strstr(html, "<b>unquoted</b>") != NULL,
546 : "mime_get_html_part: unquoted boundary content present");
547 : }
548 :
549 : /* multipart with no HTML parts → NULL (covers html_from_multipart return NULL) */
550 : {
551 1 : const char *no_html_msg =
552 : "Content-Type: multipart/mixed; boundary=\"NOHTML\"\r\n"
553 : "\r\n"
554 : "--NOHTML\r\n"
555 : "Content-Type: text/plain\r\n"
556 : "\r\n"
557 : "plain only\r\n"
558 : "--NOHTML--\r\n";
559 2 : RAII_STRING char *html = mime_get_html_part(no_html_msg);
560 1 : ASSERT(html == NULL, "mime_get_html_part: no-html multipart should return NULL");
561 : }
562 : }
|