LCOV - code coverage report
Current view: top level - libwrite/src - compose_service.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 80.2 % 207 166
Test Date: 2026-08-21 10:11:30 Functions: 100.0 % 6 6

            Line data    Source code
       1              : #include "compose_service.h"
       2              : #include "mime_util.h"
       3              : #include <stdio.h>
       4              : #include <stdlib.h>
       5              : #include <string.h>
       6              : #include <time.h>
       7              : #include <unistd.h>
       8              : #include <sys/stat.h>
       9              : #include <libgen.h>
      10              : #include <ctype.h>
      11              : 
      12              : /**
      13              :  * @file compose_service.c
      14              :  * @brief RFC 2822 message builder and reply metadata extractor.
      15              :  */
      16              : 
      17              : /* ── Internal helpers ────────────────────────────────────────────────── */
      18              : 
      19              : /** Convert LF-terminated body to CRLF. Returns heap string; caller frees. */
      20           31 : static char *lf_to_crlf(const char *body) {
      21           31 :     if (!body) return strdup("");
      22              :     /* Count LFs to pre-size the output */
      23           31 :     size_t lf_count = 0;
      24         3440 :     for (const char *p = body; *p; p++)
      25         3409 :         if (*p == '\n') lf_count++;
      26           31 :     size_t blen = strlen(body);
      27           31 :     char *out = malloc(blen + lf_count + 1);
      28           31 :     if (!out) return NULL;
      29           31 :     char *q = out;
      30         3440 :     for (const char *p = body; *p; p++) {
      31         3409 :         if (*p == '\n' && (p == body || *(p-1) != '\r'))
      32          163 :             *q++ = '\r';
      33         3409 :         *q++ = *p;
      34              :     }
      35           31 :     *q = '\0';
      36           31 :     return out;
      37              : }
      38              : 
      39              : /** Trim leading/trailing whitespace in-place (returns pointer into s). */
      40           18 : static char *trim_ws(char *s) {
      41           18 :     while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') s++;
      42           18 :     char *end = s + strlen(s);
      43           18 :     while (end > s && (*(end-1) == ' ' || *(end-1) == '\t' ||
      44           18 :                        *(end-1) == '\r' || *(end-1) == '\n'))
      45            0 :         end--;
      46           18 :     *end = '\0';
      47           18 :     return s;
      48              : }
      49              : 
      50              : /* ── Attachment helpers ───────────────────────────────────────────────── */
      51              : 
      52              : /** Base64 encoding table. */
      53              : static const char b64tab[] =
      54              :     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
      55              : 
      56              : /**
      57              :  * Base64-encode `src` of `srclen` bytes into `dst`.
      58              :  * Lines are wrapped at 76 characters with CRLF.
      59              :  * `dst` must be large enough: ((srclen+2)/3)*4 + (((srclen+2)/3)*4/76+2)*2 + 4 bytes.
      60              :  * Returns number of bytes written (excluding NUL).
      61              :  */
      62            3 : static size_t base64_encode(const unsigned char *src, size_t srclen,
      63              :                              char *dst)
      64              : {
      65            3 :     size_t col = 0;
      66            3 :     char *d = dst;
      67              : 
      68           22 :     for (size_t i = 0; i < srclen; i += 3) {
      69           19 :         unsigned int b0 = src[i];
      70           19 :         unsigned int b1 = (i + 1 < srclen) ? src[i + 1] : 0;
      71           19 :         unsigned int b2 = (i + 2 < srclen) ? src[i + 2] : 0;
      72           19 :         size_t avail = srclen - i; /* bytes remaining in this group */
      73              : 
      74           19 :         *d++ = b64tab[(b0 >> 2) & 0x3F];
      75           19 :         *d++ = b64tab[((b0 & 0x03) << 4) | ((b1 >> 4) & 0x0F)];
      76           19 :         *d++ = (avail >= 2) ? b64tab[((b1 & 0x0F) << 2) | ((b2 >> 6) & 0x03)] : '=';
      77           19 :         *d++ = (avail >= 3) ? b64tab[b2 & 0x3F] : '=';
      78           19 :         col += 4;
      79              : 
      80           19 :         if (col >= 76) {
      81            0 :             *d++ = '\r';
      82            0 :             *d++ = '\n';
      83            0 :             col = 0;
      84              :         }
      85              :     }
      86              : 
      87              :     /* Trailing CRLF if we have a partial line */
      88            3 :     if (col > 0) {
      89            3 :         *d++ = '\r';
      90            3 :         *d++ = '\n';
      91              :     }
      92              : 
      93            3 :     *d = '\0';
      94            3 :     return (size_t)(d - dst);
      95              : }
      96              : 
      97              : /**
      98              :  * Infer MIME content type from file extension (case-insensitive).
      99              :  */
     100            3 : static const char *infer_mime_type(const char *filename)
     101              : {
     102            3 :     const char *dot = strrchr(filename, '.');
     103            3 :     if (!dot) return "application/octet-stream";
     104              : 
     105              :     /* Copy extension to lowercase */
     106              :     char ext[32];
     107              :     size_t i;
     108           12 :     for (i = 0; i < sizeof(ext) - 1 && dot[1 + i]; i++)
     109            9 :         ext[i] = (char)tolower((unsigned char)dot[1 + i]);
     110            3 :     ext[i] = '\0';
     111              : 
     112            3 :     if (strcmp(ext, "pdf")  == 0) return "application/pdf";
     113            2 :     if (strcmp(ext, "txt")  == 0) return "text/plain";
     114            0 :     if (strcmp(ext, "html") == 0) return "text/html";
     115            0 :     if (strcmp(ext, "htm")  == 0) return "text/html";
     116            0 :     if (strcmp(ext, "jpg")  == 0) return "image/jpeg";
     117            0 :     if (strcmp(ext, "jpeg") == 0) return "image/jpeg";
     118            0 :     if (strcmp(ext, "png")  == 0) return "image/png";
     119            0 :     if (strcmp(ext, "gif")  == 0) return "image/gif";
     120            0 :     if (strcmp(ext, "zip")  == 0) return "application/zip";
     121            0 :     if (strcmp(ext, "gz")   == 0) return "application/gzip";
     122            0 :     if (strcmp(ext, "csv")  == 0) return "text/csv";
     123            0 :     if (strcmp(ext, "json") == 0) return "application/json";
     124            0 :     if (strcmp(ext, "xml")  == 0) return "application/xml";
     125            0 :     return "application/octet-stream";
     126              : }
     127              : 
     128              : /* ── Public API ──────────────────────────────────────────────────────── */
     129              : 
     130           31 : int compose_build_message(const ComposeParams *p, char **out, size_t *outlen) {
     131           31 :     if (!p || !p->from || !p->to || !p->subject || !out || !outlen)
     132            0 :         return -1;
     133              : 
     134              :     /* Date header in RFC 2822 format */
     135           31 :     time_t now = time(NULL);
     136              :     struct tm tm_local;
     137           31 :     localtime_r(&now, &tm_local);
     138              :     char date_str[64];
     139           31 :     strftime(date_str, sizeof(date_str), "%a, %d %b %Y %H:%M:%S %z", &tm_local);
     140              : 
     141              :     /* Message-ID */
     142           31 :     char hostname[256] = "localhost";
     143           31 :     gethostname(hostname, sizeof(hostname));
     144              :     char msgid[512];
     145           31 :     snprintf(msgid, sizeof(msgid), "<%ld.%d@%s>", (long)now, (int)getpid(), hostname);
     146              : 
     147              :     /* In-Reply-To header (replies only) */
     148           31 :     char reply_hdr[512] = "";
     149           31 :     if (p->reply_to_msg_id && p->reply_to_msg_id[0])
     150            0 :         snprintf(reply_hdr, sizeof(reply_hdr),
     151              :                  "In-Reply-To: %s\r\nReferences: %s\r\n",
     152            0 :                  p->reply_to_msg_id, p->reply_to_msg_id);
     153              : 
     154              :     /* Cc/Bcc headers (optional) */
     155           31 :     char cc_hdr[600] = "";
     156           31 :     if (p->cc && p->cc[0])
     157            4 :         snprintf(cc_hdr, sizeof(cc_hdr), "Cc: %s\r\n", p->cc);
     158              : 
     159           31 :     char bcc_hdr[600] = "";
     160           31 :     if (p->bcc && p->bcc[0])
     161            2 :         snprintf(bcc_hdr, sizeof(bcc_hdr), "Bcc: %s\r\n", p->bcc);
     162              : 
     163              :     /* Convert body line endings to CRLF */
     164           31 :     char *body_crlf = lf_to_crlf(p->body ? p->body : "");
     165           31 :     if (!body_crlf) return -1;
     166              : 
     167              :     /* ── Multipart path (attachments present) ────────────────────────── */
     168           31 :     if (p->attachments != NULL && p->attach_count > 0) {
     169              : 
     170              :         /* 1. Validate all attachments before building anything */
     171            8 :         for (int ai = 0; ai < p->attach_count; ai++) {
     172              :             struct stat st;
     173            6 :             if (stat(p->attachments[ai], &st) != 0 || !S_ISREG(st.st_mode)) {
     174            2 :                 free(body_crlf);
     175            2 :                 *out = NULL;
     176            3 :                 return -1;
     177              :             }
     178            4 :             if (st.st_size > 26214400) { /* 25 MB */
     179            1 :                 free(body_crlf);
     180            1 :                 *out = NULL;
     181            1 :                 return -1;
     182              :             }
     183              :         }
     184              : 
     185              :         /* 2. Generate unique MIME boundary — include a monotonic counter so
     186              :          *    two calls in the same second get different boundaries. */
     187              :         static unsigned int boundary_seq = 0;
     188            2 :         unsigned int seq = __atomic_add_fetch(&boundary_seq, 1, __ATOMIC_RELAXED);
     189              :         char boundary[96];
     190            4 :         snprintf(boundary, sizeof(boundary), "=_%d_%ld_%u",
     191            2 :                  (int)getpid(), (long)now, seq);
     192              : 
     193              :         /* 3. Build multipart/mixed message using open_memstream */
     194            2 :         char *buf = NULL;
     195            2 :         size_t bufsz = 0;
     196            2 :         FILE *ms = open_memstream(&buf, &bufsz);
     197            2 :         if (!ms) {
     198            0 :             free(body_crlf);
     199            0 :             return -1;
     200              :         }
     201              : 
     202              :         /* Top-level headers */
     203            2 :         fprintf(ms, "Date: %s\r\n", date_str);
     204            2 :         fprintf(ms, "From: %s\r\n", p->from);
     205            2 :         fprintf(ms, "To: %s\r\n", p->to);
     206            2 :         if (cc_hdr[0])  fprintf(ms, "%s", cc_hdr);
     207            2 :         if (bcc_hdr[0]) fprintf(ms, "%s", bcc_hdr);
     208            2 :         fprintf(ms, "Subject: %s\r\n", p->subject);
     209            2 :         fprintf(ms, "Message-ID: %s\r\n", msgid);
     210            2 :         if (reply_hdr[0]) fprintf(ms, "%s", reply_hdr);
     211            2 :         fprintf(ms, "MIME-Version: 1.0\r\n");
     212            2 :         fprintf(ms, "Content-Type: multipart/mixed; boundary=\"%s\"\r\n", boundary);
     213            2 :         fprintf(ms, "\r\n");
     214              : 
     215              :         /* First MIME part: text/plain body */
     216            2 :         fprintf(ms, "--%s\r\n", boundary);
     217            2 :         fprintf(ms, "Content-Type: text/plain; charset=UTF-8\r\n");
     218            2 :         fprintf(ms, "Content-Transfer-Encoding: 8bit\r\n");
     219            2 :         fprintf(ms, "\r\n");
     220            2 :         fprintf(ms, "%s", body_crlf);
     221            2 :         fprintf(ms, "\r\n");
     222              : 
     223              :         /* Attachment parts */
     224            5 :         for (int ai = 0; ai < p->attach_count; ai++) {
     225            3 :             const char *fpath = p->attachments[ai];
     226              : 
     227              :             /* basename — work on a copy since basename() may modify */
     228              :             char path_copy[4096];
     229            3 :             snprintf(path_copy, sizeof(path_copy), "%s", fpath);
     230            3 :             const char *fname = basename(path_copy);
     231              : 
     232            3 :             const char *mime_type = infer_mime_type(fname);
     233              : 
     234              :             /* Read file contents */
     235            3 :             FILE *f = fopen(fpath, "rb");
     236            3 :             if (!f) {
     237            0 :                 fclose(ms);
     238            0 :                 free(buf);
     239            0 :                 free(body_crlf);
     240            0 :                 *out = NULL;
     241            0 :                 return -1;
     242              :             }
     243              :             struct stat st;
     244            3 :             stat(fpath, &st);
     245            3 :             size_t fsize = (size_t)st.st_size;
     246            3 :             unsigned char *fdata = malloc(fsize);
     247            3 :             if (!fdata) {
     248            0 :                 fclose(f);
     249            0 :                 fclose(ms);
     250            0 :                 free(buf);
     251            0 :                 free(body_crlf);
     252            0 :                 return -1;
     253              :             }
     254            3 :             size_t nread = fread(fdata, 1, fsize, f);
     255            3 :             fclose(f);
     256              : 
     257              :             /* Base64 encode: worst case = ceil(nread/3)*4 + 2*(ceil(nread*4/3)/76+2) */
     258            3 :             size_t b64_max = ((nread + 2) / 3) * 4 + ((((nread + 2) / 3) * 4) / 76 + 2) * 2 + 4;
     259            3 :             char *b64 = malloc(b64_max + 1);
     260            3 :             if (!b64) {
     261            0 :                 free(fdata);
     262            0 :                 fclose(ms);
     263            0 :                 free(buf);
     264            0 :                 free(body_crlf);
     265            0 :                 return -1;
     266              :             }
     267            3 :             base64_encode(fdata, nread, b64);
     268            3 :             free(fdata);
     269              : 
     270            3 :             fprintf(ms, "--%s\r\n", boundary);
     271            3 :             fprintf(ms, "Content-Type: %s\r\n", mime_type);
     272            3 :             fprintf(ms, "Content-Transfer-Encoding: base64\r\n");
     273            3 :             fprintf(ms, "Content-Disposition: attachment; filename=\"%s\"\r\n", fname);
     274            3 :             fprintf(ms, "\r\n");
     275            3 :             fprintf(ms, "%s", b64);
     276            3 :             fprintf(ms, "\r\n");
     277            3 :             free(b64);
     278              :         }
     279              : 
     280              :         /* Closing boundary */
     281            2 :         fprintf(ms, "--%s--\r\n", boundary);
     282              : 
     283            2 :         fclose(ms);
     284            2 :         free(body_crlf);
     285              : 
     286            2 :         *out    = buf;
     287            2 :         *outlen = bufsz;
     288            2 :         return 0;
     289              :     }
     290              : 
     291              :     /* ── Plain text path (no attachments) ───────────────────────────── */
     292              : 
     293              :     /* Assemble message */
     294           26 :     char *msg = NULL;
     295           26 :     int len = asprintf(&msg,
     296              :         "Date: %s\r\n"
     297              :         "From: %s\r\n"
     298              :         "To: %s\r\n"
     299              :         "%s"
     300              :         "%s"
     301              :         "Subject: %s\r\n"
     302              :         "Message-ID: %s\r\n"
     303              :         "%s"
     304              :         "MIME-Version: 1.0\r\n"
     305              :         "Content-Type: text/plain; charset=UTF-8\r\n"
     306              :         "Content-Transfer-Encoding: 8bit\r\n"
     307              :         "\r\n"
     308              :         "%s",
     309              :         date_str,
     310           26 :         p->from,
     311           26 :         p->to,
     312              :         cc_hdr,
     313              :         bcc_hdr,
     314           26 :         p->subject,
     315              :         msgid,
     316              :         reply_hdr,
     317              :         body_crlf);
     318           26 :     free(body_crlf);
     319              : 
     320           26 :     if (len < 0 || !msg) return -1;
     321           26 :     *out    = msg;
     322           26 :     *outlen = (size_t)len;
     323           26 :     return 0;
     324              : }
     325              : 
     326           18 : int compose_extract_reply_meta(const char *raw_msg,
     327              :                                 char **reply_to_out,
     328              :                                 char **subject_out,
     329              :                                 char **msg_id_out) {
     330           18 :     if (!raw_msg || !reply_to_out || !subject_out || !msg_id_out)
     331            0 :         return -1;
     332              : 
     333           18 :     *reply_to_out = NULL;
     334           18 :     *subject_out  = NULL;
     335           18 :     *msg_id_out   = NULL;
     336              : 
     337              :     /* Prefer Reply-To header; fall back to From */
     338           18 :     char *rt = mime_get_header(raw_msg, "Reply-To");
     339           18 :     if (!rt || !rt[0]) {
     340           18 :         free(rt);
     341           18 :         rt = mime_get_header(raw_msg, "From");
     342              :     }
     343           18 :     *reply_to_out = rt ? mime_decode_words(rt) : strdup("");
     344           18 :     free(rt);
     345              : 
     346              :     /* Subject: prefix with "Re: " (strip duplicates) */
     347           18 :     char *subj_raw = mime_get_header(raw_msg, "Subject");
     348           18 :     char *subj_dec = subj_raw ? mime_decode_words(subj_raw) : strdup("");
     349           18 :     free(subj_raw);
     350           18 :     char *s = subj_dec ? trim_ws(subj_dec) : NULL;
     351              :     /* Strip all leading "Re: " / "re: " prefixes */
     352           18 :     while (s && (strncasecmp(s, "re: ", 4) == 0 || strncasecmp(s, "re:", 3) == 0)) {
     353            0 :         if (strncasecmp(s, "re: ", 4) == 0) s += 4;
     354            0 :         else s += 3;
     355            0 :         while (*s == ' ') s++;
     356              :     }
     357           18 :     char *subj_out = NULL;
     358           18 :     if (asprintf(&subj_out, "Re: %s", s ? s : "") < 0)
     359            0 :         subj_out = strdup("Re: ");
     360           18 :     free(subj_dec);
     361           18 :     *subject_out = subj_out;
     362              : 
     363              :     /* Message-ID */
     364           18 :     char *msgid = mime_get_header(raw_msg, "Message-ID");
     365           18 :     *msg_id_out = msgid ? msgid : strdup("");
     366              : 
     367           18 :     return 0;
     368              : }
        

Generated by: LCOV version 2.0-1