LCOV - code coverage report
Current view: top level - tests/unit - test_mime.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 99.4 % 171 170
Test Date: 2026-04-15 21:12:52 Functions: 100.0 % 1 1

            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              : }
        

Generated by: LCOV version 2.0-1