LCOV - code coverage report
Current view: top level - libemail/src/core - mime_util.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 69.3 % 613 425
Test Date: 2026-08-21 10:11:30 Functions: 93.5 % 31 29

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

Generated by: LCOV version 2.0-1