LCOV - code coverage report
Current view: top level - libemail/src/core - mime_util.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 98.0 % 613 601
Test Date: 2026-08-21 10:11:28 Functions: 100.0 % 31 31

            Line data    Source code
       1              : #include "mime_util.h"
       2              : #include "html_render.h"
       3              : #include "raii.h"
       4              : #include <stdio.h>
       5              : #include <stdlib.h>
       6              : #include <string.h>
       7              : #include <ctype.h>
       8              : #include <time.h>
       9              : #include <iconv.h>
      10              : #include <errno.h>
      11              : 
      12              : /* ── Header extraction ──────────────────────────────────────────────── */
      13              : 
      14        10727 : char *mime_get_header(const char *msg, const char *name) {
      15        10727 :     if (!msg || !name) return NULL;
      16        10725 :     size_t nlen = strlen(name);
      17        10725 :     const char *p = msg;
      18              : 
      19        48506 :     while (p && *p) {
      20              :         /* Stop at the blank line separating headers from body. */
      21        37777 :         if (*p == '\r' || *p == '\n')
      22              :             break;
      23              : 
      24        34396 :         if (strncasecmp(p, name, nlen) == 0 && p[nlen] == ':') {
      25         7340 :             const char *val = p + nlen + 1;
      26        14680 :             while (*val == ' ' || *val == '\t') val++;
      27              : 
      28         7340 :             size_t cap = 512, n = 0;
      29         7340 :             char *result = malloc(cap);
      30         7340 :             if (!result) return NULL;
      31              : 
      32              :             /* Collect value, unfolding continuation lines */
      33       208937 :             while (*val) {
      34       208937 :                 if (*val == '\r' || *val == '\n') {
      35         7630 :                     const char *next = val;
      36         7630 :                     if (*next == '\r') next++;
      37         7630 :                     if (*next == '\n') next++;
      38         7630 :                     if (*next == ' ' || *next == '\t') {
      39              :                         /* Continuation line: skip CRLF and the leading whitespace */
      40          290 :                         val = next;
      41          580 :                         while (*val == ' ' || *val == '\t') val++;
      42              :                         /* Add a single space to separate folded content if needed */
      43          290 :                         if (n > 0 && result[n-1] != ' ') {
      44          290 :                             if (n + 1 >= cap) {
      45            1 :                                 cap *= 2;
      46            1 :                                 char *tmp = realloc(result, cap);
      47            1 :                                 if (!tmp) { free(result); return NULL; }
      48            1 :                                 result = tmp;
      49              :                             }
      50          290 :                             result[n++] = ' ';
      51              :                         }
      52          290 :                         continue;
      53              :                     } else {
      54              :                         /* Not a continuation line: we are done with this header */
      55              :                         break;
      56              :                     }
      57              :                 }
      58              : 
      59       201307 :                 if (n + 1 >= cap) {
      60            1 :                     cap *= 2;
      61            1 :                     char *tmp = realloc(result, cap);
      62            1 :                     if (!tmp) { free(result); return NULL; }
      63            1 :                     result = tmp;
      64              :                 }
      65       201307 :                 result[n++] = *val++;
      66              :             }
      67         7340 :             result[n] = '\0';
      68         7340 :             return result;
      69              :         }
      70              : 
      71              :         /* Advance to next line */
      72        27056 :         p = strchr(p, '\n');
      73        27056 :         if (p) p++;
      74              :     }
      75         3385 :     return NULL;
      76              : }
      77              : 
      78              : /* ── Base64 decoder ─────────────────────────────────────────────────── */
      79              : 
      80         4718 : static int b64val(unsigned char c) {
      81         4718 :     if (c >= 'A' && c <= 'Z') return c - 'A';
      82         1907 :     if (c >= 'a' && c <= 'z') return c - 'a' + 26;
      83          498 :     if (c >= '0' && c <= '9') return c - '0' + 52;
      84          281 :     if (c == '+') return 62;
      85          281 :     if (c == '/') return 63;
      86          281 :     return -1;
      87              : }
      88              : 
      89          114 : static char *decode_base64(const char *in, size_t inlen, size_t *out_len) {
      90          114 :     size_t max = (inlen / 4 + 1) * 3 + 4;
      91          114 :     char *out = malloc(max);
      92          114 :     if (!out) return NULL;
      93          114 :     size_t n = 0;
      94          114 :     int buf = 0, bits = 0;
      95         4832 :     for (size_t i = 0; i < inlen; i++) {
      96         4718 :         int v = b64val((unsigned char)in[i]);
      97         4718 :         if (v < 0) continue;
      98         4437 :         buf = (buf << 6) | v;
      99         4437 :         bits += 6;
     100         4437 :         if (bits >= 8) {
     101         3311 :             bits -= 8;
     102         3311 :             out[n++] = (char)((buf >> bits) & 0xFF);
     103              :         }
     104              :     }
     105          114 :     out[n] = '\0';
     106          114 :     if (out_len) *out_len = n;
     107          114 :     return out;
     108              : }
     109              : 
     110              : /* ── Quoted-Printable decoder ───────────────────────────────────────── */
     111              : 
     112            2 : static char *decode_qp(const char *in, size_t inlen) {
     113            2 :     char *out = malloc(inlen + 1);
     114            2 :     if (!out) return NULL;
     115            2 :     size_t n = 0, i = 0;
     116           26 :     while (i < inlen) {
     117           24 :         if (in[i] == '=' && i + 1 < inlen &&
     118            4 :             (in[i + 1] == '\r' || in[i + 1] == '\n')) {
     119              :             /* Soft line break — skip */
     120            1 :             i++;
     121            1 :             if (i < inlen && in[i] == '\r') i++;
     122            1 :             if (i < inlen && in[i] == '\n') i++;
     123           23 :         } else if (in[i] == '=' && i + 2 < inlen &&
     124            3 :                    isxdigit((unsigned char)in[i + 1]) &&
     125            3 :                    isxdigit((unsigned char)in[i + 2])) {
     126            3 :             char hex[3] = { in[i + 1], in[i + 2], '\0' };
     127            3 :             out[n++] = (char)strtol(hex, NULL, 16);
     128            3 :             i += 3;
     129              :         } else {
     130           20 :             out[n++] = in[i++];
     131              :         }
     132              :     }
     133            2 :     out[n] = '\0';
     134            2 :     return out;
     135              : }
     136              : 
     137              : /* ── Body helpers ───────────────────────────────────────────────────── */
     138              : 
     139          427 : static const char *body_start(const char *msg) {
     140          427 :     const char *p = strstr(msg, "\r\n\r\n");
     141          427 :     if (p) return p + 4;
     142            4 :     p = strstr(msg, "\n\n");
     143            4 :     if (p) return p + 2;
     144            3 :     return NULL;
     145              : }
     146              : 
     147          140 : static char *decode_transfer(const char *body, size_t len, const char *enc) {
     148          140 :     if (enc && strcasecmp(enc, "base64") == 0)
     149           12 :         return decode_base64(body, len, NULL);
     150          128 :     if (enc && strcasecmp(enc, "quoted-printable") == 0)
     151            2 :         return decode_qp(body, len);
     152          126 :     return strndup(body, len);
     153              : }
     154              : 
     155              : /* Extract the charset parameter value from a Content-Type header value.
     156              :  * E.g. "text/plain; charset=iso-8859-2" → "iso-8859-2".
     157              :  * Returns a malloc'd string or NULL if not found. */
     158          325 : static char *extract_charset(const char *ctype) {
     159          325 :     if (!ctype) return NULL;
     160              : 
     161              :     /* RFC 2045 §5.1: parameters are  attribute [LWSP] "=" [LWSP] value,
     162              :      * and the value may be a token or a quoted-string.  All of these are
     163              :      * legal and must yield "iso-8859-2":
     164              :      *
     165              :      *     charset=iso-8859-2
     166              :      *     charset="iso-8859-2"
     167              :      *     charset = "iso-8859-2"
     168              :      *
     169              :      * Matching the literal "charset=" misses the spaced forms, and the
     170              :      * conversion is then skipped silently, emitting the raw bytes. */
     171          301 :     for (const char *p = ctype; (p = strcasestr(p, "charset")) != NULL; p += 7) {
     172              :         /* Must start a parameter, so that e.g. "x-charset" does not match. */
     173          192 :         if (p != ctype) {
     174          192 :             char prev = p[-1];
     175          192 :             if (prev != ';' && prev != ' ' && prev != '\t' &&
     176            1 :                 prev != '\r' && prev != '\n')
     177            1 :                 continue;
     178              :         }
     179              : 
     180          191 :         const char *q = p + 7;                       /* past "charset" */
     181          193 :         while (*q == ' ' || *q == '\t') q++;
     182          191 :         if (*q != '=') continue;
     183          191 :         q++;
     184          193 :         while (*q == ' ' || *q == '\t') q++;
     185              : 
     186          191 :         if (*q == '"') {                             /* quoted-string value */
     187            4 :             q++;
     188            4 :             const char *end = strchr(q, '"');
     189            4 :             if (!end || end == q) return NULL;
     190            3 :             return strndup(q, (size_t)(end - q));
     191              :         }
     192              : 
     193          187 :         const char *start = q;                       /* bare token value */
     194         1196 :         while (*q && *q != ';' && *q != ' ' && *q != '\t' &&
     195         2205 :                *q != '"' && *q != '\r' && *q != '\n')
     196         1009 :             q++;
     197          187 :         if (q == start) return NULL;
     198          187 :         return strndup(start, (size_t)(q - start));
     199              :     }
     200          109 :     return NULL;
     201              : }
     202              : 
     203              : /* Convert s from from_charset to UTF-8 via iconv.
     204              :  * Returns a malloc'd UTF-8 string; on failure returns strdup(s).
     205              :  *
     206              :  * @param info  Optional; records what happened so that callers can report a
     207              :  *              pass-through instead of letting undecoded bytes reach the user
     208              :  *              unannounced.  The core layer only records — deciding whether to
     209              :  *              tell the user is the caller's job. */
     210          134 : static char *charset_to_utf8_info(const char *s, const char *from_charset,
     211              :                                   MimeTextInfo *info) {
     212          134 :     if (!s) return NULL;
     213          134 :     if (info && from_charset && !info->declared_charset[0])
     214           23 :         snprintf(info->declared_charset, sizeof(info->declared_charset),
     215              :                  "%s", from_charset);
     216              : 
     217          134 :     if (!from_charset ||
     218          108 :         strcasecmp(from_charset, "utf-8")  == 0 ||
     219           13 :         strcasecmp(from_charset, "utf8")   == 0 ||
     220           13 :         strcasecmp(from_charset, "us-ascii") == 0)
     221          121 :         return strdup(s);
     222              : 
     223           13 :     iconv_t cd = iconv_open("UTF-8", from_charset);
     224           13 :     if (cd == (iconv_t)-1) {
     225            1 :         if (info) info->unknown_charset = 1;
     226            1 :         return strdup(s);
     227              :     }
     228              : 
     229           12 :     size_t in_len   = strlen(s);
     230           12 :     size_t out_size = in_len * 4 + 1;
     231           12 :     char  *out      = malloc(out_size);
     232           12 :     if (!out) { iconv_close(cd); return strdup(s); }
     233              : 
     234           12 :     char  *inp      = (char *)s;
     235           12 :     char  *outp     = out;
     236           12 :     size_t inbytes  = in_len;
     237           12 :     size_t outbytes = out_size - 1;
     238           12 :     size_t r        = iconv(cd, &inp, &inbytes, &outp, &outbytes);
     239           12 :     iconv_close(cd);
     240              : 
     241           12 :     if (r == (size_t)-1) {
     242            0 :         if (info) info->invalid_sequence = 1;
     243            0 :         free(out);
     244            0 :         return strdup(s);
     245              :     }
     246           12 :     *outp = '\0';
     247           12 :     if (info) info->converted = 1;
     248           12 :     return out;
     249              : }
     250              : 
     251              : /* Backwards-compatible wrapper for call sites that do not want diagnostics. */
     252           65 : static char *charset_to_utf8(const char *s, const char *from_charset) {
     253           65 :     return charset_to_utf8_info(s, from_charset, NULL);
     254              : }
     255              : 
     256              : static char *text_from_part_info(const char *part, MimeTextInfo *info);
     257              : 
     258           16 : static char *text_from_multipart_info(const char *msg, const char *ctype,
     259              :                                       MimeTextInfo *info) {
     260           16 :     const char *b = strcasestr(ctype, "boundary=");
     261           16 :     if (!b) return NULL;
     262           16 :     b += strlen("boundary=");
     263              : 
     264           16 :     char boundary[512] = {0};
     265           16 :     if (*b == '"') {
     266           14 :         b++;
     267           14 :         const char *end = strchr(b, '"');
     268           14 :         if (!end) return NULL;
     269           14 :         snprintf(boundary, sizeof(boundary), "%.*s", (int)(end - b), b);
     270              :     } else {
     271            2 :         size_t i = 0;
     272           11 :         while (*b && *b != ';' && *b != ' ' && *b != '\r' && *b != '\n' &&
     273              :                i < sizeof(boundary) - 1)
     274            9 :             boundary[i++] = *b++;
     275            2 :         boundary[i] = '\0';
     276              :     }
     277           16 :     if (!boundary[0]) return NULL;
     278              : 
     279              :     char delim[520];
     280           16 :     snprintf(delim, sizeof(delim), "--%s", boundary);
     281           16 :     size_t dlen = strlen(delim);
     282              : 
     283           16 :     const char *p = strstr(msg, delim);
     284           17 :     while (p) {
     285           17 :         p = strchr(p + dlen, '\n');
     286           17 :         if (!p) break;
     287           17 :         p++;
     288              : 
     289           17 :         const char *next = strstr(p, delim);
     290           17 :         if (!next) break;
     291              : 
     292           17 :         size_t partlen = (size_t)(next - p);
     293           17 :         char *part = strndup(p, partlen);
     294           17 :         if (!part) break;
     295           17 :         char *result = text_from_part_info(part, info);
     296           17 :         free(part);
     297           17 :         if (result) return result;
     298              : 
     299            2 :         p = next + dlen;
     300            2 :         if (p[0] == '-' && p[1] == '-') break;
     301            1 :         p = strchr(p, '\n');
     302            1 :         if (p) p++;
     303              :     }
     304            1 :     return NULL;
     305              : }
     306              : 
     307           88 : static char *text_from_part_info(const char *part, MimeTextInfo *info) {
     308           88 :     char *ctype   = mime_get_header(part, "Content-Type");
     309           88 :     char *enc     = mime_get_header(part, "Content-Transfer-Encoding");
     310           88 :     char *charset = extract_charset(ctype);
     311           88 :     const char *body = body_start(part);
     312           88 :     char *result = NULL;
     313              : 
     314           88 :     if (!ctype || strncasecmp(ctype, "text/plain", 10) == 0) {
     315           70 :         if (body) {
     316           68 :             char *raw = decode_transfer(body, strlen(body), enc);
     317           68 :             if (raw) {
     318           68 :                 result = charset_to_utf8_info(raw, charset, info);
     319           68 :                 free(raw);
     320              :             }
     321              :         }
     322           18 :     } else if (strncasecmp(ctype, "multipart/", 10) == 0) {
     323           16 :         result = text_from_multipart_info(part, ctype, info);
     324            2 :     } else if (strncasecmp(ctype, "text/html", 9) == 0) {
     325            1 :         if (body) {
     326            1 :             char *raw = decode_transfer(body, strlen(body), enc);
     327            1 :             if (raw) {
     328            1 :                 char *utf8 = charset_to_utf8_info(raw, charset, info);
     329            1 :                 free(raw);
     330            1 :                 if (utf8) {
     331            1 :                     result = html_render(utf8, 0, 0);
     332            1 :                     free(utf8);
     333              :                 }
     334              :             }
     335              :         }
     336              :     }
     337              : 
     338           88 :     free(ctype);
     339           88 :     free(enc);
     340           88 :     free(charset);
     341           88 :     return result;
     342              : }
     343              : 
     344              : /* ── RFC 2047 encoded-word decoder ──────────────────────────────────── */
     345              : 
     346              : /**
     347              :  * Decode the text portion of one encoded word and convert to UTF-8.
     348              :  *
     349              :  * enc == 'Q'/'q': quoted-printable variant (underscore = space).
     350              :  * enc == 'B'/'b': base64.
     351              :  * charset: the declared charset of the encoded bytes.
     352              :  *
     353              :  * Returns a malloc'd NUL-terminated UTF-8 string, or NULL on failure.
     354              :  */
     355          319 : static char *decode_encoded_word(const char *charset, char enc,
     356              :                                   const char *text, size_t text_len) {
     357          319 :     char *raw = NULL;
     358              : 
     359          319 :     if (enc == 'Q' || enc == 'q') {
     360          312 :         raw = malloc(text_len + 1);
     361          312 :         if (!raw) return NULL;
     362          312 :         size_t i = 0, j = 0;
     363         3126 :         while (i < text_len) {
     364         2814 :             if (text[i] == '_') {
     365          310 :                 raw[j++] = ' ';
     366          310 :                 i++;
     367         2504 :             } else if (text[i] == '=' && i + 2 < text_len &&
     368           11 :                        isxdigit((unsigned char)text[i + 1]) &&
     369           11 :                        isxdigit((unsigned char)text[i + 2])) {
     370           11 :                 char hex[3] = { text[i + 1], text[i + 2], '\0' };
     371           11 :                 raw[j++] = (char)strtol(hex, NULL, 16);
     372           11 :                 i += 3;
     373              :             } else {
     374         2493 :                 raw[j++] = text[i++];
     375              :             }
     376              :         }
     377          312 :         raw[j] = '\0';
     378              :     } else {
     379              :         /* B encoding */
     380            7 :         raw = decode_base64(text, text_len, NULL);
     381            7 :         if (!raw) return NULL;
     382              :     }
     383              : 
     384              :     /* If the declared charset is already UTF-8, return as-is. */
     385          319 :     if (strcasecmp(charset, "utf-8") == 0 || strcasecmp(charset, "utf8") == 0)
     386          317 :         return raw;
     387              : 
     388              :     /* Otherwise convert via iconv. */
     389            2 :     iconv_t cd = iconv_open("UTF-8", charset);
     390            2 :     if (cd == (iconv_t)-1)
     391            1 :         return raw;   /* unknown charset — return raw bytes */
     392              : 
     393            1 :     size_t raw_len   = strlen(raw);
     394            1 :     size_t out_size  = raw_len * 4 + 1;
     395            1 :     char  *utf8      = malloc(out_size);
     396            1 :     if (!utf8) { iconv_close(cd); return raw; }
     397              : 
     398            1 :     char   *inp      = raw;
     399            1 :     char   *outp     = utf8;
     400            1 :     size_t  inbytes  = raw_len;
     401            1 :     size_t  outbytes = out_size - 1;
     402            1 :     size_t  r        = iconv(cd, &inp, &inbytes, &outp, &outbytes);
     403            1 :     iconv_close(cd);
     404              : 
     405            1 :     if (r == (size_t)-1) { free(utf8); return raw; }
     406              : 
     407            1 :     *outp = '\0';
     408            1 :     free(raw);
     409            1 :     return utf8;
     410              : }
     411              : 
     412              : /**
     413              :  * Try to parse and decode one encoded word starting exactly at *pp.
     414              :  * Format: =?charset?Q|B?encoded_text?=
     415              :  *
     416              :  * On success, *pp is advanced past the closing "?=" and the decoded
     417              :  * UTF-8 string (malloc'd) is returned.
     418              :  * On failure, *pp is unchanged and NULL is returned.
     419              :  */
     420          319 : static char *try_decode_encoded_word(const char **pp) {
     421          319 :     const char *p = *pp;
     422          319 :     if (p[0] != '=' || p[1] != '?') return NULL;
     423          319 :     p += 2;
     424              : 
     425              :     /* charset */
     426          319 :     const char *cs = p;
     427         1931 :     while (*p && *p != '?') p++;
     428          319 :     if (!*p) return NULL;
     429          319 :     size_t cs_len = (size_t)(p - cs);
     430          319 :     if (cs_len == 0 || cs_len >= 64) return NULL;
     431              :     char charset[64];
     432          319 :     memcpy(charset, cs, cs_len);
     433          319 :     charset[cs_len] = '\0';
     434          319 :     p++;   /* skip ? */
     435              : 
     436              :     /* encoding indicator */
     437          319 :     char enc = *p;
     438          319 :     if (enc != 'Q' && enc != 'q' && enc != 'B' && enc != 'b') return NULL;
     439          319 :     p++;
     440          319 :     if (*p != '?') return NULL;
     441          319 :     p++;   /* skip ? */
     442              : 
     443              :     /* encoded text — ends at next ?= */
     444          319 :     const char *txt = p;
     445         5563 :     while (*p && !(*p == '?' && p[1] == '=')) p++;
     446          319 :     if (!*p) return NULL;
     447          319 :     size_t txt_len = (size_t)(p - txt);
     448          319 :     p += 2;   /* skip ?= */
     449              : 
     450          319 :     char *decoded = decode_encoded_word(charset, enc, txt, txt_len);
     451          319 :     if (!decoded) return NULL;
     452          319 :     *pp = p;
     453          319 :     return decoded;
     454              : }
     455              : 
     456         3550 : char *mime_decode_words(const char *value) {
     457         3550 :     if (!value) return NULL;
     458              : 
     459         3549 :     size_t vlen = strlen(value);
     460              :     /* Upper bound: each raw byte can expand to at most 4 UTF-8 bytes. */
     461         3549 :     size_t cap = vlen * 4 + 1;
     462         3549 :     char  *out = malloc(cap);
     463         3549 :     if (!out) return NULL;
     464              : 
     465         3549 :     size_t      n             = 0;
     466         3549 :     const char *p             = value;
     467         3549 :     int         prev_encoded  = 0;
     468              : 
     469        87690 :     while (*p) {
     470              :         /* RFC 2047 §6.2: linear whitespace between adjacent encoded words
     471              :          * must be ignored. */
     472        84141 :         if (prev_encoded && (*p == ' ' || *p == '\t')) {
     473          308 :             const char *ws = p;
     474          616 :             while (*ws == ' ' || *ws == '\t') ws++;
     475          308 :             if (ws[0] == '=' && ws[1] == '?') {
     476            1 :                 p = ws;
     477            1 :                 continue;
     478              :             }
     479              :         }
     480              : 
     481        84140 :         if (p[0] == '=' && p[1] == '?') {
     482          319 :             char *decoded = try_decode_encoded_word(&p);
     483          319 :             if (decoded) {
     484          319 :                 size_t dlen = strlen(decoded);
     485          319 :                 if (n + dlen >= cap) {
     486            0 :                     cap = n + dlen + vlen + 1;
     487            0 :                     char *tmp = realloc(out, cap);
     488            0 :                     if (!tmp) { free(decoded); break; }
     489            0 :                     out = tmp;
     490              :                 }
     491          319 :                 memcpy(out + n, decoded, dlen);
     492          319 :                 n += dlen;
     493          319 :                 free(decoded);
     494          319 :                 prev_encoded = 1;
     495          319 :                 continue;
     496              :             }
     497              :         }
     498              : 
     499        83821 :         prev_encoded = 0;
     500        83821 :         out[n++] = *p++;
     501              :     }
     502              : 
     503         3549 :     out[n] = '\0';
     504         3549 :     return out;
     505              : }
     506              : 
     507              : /* ── Date formatting ────────────────────────────────────────────────── */
     508              : 
     509         1855 : char *mime_format_date(const char *date) {
     510         1855 :     if (!date || !*date) return NULL;
     511              : 
     512              :     static const char * const fmts[] = {
     513              :         "%a, %d %b %Y %T %z",  /* "Tue, 10 Mar 2026 15:07:40 +0000"     */
     514              :         "%d %b %Y %T %z",       /* "10 Mar 2026 15:07:40 +0000"          */
     515              :         "%a, %d %b %Y %T %Z",  /* "Tue, 24 Mar 2026 16:38:21 GMT"       */
     516              :         "%d %b %Y %T %Z",       /* "24 Mar 2026 16:38:21 UTC"            */
     517              :         NULL
     518              :     };
     519              : 
     520              :     struct tm tm;
     521         1854 :     int parsed = 0;
     522         1861 :     for (int i = 0; fmts[i]; i++) {
     523         1860 :         memset(&tm, 0, sizeof(tm));
     524         1860 :         if (strptime(date, fmts[i], &tm)) { parsed = 1; break; }
     525              :     }
     526         1854 :     if (!parsed) return strdup(date);
     527              : 
     528              :     /* Save tm_gmtoff before calling timegm(): timegm() normalises the struct
     529              :      * and resets tm_gmtoff to 0.  timegm() treats the fields as UTC, so
     530              :      * subtracting the original offset converts to true UTC. */
     531         1853 :     long gmtoff = tm.tm_gmtoff;
     532         1853 :     time_t utc = timegm(&tm) - gmtoff;
     533         1853 :     if (utc == (time_t)-1) return strdup(date);
     534              : 
     535              :     struct tm local;
     536         1853 :     localtime_r(&utc, &local);
     537              : 
     538         1853 :     char *buf = malloc(17);   /* "YYYY-MM-DD HH:MM\0" */
     539         1853 :     if (!buf) return NULL;
     540         1853 :     if (strftime(buf, 17, "%Y-%m-%d %H:%M", &local) == 0) {
     541            0 :         free(buf);
     542            0 :         return strdup(date);
     543              :     }
     544         1853 :     return buf;
     545              : }
     546              : 
     547              : /* ── HTML part extractor ────────────────────────────────────────────── */
     548              : 
     549              : static char *html_from_part(const char *part);
     550              : 
     551           70 : static char *html_from_multipart(const char *msg, const char *ctype) {
     552           70 :     const char *b = strcasestr(ctype, "boundary=");
     553           70 :     if (!b) return NULL;
     554           70 :     b += strlen("boundary=");
     555              : 
     556           70 :     char boundary[512] = {0};
     557           70 :     if (*b == '"') {
     558           69 :         b++;
     559           69 :         const char *end = strchr(b, '"');
     560           69 :         if (!end) return NULL;
     561           69 :         snprintf(boundary, sizeof(boundary), "%.*s", (int)(end - b), b);
     562              :     } else {
     563            1 :         size_t i = 0;
     564            9 :         while (*b && *b != ';' && *b != ' ' && *b != '\r' && *b != '\n' &&
     565              :                i < sizeof(boundary) - 1)
     566            8 :             boundary[i++] = *b++;
     567            1 :         boundary[i] = '\0';
     568              :     }
     569           70 :     if (!boundary[0]) return NULL;
     570              : 
     571              :     char delim[520];
     572           70 :     snprintf(delim, sizeof(delim), "--%s", boundary);
     573           70 :     size_t dlen = strlen(delim);
     574              : 
     575           70 :     const char *p = strstr(msg, delim);
     576          145 :     while (p) {
     577          145 :         if (p[dlen] == '-' && p[dlen+1] == '-') break; /* end boundary */
     578          138 :         p = strchr(p + dlen, '\n');
     579          138 :         if (!p) break;
     580          138 :         p++;
     581          138 :         const char *next = strstr(p, delim);
     582          138 :         if (!next) break;
     583          138 :         size_t partlen = (size_t)(next - p);
     584          138 :         char *part = strndup(p, partlen);
     585          138 :         if (!part) break;
     586          138 :         char *result = html_from_part(part);
     587          138 :         free(part);
     588          138 :         if (result) return result;
     589           75 :         p = next; /* keep p pointing at delimiter for next iteration */
     590              :     }
     591            7 :     return NULL;
     592              : }
     593              : 
     594          237 : static char *html_from_part(const char *part) {
     595          237 :     char *ctype   = mime_get_header(part, "Content-Type");
     596          237 :     char *enc     = mime_get_header(part, "Content-Transfer-Encoding");
     597          237 :     char *charset = extract_charset(ctype);
     598          237 :     const char *body = body_start(part);
     599          237 :     char *result = NULL;
     600              : 
     601          237 :     if (ctype && strncasecmp(ctype, "text/html", 9) == 0) {
     602           65 :         if (body) {
     603           65 :             char *raw = decode_transfer(body, strlen(body), enc);
     604           65 :             if (raw) {
     605           65 :                 result = charset_to_utf8(raw, charset);
     606           65 :                 free(raw);
     607              :             }
     608              :         }
     609          172 :     } else if (ctype && strncasecmp(ctype, "multipart/", 10) == 0) {
     610           70 :         result = html_from_multipart(part, ctype);
     611              :     }
     612              : 
     613          237 :     free(ctype); free(enc); free(charset);
     614          237 :     return result;
     615              : }
     616              : 
     617              : /* ── Public API ─────────────────────────────────────────────────────── */
     618              : 
     619           37 : char *mime_get_text_body(const char *msg) {
     620           37 :     return mime_get_text_body_ex(msg, NULL);
     621              : }
     622              : 
     623           72 : char *mime_get_text_body_ex(const char *msg, MimeTextInfo *info) {
     624           72 :     if (info) memset(info, 0, sizeof(*info));
     625           72 :     if (!msg) return NULL;
     626           71 :     return text_from_part_info(msg, info);
     627              : }
     628              : 
     629          100 : char *mime_get_html_part(const char *msg) {
     630          100 :     if (!msg) return NULL;
     631           99 :     return html_from_part(msg);
     632              : }
     633              : 
     634              : /* ── Attachment extraction ──────────────────────────────────────────── */
     635              : 
     636              : /* Extract a MIME header parameter value, e.g. filename="foo.pdf" or name=bar.
     637              :  * Handles quoted and unquoted values.  Returns malloc'd string or NULL. */
     638          187 : static char *extract_param(const char *header, const char *param) {
     639          187 :     if (!header || !param) return NULL;
     640              :     char search[64];
     641          187 :     snprintf(search, sizeof(search), "%s=", param);
     642          187 :     const char *p = strcasestr(header, search);
     643          187 :     if (!p) return NULL;
     644          103 :     p += strlen(search);
     645          103 :     if (*p == '"') {
     646           99 :         p++;
     647           99 :         const char *end = strchr(p, '"');
     648           99 :         if (!end) return NULL;
     649           99 :         return strndup(p, (size_t)(end - p));
     650              :     }
     651              :     /* unquoted value: ends at ';', whitespace, or end-of-string */
     652            4 :     const char *end = p;
     653           44 :     while (*end && *end != ';' && *end != ' ' && *end != '\t' &&
     654           84 :            *end != '\r' && *end != '\n')
     655           40 :         end++;
     656            4 :     if (end == p) return NULL;
     657            4 :     return strndup(p, (size_t)(end - p));
     658              : }
     659              : 
     660              : /* Sanitise a filename: strip directory separators and leading dots. */
     661          100 : static char *sanitise_filename(const char *name) {
     662          100 :     if (!name || !*name) return NULL;
     663              :     /* take only the basename portion */
     664          100 :     const char *base = name;
     665         1037 :     for (const char *p = name; *p; p++)
     666          937 :         if (*p == '/' || *p == '\\') base = p + 1;
     667          100 :     if (!*base) return NULL;
     668          100 :     char *s = strdup(base);
     669          100 :     if (!s) return NULL;
     670              :     /* strip leading dots (hidden files / directory traversal) */
     671          100 :     char *p = s;
     672          100 :     while (*p == '.') p++;
     673          100 :     if (!*p) { free(s); return strdup("attachment"); }
     674          100 :     if (p != s) memmove(s, p, strlen(p) + 1);
     675          100 :     return s;
     676              : }
     677              : 
     678              : /* Dynamic array for building the attachment list */
     679              : typedef struct { MimeAttachment *data; int count; int cap; } AttachList;
     680              : 
     681          101 : static int alist_push(AttachList *al, MimeAttachment att) {
     682          101 :     if (al->count >= al->cap) {
     683           62 :         int newcap = al->cap ? al->cap * 2 : 4;
     684           62 :         MimeAttachment *tmp = realloc(al->data,
     685           62 :                                       (size_t)newcap * sizeof(MimeAttachment));
     686           62 :         if (!tmp) return -1;
     687           62 :         al->data = tmp;
     688           62 :         al->cap  = newcap;
     689              :     }
     690          101 :     al->data[al->count++] = att;
     691          101 :     return 0;
     692              : }
     693              : 
     694              : /* Forward declaration */
     695              : static void collect_parts(const char *msg, AttachList *al, int *unnamed_idx);
     696              : 
     697              : /* Walk a multipart body and collect attachments from each sub-part. */
     698           65 : static void collect_multipart_attachments(const char *msg, const char *ctype,
     699              :                                           AttachList *al, int *idx) {
     700           65 :     const char *b = strcasestr(ctype, "boundary=");
     701           65 :     if (!b) return;
     702           65 :     b += strlen("boundary=");
     703              : 
     704           65 :     char boundary[512] = {0};
     705           65 :     if (*b == '"') {
     706           64 :         b++;
     707           64 :         const char *end = strchr(b, '"');
     708           64 :         if (!end) return;
     709           64 :         snprintf(boundary, sizeof(boundary), "%.*s", (int)(end - b), b);
     710              :     } else {
     711            1 :         size_t i = 0;
     712            5 :         while (*b && *b != ';' && *b != ' ' && *b != '\r' && *b != '\n' &&
     713              :                i < sizeof(boundary) - 1)
     714            4 :             boundary[i++] = *b++;
     715            1 :         boundary[i] = '\0';
     716              :     }
     717           65 :     if (!boundary[0]) return;
     718              : 
     719              :     char delim[520];
     720           65 :     snprintf(delim, sizeof(delim), "--%s", boundary);
     721           65 :     size_t dlen = strlen(delim);
     722              : 
     723           65 :     const char *p = strstr(msg, delim);
     724          198 :     while (p) {
     725          198 :         p = strchr(p + dlen, '\n');
     726          198 :         if (!p) break;
     727          198 :         p++;
     728              : 
     729          198 :         const char *next = strstr(p, delim);
     730          198 :         if (!next) break;
     731              : 
     732          198 :         size_t partlen = (size_t)(next - p);
     733          198 :         char *part = strndup(p, partlen);
     734          198 :         if (!part) break;
     735          198 :         collect_parts(part, al, idx);
     736          198 :         free(part);
     737              : 
     738          198 :         p = next + dlen;
     739          198 :         if (p[0] == '-' && p[1] == '-') break;
     740          133 :         p = strchr(p, '\n');
     741          133 :         if (p) p++;
     742              :     }
     743              : }
     744              : 
     745              : /* Examine one MIME part (headers + body) and add to al if it is an attachment. */
     746          295 : static void collect_parts(const char *msg, AttachList *al, int *unnamed_idx) {
     747          295 :     char *ctype = mime_get_header(msg, "Content-Type");
     748          295 :     char *disp  = mime_get_header(msg, "Content-Disposition");
     749          295 :     char *enc   = mime_get_header(msg, "Content-Transfer-Encoding");
     750              : 
     751              :     /* Recurse into multipart containers */
     752          295 :     if (ctype && strncasecmp(ctype, "multipart/", 10) == 0) {
     753           65 :         collect_multipart_attachments(msg, ctype, al, unnamed_idx);
     754           65 :         free(ctype); free(disp); free(enc);
     755          194 :         return;
     756              :     }
     757              : 
     758              :     /* Determine filename from Content-Disposition or Content-Type name= */
     759          230 :     char *filename = NULL;
     760          230 :     int explicit_attach = 0;
     761          230 :     if (disp) {
     762          104 :         if (strncasecmp(disp, "attachment", 10) == 0) explicit_attach = 1;
     763          104 :         filename = extract_param(disp, "filename");
     764              :         /* RFC 5987: filename*=charset''encoded — simplified: strip trailing * */
     765          104 :         if (!filename) filename = extract_param(disp, "filename*");
     766              :     }
     767          230 :     if (!filename && ctype)
     768           80 :         filename = extract_param(ctype, "name");
     769              : 
     770              :     /* Skip non-attachment text and multipart parts unless explicitly marked */
     771          230 :     if (!explicit_attach) {
     772          128 :         if (!filename) {
     773          126 :             free(ctype); free(disp); free(enc);
     774          126 :             return;  /* no filename → body part, skip */
     775              :         }
     776              :         /* text/plain and text/html without attachment disposition are body parts */
     777            2 :         if (ctype && (strncasecmp(ctype, "text/plain", 10) == 0 ||
     778            1 :                       strncasecmp(ctype, "text/html",   9) == 0)) {
     779            2 :             free(ctype); free(disp); free(enc); free(filename);
     780            2 :             return;
     781              :         }
     782              :     }
     783              : 
     784          102 :     const char *body = body_start(msg);
     785          102 :     if (!body) {
     786            1 :         free(ctype); free(disp); free(enc); free(filename);
     787            1 :         return;
     788              :     }
     789              : 
     790              :     /* Decode body content; for base64 capture exact decoded byte count. */
     791          101 :     size_t data_size = 0;
     792              :     unsigned char *data;
     793          101 :     if (enc && strcasecmp(enc, "base64") == 0)
     794           95 :         data = (unsigned char *)decode_base64(body, strlen(body), &data_size);
     795              :     else {
     796            6 :         data = (unsigned char *)decode_transfer(body, strlen(body), enc);
     797            6 :         data_size = data ? strlen((char *)data) : 0;
     798              :     }
     799              : 
     800              :     /* Sanitise / generate filename */
     801          101 :     char *safe_name = NULL;
     802          101 :     if (filename) {
     803          100 :         char *decoded = mime_decode_words(filename);
     804          100 :         free(filename);
     805          100 :         safe_name = sanitise_filename(decoded ? decoded : "");
     806          100 :         free(decoded);
     807              :     }
     808          101 :     if (!safe_name) {
     809              :         char gen[32];
     810            1 :         snprintf(gen, sizeof(gen), "attachment-%d.bin", ++(*unnamed_idx));
     811            1 :         safe_name = strdup(gen);
     812              :     }
     813              : 
     814          101 :     MimeAttachment att = {0};
     815          101 :     att.filename     = safe_name;
     816          101 :     att.content_type = ctype ? strdup(ctype) : strdup("application/octet-stream");
     817          101 :     att.data         = data;
     818          101 :     att.size         = data_size;
     819              : 
     820          101 :     if (alist_push(al, att) < 0) {
     821            0 :         free(att.filename); free(att.content_type); free(att.data);
     822              :     }
     823              : 
     824          101 :     free(ctype); free(disp); free(enc);
     825              : }
     826              : 
     827           98 : MimeAttachment *mime_list_attachments(const char *msg, int *count_out) {
     828           98 :     if (!msg || !count_out) { if (count_out) *count_out = 0; return NULL; }
     829           97 :     AttachList al = {NULL, 0, 0};
     830           97 :     int idx = 0;
     831           97 :     collect_parts(msg, &al, &idx);
     832           97 :     *count_out = al.count;
     833           97 :     if (al.count == 0) { free(al.data); return NULL; }
     834           62 :     return al.data;
     835              : }
     836              : 
     837           85 : void mime_free_attachments(MimeAttachment *list, int count) {
     838           85 :     if (!list) return;
     839          127 :     for (int i = 0; i < count; i++) {
     840           77 :         free(list[i].filename);
     841           77 :         free(list[i].content_type);
     842           77 :         free(list[i].data);
     843              :     }
     844           50 :     free(list);
     845              : }
     846              : 
     847           12 : int mime_save_attachment(const MimeAttachment *att, const char *dest_path) {
     848           12 :     if (!att || !dest_path || !att->data) return -1;
     849           22 :     RAII_FILE FILE *f = fopen(dest_path, "wb");
     850           11 :     if (!f) return -1;
     851              :     /* Write the full decoded buffer; for base64 the NUL terminator is not
     852              :      * part of the content — use att->size if accurate, else strlen fallback. */
     853           11 :     size_t n = att->size > 0 ? att->size : strlen((char *)att->data);
     854           11 :     size_t written = fwrite(att->data, 1, n, f);
     855           11 :     return (written != n) ? -1 : 0;
     856              : }
     857              : 
     858            4 : char *mime_extract_imap_literal(const char *response) {
     859            4 :     if (!response) return NULL;
     860            3 :     const char *brace = strchr(response, '{');
     861            3 :     if (!brace) return NULL;
     862              : 
     863            2 :     char *end = NULL;
     864            2 :     long size = strtol(brace + 1, &end, 10);
     865            2 :     if (!end || *end != '}' || size <= 0) return NULL;
     866              : 
     867            2 :     const char *content = end + 1;
     868            2 :     if (*content == '\r') content++;
     869            2 :     if (*content == '\n') content++;
     870              : 
     871              :     // Safety check
     872            2 :     size_t avail = strlen(content);
     873            2 :     if (avail < (size_t)size) {
     874            1 :         return strndup(content, avail);
     875              :     }
     876              : 
     877            1 :     return strndup(content, (size_t)size);
     878              : }
     879              : 
     880         1441 : int mime_get_dmarc_status(const char *auth_results) {
     881         1441 :     if (!auth_results) return 0;
     882           20 :     const char *p = auth_results;
     883           22 :     while ((p = strcasestr(p, "dmarc=")) != NULL) {
     884           18 :         p += 6;
     885           18 :         if (strncasecmp(p, "pass",       4) == 0) return  1;
     886           11 :         if (strncasecmp(p, "fail",       4) == 0) return -1;
     887            5 :         if (strncasecmp(p, "temperror",  9) == 0) return -2;
     888              :     }
     889            4 :     return 0;
     890              : }
     891              : 
     892           78 : char *mime_describe_dmarc(const char *auth_results) {
     893           78 :     if (!auth_results)
     894           66 :         return strdup("Not evaluated (no Authentication-Results header)");
     895              : 
     896           12 :     const char *dpos = strcasestr(auth_results, "dmarc=");
     897           12 :     if (!dpos)
     898            1 :         return strdup("Not evaluated (no dmarc entry in Authentication-Results)");
     899              : 
     900           11 :     const char *r = dpos + 6;
     901              : 
     902              :     /* Extract result token */
     903              :     char result[32];
     904           11 :     size_t ri = 0;
     905           83 :     while (*r && *r != ' ' && *r != ';' && *r != '\t' && *r != '\r' && *r != '\n'
     906          147 :            && ri < sizeof(result) - 1)
     907           68 :         result[ri++] = *r++;
     908           11 :     result[ri] = '\0';
     909              : 
     910           11 :     if (strcasecmp(result, "pass") == 0) {
     911              :         /* Optionally include header.from= for clarity */
     912            2 :         const char *hf = strcasestr(dpos, "header.from=");
     913            2 :         if (hf) {
     914            1 :             hf += 12;
     915              :             char hfrom[64];
     916            1 :             size_t hfi = 0;
     917            9 :             while (*hf && *hf != ' ' && *hf != ';' && *hf != '\t' &&
     918           17 :                    *hf != '\r' && *hf != '\n' && hfi < sizeof(hfrom) - 1)
     919            8 :                 hfrom[hfi++] = *hf++;
     920            1 :             hfrom[hfi] = '\0';
     921            1 :             if (hfi > 0) {
     922            1 :                 char *out = NULL;
     923            1 :                 if (asprintf(&out, "Pass (header.from=%s)", hfrom) >= 0)
     924            1 :                     return out;
     925              :             }
     926              :         }
     927            1 :         return strdup("Pass");
     928              :     }
     929              : 
     930            9 :     if (strcasecmp(result, "fail") == 0) {
     931              :         /* Policy is in "(p=REJECT sp=... dis=...)" after the dmarc= token */
     932            4 :         char policy[32] = "";
     933            4 :         const char *pp = strcasestr(dpos, "(p=");
     934            4 :         if (pp) {
     935            3 :             pp += 3;
     936            3 :             size_t pi = 0;
     937           23 :             while (*pp && *pp != ')' && *pp != ' ' && *pp != ';' &&
     938              :                    pi < sizeof(policy) - 1)
     939           20 :                 policy[pi++] = *pp++;
     940            3 :             policy[pi] = '\0';
     941              :         }
     942            4 :         char *out = NULL;
     943            4 :         if (strcasecmp(policy, "reject") == 0)
     944            1 :             out = strdup("Fail — policy: reject (message rejected, likely spoofed)");
     945            3 :         else if (strcasecmp(policy, "quarantine") == 0)
     946            1 :             out = strdup("Fail — policy: quarantine (message may be spoofed or spam)");
     947            2 :         else if (strcasecmp(policy, "none") == 0)
     948            1 :             out = strdup("Fail — policy: none (monitoring only, no enforcement)");
     949            1 :         else if (policy[0]) {
     950            0 :             if (asprintf(&out, "Fail — policy: %s", policy) < 0) out = NULL;
     951              :         } else
     952            1 :             out = strdup("Fail (no policy information)");
     953            4 :         return out ? out : strdup("Fail");
     954              :     }
     955              : 
     956            5 :     if (strcasecmp(result, "none") == 0)
     957            1 :         return strdup("No record (domain has no DMARC policy)");
     958            4 :     if (strcasecmp(result, "bestguesspass") == 0)
     959            1 :         return strdup("Best-guess pass (no formal DMARC record, implicit pass)");
     960            3 :     if (strcasecmp(result, "temperror") == 0)
     961            1 :         return strdup("Temporary error (DNS lookup may have failed during evaluation)");
     962            2 :     if (strcasecmp(result, "permerror") == 0)
     963            1 :         return strdup("Permanent error (malformed or invalid DMARC record)");
     964              : 
     965            1 :     char *out = NULL;
     966            1 :     if (asprintf(&out, "Unknown result: %s", result) >= 0)
     967            1 :         return out;
     968            0 :     return strdup("Unknown");
     969              : }
        

Generated by: LCOV version 2.0-1