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 :
9 : /**
10 : * @file compose_service.c
11 : * @brief RFC 2822 message builder and reply metadata extractor.
12 : */
13 :
14 : /* ── Internal helpers ────────────────────────────────────────────────── */
15 :
16 : /** Convert LF-terminated body to CRLF. Returns heap string; caller frees. */
17 6 : static char *lf_to_crlf(const char *body) {
18 6 : if (!body) return strdup("");
19 : /* Count LFs to pre-size the output */
20 6 : size_t lf_count = 0;
21 132 : for (const char *p = body; *p; p++)
22 126 : if (*p == '\n') lf_count++;
23 6 : size_t blen = strlen(body);
24 6 : char *out = malloc(blen + lf_count + 1);
25 6 : if (!out) return NULL;
26 6 : char *q = out;
27 132 : for (const char *p = body; *p; p++) {
28 126 : if (*p == '\n' && (p == body || *(p-1) != '\r'))
29 2 : *q++ = '\r';
30 126 : *q++ = *p;
31 : }
32 6 : *q = '\0';
33 6 : return out;
34 : }
35 :
36 : /** Trim leading/trailing whitespace in-place (returns pointer into s). */
37 1 : static char *trim_ws(char *s) {
38 1 : while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') s++;
39 1 : char *end = s + strlen(s);
40 1 : while (end > s && (*(end-1) == ' ' || *(end-1) == '\t' ||
41 1 : *(end-1) == '\r' || *(end-1) == '\n'))
42 0 : end--;
43 1 : *end = '\0';
44 1 : return s;
45 : }
46 :
47 : /* ── Public API ──────────────────────────────────────────────────────── */
48 :
49 6 : int compose_build_message(const ComposeParams *p, char **out, size_t *outlen) {
50 6 : if (!p || !p->from || !p->to || !p->subject || !out || !outlen)
51 0 : return -1;
52 :
53 : /* Date header in RFC 2822 format */
54 6 : time_t now = time(NULL);
55 : struct tm tm_local;
56 6 : localtime_r(&now, &tm_local);
57 : char date_str[64];
58 6 : strftime(date_str, sizeof(date_str), "%a, %d %b %Y %H:%M:%S %z", &tm_local);
59 :
60 : /* Message-ID */
61 6 : char hostname[256] = "localhost";
62 6 : gethostname(hostname, sizeof(hostname));
63 : char msgid[512];
64 6 : snprintf(msgid, sizeof(msgid), "<%ld.%d@%s>", (long)now, (int)getpid(), hostname);
65 :
66 : /* In-Reply-To header (replies only) */
67 6 : char reply_hdr[512] = "";
68 6 : if (p->reply_to_msg_id && p->reply_to_msg_id[0])
69 0 : snprintf(reply_hdr, sizeof(reply_hdr),
70 : "In-Reply-To: %s\r\nReferences: %s\r\n",
71 0 : p->reply_to_msg_id, p->reply_to_msg_id);
72 :
73 : /* Cc/Bcc headers (optional) */
74 6 : char cc_hdr[600] = "";
75 6 : if (p->cc && p->cc[0])
76 0 : snprintf(cc_hdr, sizeof(cc_hdr), "Cc: %s\r\n", p->cc);
77 :
78 6 : char bcc_hdr[600] = "";
79 6 : if (p->bcc && p->bcc[0])
80 0 : snprintf(bcc_hdr, sizeof(bcc_hdr), "Bcc: %s\r\n", p->bcc);
81 :
82 : /* Convert body line endings to CRLF */
83 6 : char *body_crlf = lf_to_crlf(p->body ? p->body : "");
84 6 : if (!body_crlf) return -1;
85 :
86 : /* Assemble message */
87 6 : char *msg = NULL;
88 6 : int len = asprintf(&msg,
89 : "Date: %s\r\n"
90 : "From: %s\r\n"
91 : "To: %s\r\n"
92 : "%s"
93 : "%s"
94 : "Subject: %s\r\n"
95 : "Message-ID: %s\r\n"
96 : "%s"
97 : "MIME-Version: 1.0\r\n"
98 : "Content-Type: text/plain; charset=UTF-8\r\n"
99 : "Content-Transfer-Encoding: 8bit\r\n"
100 : "\r\n"
101 : "%s",
102 : date_str,
103 6 : p->from,
104 6 : p->to,
105 : cc_hdr,
106 : bcc_hdr,
107 6 : p->subject,
108 : msgid,
109 : reply_hdr,
110 : body_crlf);
111 6 : free(body_crlf);
112 :
113 6 : if (len < 0 || !msg) return -1;
114 6 : *out = msg;
115 6 : *outlen = (size_t)len;
116 6 : return 0;
117 : }
118 :
119 1 : int compose_extract_reply_meta(const char *raw_msg,
120 : char **reply_to_out,
121 : char **subject_out,
122 : char **msg_id_out) {
123 1 : if (!raw_msg || !reply_to_out || !subject_out || !msg_id_out)
124 0 : return -1;
125 :
126 1 : *reply_to_out = NULL;
127 1 : *subject_out = NULL;
128 1 : *msg_id_out = NULL;
129 :
130 : /* Prefer Reply-To header; fall back to From */
131 1 : char *rt = mime_get_header(raw_msg, "Reply-To");
132 1 : if (!rt || !rt[0]) {
133 1 : free(rt);
134 1 : rt = mime_get_header(raw_msg, "From");
135 : }
136 1 : *reply_to_out = rt ? mime_decode_words(rt) : strdup("");
137 1 : free(rt);
138 :
139 : /* Subject: prefix with "Re: " (strip duplicates) */
140 1 : char *subj_raw = mime_get_header(raw_msg, "Subject");
141 1 : char *subj_dec = subj_raw ? mime_decode_words(subj_raw) : strdup("");
142 1 : free(subj_raw);
143 1 : char *s = subj_dec ? trim_ws(subj_dec) : NULL;
144 : /* Strip all leading "Re: " / "re: " prefixes */
145 1 : while (s && (strncasecmp(s, "re: ", 4) == 0 || strncasecmp(s, "re:", 3) == 0)) {
146 0 : if (strncasecmp(s, "re: ", 4) == 0) s += 4;
147 0 : else s += 3;
148 0 : while (*s == ' ') s++;
149 : }
150 1 : char *subj_out = NULL;
151 1 : if (asprintf(&subj_out, "Re: %s", s ? s : "") < 0)
152 0 : subj_out = strdup("Re: ");
153 1 : free(subj_dec);
154 1 : *subject_out = subj_out;
155 :
156 : /* Message-ID */
157 1 : char *msgid = mime_get_header(raw_msg, "Message-ID");
158 1 : *msg_id_out = msgid ? msgid : strdup("");
159 :
160 1 : return 0;
161 : }
|