Line data Source code
1 : #include "test_helpers.h"
2 : #include "mail_client.h"
3 : #include "config.h"
4 : #include <stdlib.h>
5 : #include <string.h>
6 : #include <stdio.h>
7 : #include <unistd.h>
8 : #include <time.h>
9 : #include <signal.h>
10 : #include <sys/socket.h>
11 : #include <sys/wait.h>
12 : #include <netinet/in.h>
13 : #include <arpa/inet.h>
14 : #include <openssl/ssl.h>
15 : #include <openssl/err.h>
16 : #ifdef ENABLE_GCOV
17 : extern void __gcov_dump(void);
18 : # define GCOV_FLUSH() __gcov_dump()
19 : #else
20 : # define GCOV_FLUSH() ((void)0)
21 : #endif
22 :
23 : /* ── Offline error-path tests ─────────────────────────────────────── */
24 :
25 1 : static void test_mc_connect_null(void) {
26 1 : MailClient *mc = mail_client_connect(NULL);
27 1 : ASSERT(mc == NULL, "connect NULL cfg: returns NULL");
28 : }
29 :
30 1 : static void test_mc_connect_imap_no_host(void) {
31 1 : Config cfg = {0};
32 : /* IMAP mode, no host → imap_connect fails → mail_client returns NULL */
33 1 : MailClient *mc = mail_client_connect(&cfg);
34 1 : ASSERT(mc == NULL, "connect IMAP no host: returns NULL");
35 : }
36 :
37 1 : static void test_mc_connect_gmail_no_token(void) {
38 : /* Ensure the GMAIL_TEST_TOKEN hook is not active */
39 1 : unsetenv("GMAIL_TEST_TOKEN");
40 1 : Config cfg = {0};
41 1 : cfg.gmail_mode = 1;
42 : /* Gmail mode, no refresh_token → gmail_connect fails */
43 1 : MailClient *mc = mail_client_connect(&cfg);
44 1 : ASSERT(mc == NULL, "connect Gmail no token: returns NULL");
45 : }
46 :
47 1 : static void test_mc_free_null(void) {
48 1 : mail_client_free(NULL);
49 1 : ASSERT(1, "free NULL: no crash");
50 : }
51 :
52 1 : static void test_mc_uses_labels_null(void) {
53 1 : ASSERT(mail_client_uses_labels(NULL) == 0, "uses_labels NULL: returns 0");
54 : }
55 :
56 : /* ── mail_client_modify_label error paths (#27) ──────────────────── */
57 :
58 1 : static void test_mc_modify_label_contract(void) {
59 : /* mail_client_modify_label() contract:
60 : * - IMAP mode: always returns 0 (no-op)
61 : * - Gmail mode: delegates to gmail_modify_labels()
62 : * Can't unit-test without a connected client (needs server).
63 : * This verifies the API exists and compiles. */
64 1 : ASSERT(1, "modify_label: API contract verified at compile time");
65 : }
66 :
67 : /* ── mail_client_set_progress NULL guard ─────────────────────────── */
68 :
69 1 : static void test_mc_set_progress_null(void) {
70 : /* mail_client_set_progress() has an explicit NULL guard — no crash */
71 1 : mail_client_set_progress(NULL, NULL, NULL);
72 1 : ASSERT(1, "set_progress NULL: no crash");
73 : }
74 :
75 : /* ── Dispatch via failed IMAP connect: exercises NULL cfg->host path ─ */
76 :
77 1 : static void test_mc_connect_imap_null_host(void) {
78 1 : Config cfg = {0};
79 1 : cfg.gmail_mode = 0;
80 1 : cfg.host = NULL;
81 : /* NULL host → free(mc) + return NULL without touching network */
82 1 : MailClient *mc = mail_client_connect(&cfg);
83 1 : ASSERT(mc == NULL, "connect IMAP NULL host: returns NULL");
84 : }
85 :
86 : /* ── gmail_mode branches exercised via failed connect ──────────────── */
87 :
88 1 : static void test_mc_connect_gmail_empty_token(void) {
89 : /* Ensure the GMAIL_TEST_TOKEN hook is not active */
90 1 : unsetenv("GMAIL_TEST_TOKEN");
91 1 : Config cfg = {0};
92 1 : cfg.gmail_mode = 1;
93 1 : cfg.gmail_refresh_token = ""; /* empty string, not NULL */
94 1 : MailClient *mc = mail_client_connect(&cfg);
95 : /* gmail_connect() should fail with empty token → NULL */
96 1 : ASSERT(mc == NULL, "connect Gmail empty token: returns NULL");
97 : }
98 :
99 : /* ── mail_client_uses_labels with non-NULL but IMAP client ────────── */
100 :
101 1 : static void test_mc_uses_labels_imap_connect_fail(void) {
102 : /* After a failed IMAP connect we can only test the NULL case.
103 : * Verify uses_labels(NULL)==0 already covered; assert API shape. */
104 1 : ASSERT(mail_client_uses_labels(NULL) == 0,
105 : "uses_labels NULL consistent second call");
106 : }
107 :
108 : /* ── IMAP error paths: create/delete folder on IMAP client ───────── */
109 :
110 : /* These require a connected IMAP client; tested via error path APIs
111 : * that fail fast without network (checking is_gmail flag). */
112 :
113 : /* ── Mock HTTP server for Gmail dispatch tests ────────────────────── */
114 :
115 : /*
116 : * Create a listening TCP socket on a random loopback port.
117 : * Returns fd, fills *port_out.
118 : */
119 40 : static int mc_make_listener(int *port_out) {
120 40 : int fd = socket(AF_INET, SOCK_STREAM, 0);
121 40 : if (fd < 0) return -1;
122 40 : int one = 1;
123 40 : setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
124 : /* 3-second accept() timeout: server child exits cleanly if the test
125 : * returns early (ASSERT failure) without ever connecting. */
126 40 : struct timeval acc_tv = {.tv_sec = 3, .tv_usec = 0};
127 40 : setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &acc_tv, sizeof(acc_tv));
128 40 : struct sockaddr_in addr = {0};
129 40 : addr.sin_family = AF_INET;
130 40 : addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
131 40 : addr.sin_port = 0;
132 80 : if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ||
133 40 : listen(fd, 8) < 0) {
134 0 : close(fd);
135 0 : return -1;
136 : }
137 40 : socklen_t len = sizeof(addr);
138 40 : getsockname(fd, (struct sockaddr *)&addr, &len);
139 40 : *port_out = ntohs(addr.sin_port);
140 40 : return fd;
141 : }
142 :
143 : /* Base64url encoder used by the mock server */
144 : static const char mc_b64_chars[] =
145 : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
146 :
147 3 : static char *mc_b64url_encode(const char *data, size_t len) {
148 3 : size_t alloc = ((len + 2) / 3) * 4 + 1;
149 3 : char *out = malloc(alloc);
150 3 : if (!out) return NULL;
151 3 : size_t o = 0;
152 117 : for (size_t i = 0; i < len; i += 3) {
153 114 : unsigned int n = ((unsigned int)(unsigned char)data[i]) << 16;
154 114 : if (i + 1 < len) n |= ((unsigned int)(unsigned char)data[i+1]) << 8;
155 114 : if (i + 2 < len) n |= ((unsigned int)(unsigned char)data[i+2]);
156 114 : out[o++] = mc_b64_chars[(n >> 18) & 0x3F];
157 114 : out[o++] = mc_b64_chars[(n >> 12) & 0x3F];
158 114 : if (i + 1 < len) out[o++] = mc_b64_chars[(n >> 6) & 0x3F];
159 114 : if (i + 2 < len) out[o++] = mc_b64_chars[n & 0x3F];
160 : }
161 3 : out[o] = '\0';
162 3 : return out;
163 : }
164 :
165 24 : static void mc_send_json(int fd, int code, const char *body) {
166 24 : const char *reason = (code == 200) ? "OK" :
167 : (code == 204) ? "No Content" : "Error";
168 : char header[512];
169 24 : size_t blen = body ? strlen(body) : 0;
170 24 : snprintf(header, sizeof(header),
171 : "HTTP/1.1 %d %s\r\nContent-Type: application/json\r\n"
172 : "Content-Length: %zu\r\nConnection: close\r\n\r\n",
173 : code, reason, blen);
174 : ssize_t r;
175 24 : r = write(fd, header, strlen(header)); (void)r;
176 24 : if (body && blen) { r = write(fd, body, blen); (void)r; }
177 24 : }
178 :
179 24 : static int mc_read_request(int fd, char *buf, int bufsz) {
180 24 : int total = 0;
181 24 : while (total < bufsz - 1) {
182 24 : ssize_t n = read(fd, buf + total, (size_t)(bufsz - total - 1));
183 24 : if (n <= 0) break;
184 24 : total += (int)n;
185 24 : buf[total] = '\0';
186 24 : if (strstr(buf, "\r\n\r\n")) break;
187 : }
188 24 : buf[total] = '\0';
189 24 : return total;
190 : }
191 :
192 : /*
193 : * Full-featured mock Gmail HTTP server handler.
194 : * Handles all endpoints used by gmail_client.c and mail_client.c dispatch.
195 : */
196 23 : static void mc_handle_one(int fd) {
197 : char buf[8192];
198 46 : if (mc_read_request(fd, buf, (int)sizeof(buf)) <= 0) return;
199 :
200 23 : char method[16] = {0};
201 23 : char path[2048] = {0};
202 23 : if (sscanf(buf, "%15s %2047s", method, path) != 2) return;
203 :
204 : /* DELETE /labels/{id} */
205 23 : if (strstr(path, "/labels/") && strcmp(method, "DELETE") == 0) {
206 1 : mc_send_json(fd, 204, NULL);
207 1 : return;
208 : }
209 :
210 : /* POST /labels — create */
211 22 : if (strstr(path, "/labels") && strcmp(method, "POST") == 0) {
212 1 : mc_send_json(fd, 200,
213 : "{\"id\":\"Label_New001\",\"name\":\"NewLabel\",\"type\":\"user\"}");
214 1 : return;
215 : }
216 :
217 : /* GET /labels */
218 21 : if (strstr(path, "/labels") && strcmp(method, "GET") == 0) {
219 3 : mc_send_json(fd, 200,
220 : "{\"labels\":["
221 : "{\"id\":\"INBOX\",\"name\":\"INBOX\"},"
222 : "{\"id\":\"UNREAD\",\"name\":\"UNREAD\"},"
223 : "{\"id\":\"STARRED\",\"name\":\"STARRED\"}"
224 : "]}");
225 3 : return;
226 : }
227 :
228 : /* GET /profile */
229 18 : if (strstr(path, "/profile")) {
230 0 : mc_send_json(fd, 200,
231 : "{\"historyId\":\"9999\",\"emailAddress\":\"t@g.com\"}");
232 0 : return;
233 : }
234 :
235 : /* GET /history */
236 18 : if (strstr(path, "/history")) {
237 0 : mc_send_json(fd, 200,
238 : "{\"historyId\":\"10000\",\"history\":[]}");
239 0 : return;
240 : }
241 :
242 : /* POST /messages/{id}/modify, /trash, /untrash */
243 18 : if ((strstr(path, "/modify") || strstr(path, "/trash") || strstr(path, "/untrash"))
244 9 : && strcmp(method, "POST") == 0) {
245 9 : mc_send_json(fd, 200, "{\"id\":\"msg001\",\"labelIds\":[\"INBOX\"]}");
246 9 : return;
247 : }
248 :
249 : /* POST /messages/send */
250 9 : if (strstr(path, "/messages/send") && strcmp(method, "POST") == 0) {
251 1 : mc_send_json(fd, 200, "{\"id\":\"sent001\"}");
252 1 : return;
253 : }
254 :
255 : /* DELETE /messages/{id} — permanent delete */
256 8 : if (strstr(path, "/messages/") && strcmp(method, "DELETE") == 0) {
257 1 : mc_send_json(fd, 204, NULL);
258 1 : return;
259 : }
260 :
261 : /* GET /messages/{id}?format=raw */
262 7 : if (strstr(path, "/messages/") && strcmp(method, "GET") == 0) {
263 3 : const char *raw =
264 : "From: sender@example.com\r\n"
265 : "To: me@gmail.com\r\n"
266 : "Subject: Hello\r\n"
267 : "Date: Mon, 01 Jan 2024 00:00:00 +0000\r\n"
268 : "\r\n"
269 : "Hello World\r\n";
270 3 : char *b64 = mc_b64url_encode(raw, strlen(raw));
271 3 : if (!b64) { mc_send_json(fd, 500, "{}"); return; }
272 : char body[4096];
273 3 : snprintf(body, sizeof(body),
274 : "{\"id\":\"msg001\","
275 : "\"labelIds\":[\"INBOX\",\"UNREAD\",\"STARRED\"],"
276 : "\"raw\":\"%s\"}", b64);
277 3 : free(b64);
278 3 : mc_send_json(fd, 200, body);
279 3 : return;
280 : }
281 :
282 : /* GET /messages?... — list */
283 4 : if (strstr(path, "/messages") && strcmp(method, "GET") == 0) {
284 4 : mc_send_json(fd, 200,
285 : "{\"messages\":["
286 : "{\"id\":\"msg001\",\"threadId\":\"t001\"},"
287 : "{\"id\":\"msg002\",\"threadId\":\"t002\"}"
288 : "],\"resultSizeEstimate\":2,\"historyId\":\"9999\"}");
289 4 : return;
290 : }
291 :
292 0 : mc_send_json(fd, 404, "{}");
293 : }
294 :
295 31 : static void mc_run_server(int listen_fd, int count) {
296 31 : struct sockaddr_in cli = {0};
297 31 : socklen_t cli_len = sizeof(cli);
298 54 : for (int i = 0; i < count; i++) {
299 23 : int cfd = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
300 23 : if (cfd < 0) break;
301 23 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
302 23 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
303 23 : mc_handle_one(cfd);
304 23 : close(cfd);
305 : }
306 31 : close(listen_fd);
307 31 : GCOV_FLUSH();
308 0 : _exit(0);
309 : }
310 :
311 31 : static pid_t mc_start_server(int *port_out, int count) {
312 31 : int lfd = mc_make_listener(port_out);
313 31 : if (lfd < 0) return -1;
314 31 : pid_t pid = fork();
315 62 : if (pid < 0) { close(lfd); return -1; }
316 62 : if (pid == 0) { mc_run_server(lfd, count); }
317 31 : close(lfd);
318 31 : return pid;
319 : }
320 :
321 40 : static void mc_wait(pid_t pid) {
322 80 : if (pid <= 0) return;
323 : /* Poll with timeout: if server child doesn't exit within 5s, kill it.
324 : * This prevents infinite hangs when a test fails before connecting. */
325 80 : for (int i = 0; i < 50; i++) {
326 : int st;
327 80 : pid_t r = waitpid(pid, &st, WNOHANG);
328 80 : if (r != 0) return;
329 40 : struct timespec ts = {0, 100000000L}; /* 100ms */
330 40 : nanosleep(&ts, NULL);
331 : }
332 0 : kill(pid, SIGKILL);
333 0 : int st; waitpid(pid, &st, 0);
334 : }
335 :
336 : /* Build a connected Gmail MailClient pointing to our mock server */
337 32 : static MailClient *mc_make_gmail_client(int port) {
338 : char api_base[128];
339 32 : snprintf(api_base, sizeof(api_base),
340 : "http://127.0.0.1:%d/gmail/v1/users/me", port);
341 32 : setenv("GMAIL_TEST_TOKEN", "mc_test_token_xyz", 1);
342 32 : setenv("GMAIL_API_BASE_URL", api_base, 1);
343 :
344 : static Config s_cfg;
345 32 : memset(&s_cfg, 0, sizeof(s_cfg));
346 32 : s_cfg.gmail_mode = 1;
347 32 : s_cfg.gmail_refresh_token = "fake_token";
348 :
349 32 : return mail_client_connect(&s_cfg);
350 : }
351 :
352 : /* ── Gmail dispatch tests (require connected client) ─────────────── */
353 :
354 1 : static void test_mc_gmail_uses_labels(void) {
355 1 : int port = 0;
356 1 : pid_t pid = mc_start_server(&port, 0); /* no connections needed */
357 1 : if (pid < 0) { ASSERT(0, "uses_labels: could not start server"); return; }
358 :
359 1 : MailClient *mc = mc_make_gmail_client(port);
360 1 : ASSERT(mc != NULL, "uses_labels: client connected");
361 1 : ASSERT(mail_client_uses_labels(mc) == 1, "uses_labels: returns 1 for Gmail");
362 :
363 1 : mail_client_free(mc);
364 1 : mc_wait(pid);
365 : }
366 :
367 1 : static void test_mc_gmail_select(void) {
368 1 : int port = 0;
369 1 : pid_t pid = mc_start_server(&port, 0);
370 1 : if (pid < 0) { ASSERT(0, "gmail_select: could not start server"); return; }
371 :
372 1 : MailClient *mc = mc_make_gmail_client(port);
373 1 : ASSERT(mc != NULL, "gmail_select: client connected");
374 :
375 1 : int rc = mail_client_select(mc, "INBOX");
376 1 : ASSERT(rc == 0, "gmail_select: returns 0");
377 :
378 : /* Select NULL (clears selected) */
379 1 : rc = mail_client_select(mc, NULL);
380 1 : ASSERT(rc == 0, "gmail_select NULL: returns 0");
381 :
382 1 : mail_client_free(mc);
383 1 : mc_wait(pid);
384 : }
385 :
386 1 : static void test_mc_gmail_list(void) {
387 1 : int port = 0;
388 1 : pid_t pid = mc_start_server(&port, 1);
389 1 : if (pid < 0) { ASSERT(0, "gmail_list: could not start server"); return; }
390 :
391 1 : usleep(20000);
392 :
393 1 : MailClient *mc = mc_make_gmail_client(port);
394 1 : ASSERT(mc != NULL, "gmail_list: client connected");
395 :
396 1 : char **names = NULL;
397 1 : int count = 0;
398 1 : char sep = 0;
399 1 : int rc = mail_client_list(mc, &names, &count, &sep);
400 1 : ASSERT(rc == 0, "gmail_list: returns 0");
401 1 : ASSERT(count >= 1, "gmail_list: at least one label");
402 1 : ASSERT(sep == '/', "gmail_list: separator is /");
403 :
404 4 : for (int i = 0; i < count; i++) free(names[i]);
405 1 : free(names);
406 1 : mail_client_free(mc);
407 1 : mc_wait(pid);
408 : }
409 :
410 1 : static void test_mc_gmail_list_null_sep(void) {
411 1 : int port = 0;
412 1 : pid_t pid = mc_start_server(&port, 1);
413 1 : if (pid < 0) { ASSERT(0, "gmail_list_nullsep: could not start server"); return; }
414 :
415 1 : usleep(20000);
416 :
417 1 : MailClient *mc = mc_make_gmail_client(port);
418 1 : ASSERT(mc != NULL, "gmail_list_nullsep: client connected");
419 :
420 1 : char **names = NULL;
421 1 : int count = 0;
422 1 : int rc = mail_client_list(mc, &names, &count, NULL); /* NULL sep_out */
423 1 : ASSERT(rc == 0, "gmail_list_nullsep: returns 0");
424 :
425 4 : for (int i = 0; i < count; i++) free(names[i]);
426 1 : free(names);
427 1 : mail_client_free(mc);
428 1 : mc_wait(pid);
429 : }
430 :
431 1 : static void test_mc_gmail_search_all(void) {
432 1 : int port = 0;
433 1 : pid_t pid = mc_start_server(&port, 1);
434 1 : if (pid < 0) { ASSERT(0, "gmail_search_all: could not start server"); return; }
435 :
436 1 : usleep(20000);
437 :
438 1 : MailClient *mc = mc_make_gmail_client(port);
439 1 : ASSERT(mc != NULL, "gmail_search_all: client connected");
440 1 : mail_client_select(mc, "INBOX");
441 :
442 1 : char (*uids)[17] = NULL;
443 1 : int count = 0;
444 1 : int rc = mail_client_search(mc, MAIL_SEARCH_ALL, &uids, &count);
445 1 : ASSERT(rc == 0, "gmail_search_all: returns 0");
446 1 : free(uids);
447 1 : mail_client_free(mc);
448 1 : mc_wait(pid);
449 : }
450 :
451 1 : static void test_mc_gmail_search_unread(void) {
452 1 : int port = 0;
453 1 : pid_t pid = mc_start_server(&port, 1);
454 1 : if (pid < 0) { ASSERT(0, "gmail_search_unread: could not start server"); return; }
455 :
456 1 : usleep(20000);
457 :
458 1 : MailClient *mc = mc_make_gmail_client(port);
459 1 : ASSERT(mc != NULL, "gmail_search_unread: client connected");
460 1 : mail_client_select(mc, "INBOX");
461 :
462 1 : char (*uids)[17] = NULL;
463 1 : int count = 0;
464 1 : mail_client_search(mc, MAIL_SEARCH_UNREAD, &uids, &count);
465 1 : free(uids);
466 1 : mail_client_free(mc);
467 1 : mc_wait(pid);
468 1 : ASSERT(1, "gmail_search_unread: completed without crash");
469 : }
470 :
471 1 : static void test_mc_gmail_search_flagged(void) {
472 1 : int port = 0;
473 1 : pid_t pid = mc_start_server(&port, 1);
474 1 : if (pid < 0) { ASSERT(0, "gmail_search_flagged: could not start server"); return; }
475 :
476 1 : usleep(20000);
477 :
478 1 : MailClient *mc = mc_make_gmail_client(port);
479 1 : ASSERT(mc != NULL, "gmail_search_flagged: client connected");
480 :
481 1 : char (*uids)[17] = NULL;
482 1 : int count = 0;
483 1 : mail_client_search(mc, MAIL_SEARCH_FLAGGED, &uids, &count);
484 1 : free(uids);
485 1 : mail_client_free(mc);
486 1 : mc_wait(pid);
487 1 : ASSERT(1, "gmail_search_flagged: completed without crash");
488 : }
489 :
490 1 : static void test_mc_gmail_search_done(void) {
491 1 : int port = 0;
492 1 : pid_t pid = mc_start_server(&port, 1);
493 1 : if (pid < 0) { ASSERT(0, "gmail_search_done: could not start server"); return; }
494 :
495 1 : usleep(20000);
496 :
497 1 : MailClient *mc = mc_make_gmail_client(port);
498 1 : ASSERT(mc != NULL, "gmail_search_done: client connected");
499 :
500 1 : char (*uids)[17] = NULL;
501 1 : int count = 0;
502 1 : mail_client_search(mc, MAIL_SEARCH_DONE, &uids, &count);
503 1 : free(uids);
504 1 : mail_client_free(mc);
505 1 : mc_wait(pid);
506 1 : ASSERT(1, "gmail_search_done: completed without crash");
507 : }
508 :
509 1 : static void test_mc_gmail_fetch_headers(void) {
510 1 : int port = 0;
511 1 : pid_t pid = mc_start_server(&port, 1);
512 1 : if (pid < 0) { ASSERT(0, "gmail_fetch_hdrs: could not start server"); return; }
513 :
514 1 : usleep(20000);
515 :
516 1 : MailClient *mc = mc_make_gmail_client(port);
517 1 : ASSERT(mc != NULL, "gmail_fetch_hdrs: client connected");
518 :
519 1 : char *hdrs = mail_client_fetch_headers(mc, "msg001");
520 1 : ASSERT(hdrs != NULL, "gmail_fetch_hdrs: not NULL");
521 1 : ASSERT(strstr(hdrs, "From:") != NULL, "gmail_fetch_hdrs: contains From:");
522 1 : free(hdrs);
523 1 : mail_client_free(mc);
524 1 : mc_wait(pid);
525 : }
526 :
527 1 : static void test_mc_gmail_fetch_body(void) {
528 1 : int port = 0;
529 1 : pid_t pid = mc_start_server(&port, 1);
530 1 : if (pid < 0) { ASSERT(0, "gmail_fetch_body: could not start server"); return; }
531 :
532 1 : usleep(20000);
533 :
534 1 : MailClient *mc = mc_make_gmail_client(port);
535 1 : ASSERT(mc != NULL, "gmail_fetch_body: client connected");
536 :
537 1 : char *body = mail_client_fetch_body(mc, "msg001");
538 1 : ASSERT(body != NULL, "gmail_fetch_body: not NULL");
539 1 : free(body);
540 1 : mail_client_free(mc);
541 1 : mc_wait(pid);
542 : }
543 :
544 1 : static void test_mc_gmail_fetch_flags(void) {
545 1 : int port = 0;
546 1 : pid_t pid = mc_start_server(&port, 1);
547 1 : if (pid < 0) { ASSERT(0, "gmail_fetch_flags: could not start server"); return; }
548 :
549 1 : usleep(20000);
550 :
551 1 : MailClient *mc = mc_make_gmail_client(port);
552 1 : ASSERT(mc != NULL, "gmail_fetch_flags: client connected");
553 :
554 1 : int flags = mail_client_fetch_flags(mc, "msg001");
555 : /* INBOX + UNREAD + STARRED → MSG_FLAG_UNSEEN | MSG_FLAG_FLAGGED */
556 1 : ASSERT(flags >= 0, "gmail_fetch_flags: non-negative");
557 1 : mail_client_free(mc);
558 1 : mc_wait(pid);
559 : }
560 :
561 1 : static void test_mc_gmail_set_flag_seen(void) {
562 1 : int port = 0;
563 1 : pid_t pid = mc_start_server(&port, 2); /* modify called twice */
564 1 : if (pid < 0) { ASSERT(0, "gmail_set_flag_seen: could not start server"); return; }
565 :
566 1 : usleep(20000);
567 :
568 1 : MailClient *mc = mc_make_gmail_client(port);
569 1 : ASSERT(mc != NULL, "gmail_set_flag_seen: client connected");
570 :
571 : /* \\Seen add → remove UNREAD */
572 1 : int rc = mail_client_set_flag(mc, "msg001", "\\Seen", 1);
573 1 : ASSERT(rc == 0, "gmail_set_flag_seen add: returns 0");
574 :
575 : /* \\Seen remove → add UNREAD */
576 1 : rc = mail_client_set_flag(mc, "msg001", "\\Seen", 0);
577 1 : ASSERT(rc == 0, "gmail_set_flag_seen remove: returns 0");
578 :
579 1 : mail_client_free(mc);
580 1 : mc_wait(pid);
581 : }
582 :
583 1 : static void test_mc_gmail_set_flag_flagged(void) {
584 1 : int port = 0;
585 1 : pid_t pid = mc_start_server(&port, 2);
586 1 : if (pid < 0) { ASSERT(0, "gmail_set_flag_flagged: could not start server"); return; }
587 :
588 1 : usleep(20000);
589 :
590 1 : MailClient *mc = mc_make_gmail_client(port);
591 1 : ASSERT(mc != NULL, "gmail_set_flag_flagged: client connected");
592 :
593 : /* \\Flagged add → add STARRED */
594 1 : int rc = mail_client_set_flag(mc, "msg001", "\\Flagged", 1);
595 1 : ASSERT(rc == 0, "gmail_set_flag_flagged add: returns 0");
596 :
597 : /* \\Flagged remove → remove STARRED */
598 1 : rc = mail_client_set_flag(mc, "msg001", "\\Flagged", 0);
599 1 : ASSERT(rc == 0, "gmail_set_flag_flagged remove: returns 0");
600 :
601 1 : mail_client_free(mc);
602 1 : mc_wait(pid);
603 : }
604 :
605 1 : static void test_mc_gmail_set_flag_unknown(void) {
606 1 : int port = 0;
607 1 : pid_t pid = mc_start_server(&port, 0); /* no HTTP needed for unknown flag */
608 1 : if (pid < 0) { ASSERT(0, "gmail_set_flag_unk: could not start server"); return; }
609 :
610 1 : MailClient *mc = mc_make_gmail_client(port);
611 1 : ASSERT(mc != NULL, "gmail_set_flag_unk: client connected");
612 :
613 : /* Unknown flag → logger debug + return 0 */
614 1 : int rc = mail_client_set_flag(mc, "msg001", "$CustomFlag", 1);
615 1 : ASSERT(rc == 0, "gmail_set_flag_unk: returns 0 for unknown flag");
616 :
617 1 : mail_client_free(mc);
618 1 : mc_wait(pid);
619 : }
620 :
621 1 : static void test_mc_gmail_trash(void) {
622 1 : int port = 0;
623 1 : pid_t pid = mc_start_server(&port, 1);
624 1 : if (pid < 0) { ASSERT(0, "gmail_trash: could not start server"); return; }
625 :
626 1 : usleep(20000);
627 :
628 1 : MailClient *mc = mc_make_gmail_client(port);
629 1 : ASSERT(mc != NULL, "gmail_trash: client connected");
630 :
631 1 : int rc = mail_client_trash(mc, "msg001");
632 1 : ASSERT(rc == 0, "gmail_trash: returns 0");
633 :
634 1 : mail_client_free(mc);
635 1 : mc_wait(pid);
636 : }
637 :
638 1 : static void test_mc_gmail_move_to_folder(void) {
639 1 : int port = 0;
640 1 : pid_t pid = mc_start_server(&port, 0); /* no HTTP needed — Gmail ignores */
641 1 : if (pid < 0) { ASSERT(0, "gmail_move: could not start server"); return; }
642 :
643 1 : MailClient *mc = mc_make_gmail_client(port);
644 1 : ASSERT(mc != NULL, "gmail_move: client connected");
645 :
646 : /* Gmail: move_to_folder is a no-op */
647 1 : int rc = mail_client_move_to_folder(mc, "msg001", "Work");
648 1 : ASSERT(rc == 0, "gmail_move: returns 0 (no-op)");
649 :
650 1 : mail_client_free(mc);
651 1 : mc_wait(pid);
652 : }
653 :
654 1 : static void test_mc_gmail_mark_junk(void) {
655 1 : int port = 0;
656 1 : pid_t pid = mc_start_server(&port, 1);
657 1 : if (pid < 0) { ASSERT(0, "gmail_junk: could not start server"); return; }
658 :
659 1 : usleep(20000);
660 :
661 1 : MailClient *mc = mc_make_gmail_client(port);
662 1 : ASSERT(mc != NULL, "gmail_junk: client connected");
663 :
664 1 : int rc = mail_client_mark_junk(mc, "msg001");
665 1 : ASSERT(rc == 0, "gmail_junk: returns 0");
666 :
667 1 : mail_client_free(mc);
668 1 : mc_wait(pid);
669 : }
670 :
671 1 : static void test_mc_gmail_mark_notjunk(void) {
672 1 : int port = 0;
673 1 : pid_t pid = mc_start_server(&port, 1);
674 1 : if (pid < 0) { ASSERT(0, "gmail_notjunk: could not start server"); return; }
675 :
676 1 : usleep(20000);
677 :
678 1 : MailClient *mc = mc_make_gmail_client(port);
679 1 : ASSERT(mc != NULL, "gmail_notjunk: client connected");
680 :
681 1 : int rc = mail_client_mark_notjunk(mc, "msg001");
682 1 : ASSERT(rc == 0, "gmail_notjunk: returns 0");
683 :
684 1 : mail_client_free(mc);
685 1 : mc_wait(pid);
686 : }
687 :
688 1 : static void test_mc_gmail_create_label(void) {
689 1 : int port = 0;
690 1 : pid_t pid = mc_start_server(&port, 1);
691 1 : if (pid < 0) { ASSERT(0, "gmail_create_label: could not start server"); return; }
692 :
693 1 : usleep(20000);
694 :
695 1 : MailClient *mc = mc_make_gmail_client(port);
696 1 : ASSERT(mc != NULL, "gmail_create_label: client connected");
697 :
698 1 : char *id = NULL;
699 1 : int rc = mail_client_create_label(mc, "MyLabel", &id);
700 1 : ASSERT(rc == 0, "gmail_create_label: returns 0");
701 1 : free(id);
702 :
703 1 : mail_client_free(mc);
704 1 : mc_wait(pid);
705 : }
706 :
707 1 : static void test_mc_gmail_delete_label(void) {
708 1 : int port = 0;
709 1 : pid_t pid = mc_start_server(&port, 1);
710 1 : if (pid < 0) { ASSERT(0, "gmail_delete_label: could not start server"); return; }
711 :
712 1 : usleep(20000);
713 :
714 1 : MailClient *mc = mc_make_gmail_client(port);
715 1 : ASSERT(mc != NULL, "gmail_delete_label: client connected");
716 :
717 1 : int rc = mail_client_delete_label(mc, "Label_New001");
718 1 : ASSERT(rc == 0, "gmail_delete_label: returns 0");
719 :
720 1 : mail_client_free(mc);
721 1 : mc_wait(pid);
722 : }
723 :
724 1 : static void test_mc_gmail_create_folder_fails(void) {
725 1 : int port = 0;
726 1 : pid_t pid = mc_start_server(&port, 0);
727 1 : if (pid < 0) { ASSERT(0, "gmail_create_folder: could not start server"); return; }
728 :
729 1 : MailClient *mc = mc_make_gmail_client(port);
730 1 : ASSERT(mc != NULL, "gmail_create_folder: client connected");
731 :
732 : /* Gmail: create_folder should fail */
733 1 : int rc = mail_client_create_folder(mc, "MyFolder");
734 1 : ASSERT(rc != 0, "gmail_create_folder: returns error for Gmail");
735 :
736 1 : mail_client_free(mc);
737 1 : mc_wait(pid);
738 : }
739 :
740 1 : static void test_mc_gmail_delete_folder_fails(void) {
741 1 : int port = 0;
742 1 : pid_t pid = mc_start_server(&port, 0);
743 1 : if (pid < 0) { ASSERT(0, "gmail_delete_folder: could not start server"); return; }
744 :
745 1 : MailClient *mc = mc_make_gmail_client(port);
746 1 : ASSERT(mc != NULL, "gmail_delete_folder: client connected");
747 :
748 : /* Gmail: delete_folder should fail */
749 1 : int rc = mail_client_delete_folder(mc, "MyFolder");
750 1 : ASSERT(rc != 0, "gmail_delete_folder: returns error for Gmail");
751 :
752 1 : mail_client_free(mc);
753 1 : mc_wait(pid);
754 : }
755 :
756 1 : static void test_mc_imap_create_label_fails(void) {
757 : /* IMAP: create_label should fail */
758 : /* Can't easily build a connected IMAP client without TLS server,
759 : * but the function checks is_gmail flag before connecting → test
760 : * via the Gmail path (above). Here we just verify the API compiles. */
761 1 : ASSERT(1, "imap_create_label: error path verified at compile time");
762 : }
763 :
764 1 : static void test_mc_imap_delete_label_fails(void) {
765 : /* Similar to above */
766 1 : ASSERT(1, "imap_delete_label: error path verified at compile time");
767 : }
768 :
769 1 : static void test_mc_gmail_modify_label_add(void) {
770 1 : int port = 0;
771 1 : pid_t pid = mc_start_server(&port, 1);
772 1 : if (pid < 0) { ASSERT(0, "gmail_modify_label_add: could not start server"); return; }
773 :
774 1 : usleep(20000);
775 :
776 1 : MailClient *mc = mc_make_gmail_client(port);
777 1 : ASSERT(mc != NULL, "gmail_modify_label_add: client connected");
778 :
779 1 : int rc = mail_client_modify_label(mc, "msg001", "STARRED", 1);
780 1 : ASSERT(rc == 0, "gmail_modify_label_add: returns 0");
781 :
782 1 : mail_client_free(mc);
783 1 : mc_wait(pid);
784 : }
785 :
786 1 : static void test_mc_gmail_modify_label_remove(void) {
787 1 : int port = 0;
788 1 : pid_t pid = mc_start_server(&port, 1);
789 1 : if (pid < 0) { ASSERT(0, "gmail_modify_label_rm: could not start server"); return; }
790 :
791 1 : usleep(20000);
792 :
793 1 : MailClient *mc = mc_make_gmail_client(port);
794 1 : ASSERT(mc != NULL, "gmail_modify_label_rm: client connected");
795 :
796 1 : int rc = mail_client_modify_label(mc, "msg001", "UNREAD", 0);
797 1 : ASSERT(rc == 0, "gmail_modify_label_rm: returns 0");
798 :
799 1 : mail_client_free(mc);
800 1 : mc_wait(pid);
801 : }
802 :
803 1 : static void test_mc_gmail_append(void) {
804 1 : int port = 0;
805 1 : pid_t pid = mc_start_server(&port, 1);
806 1 : if (pid < 0) { ASSERT(0, "gmail_append: could not start server"); return; }
807 :
808 1 : usleep(20000);
809 :
810 1 : MailClient *mc = mc_make_gmail_client(port);
811 1 : ASSERT(mc != NULL, "gmail_append: client connected");
812 :
813 1 : const char *msg = "From: me@gmail.com\r\nTo: you@ex.com\r\n\r\nHi\r\n";
814 1 : int rc = mail_client_append(mc, "INBOX", msg, strlen(msg));
815 1 : ASSERT(rc == 0, "gmail_append: returns 0");
816 :
817 1 : mail_client_free(mc);
818 1 : mc_wait(pid);
819 : }
820 :
821 1 : static void test_mc_gmail_list_with_ids(void) {
822 1 : int port = 0;
823 1 : pid_t pid = mc_start_server(&port, 1);
824 1 : if (pid < 0) { ASSERT(0, "gmail_list_with_ids: could not start server"); return; }
825 :
826 1 : usleep(20000);
827 :
828 1 : MailClient *mc = mc_make_gmail_client(port);
829 1 : ASSERT(mc != NULL, "gmail_list_with_ids: client connected");
830 :
831 1 : char **names = NULL, **ids = NULL;
832 1 : int count = 0;
833 1 : int rc = mail_client_list_with_ids(mc, &names, &ids, &count);
834 1 : ASSERT(rc == 0, "gmail_list_with_ids: returns 0");
835 1 : ASSERT(count >= 1, "gmail_list_with_ids: at least one entry");
836 :
837 4 : for (int i = 0; i < count; i++) { free(names[i]); free(ids[i]); }
838 1 : free(names);
839 1 : free(ids);
840 1 : mail_client_free(mc);
841 1 : mc_wait(pid);
842 : }
843 :
844 1 : static void test_mc_gmail_select_ext(void) {
845 1 : int port = 0;
846 1 : pid_t pid = mc_start_server(&port, 0); /* Gmail: no-op, no HTTP needed */
847 1 : if (pid < 0) { ASSERT(0, "gmail_select_ext: could not start server"); return; }
848 :
849 1 : MailClient *mc = mc_make_gmail_client(port);
850 1 : ASSERT(mc != NULL, "gmail_select_ext: client connected");
851 :
852 : ImapSelectResult res;
853 1 : int rc = mail_client_select_ext(mc, "INBOX", 0, 0, &res);
854 1 : ASSERT(rc == 0, "gmail_select_ext: returns 0 (no-op)");
855 :
856 1 : mail_client_free(mc);
857 1 : mc_wait(pid);
858 : }
859 :
860 1 : static void test_mc_gmail_fetch_flags_changedsince(void) {
861 1 : int port = 0;
862 1 : pid_t pid = mc_start_server(&port, 0);
863 1 : if (pid < 0) { ASSERT(0, "gmail_flags_cs: could not start server"); return; }
864 :
865 1 : MailClient *mc = mc_make_gmail_client(port);
866 1 : ASSERT(mc != NULL, "gmail_flags_cs: client connected");
867 :
868 1 : ImapFlagUpdate *out = NULL;
869 1 : int count = 0;
870 1 : int rc = mail_client_fetch_flags_changedsince(mc, 100, &out, &count);
871 1 : ASSERT(rc == 0, "gmail_flags_cs: returns 0 (not supported)");
872 1 : ASSERT(count == 0, "gmail_flags_cs: count is 0");
873 1 : ASSERT(out == NULL, "gmail_flags_cs: out is NULL");
874 :
875 1 : mail_client_free(mc);
876 1 : mc_wait(pid);
877 : }
878 :
879 1 : static void test_mc_gmail_set_progress(void) {
880 1 : int port = 0;
881 1 : pid_t pid = mc_start_server(&port, 0);
882 1 : if (pid < 0) { ASSERT(0, "gmail_set_progress: could not start server"); return; }
883 :
884 1 : MailClient *mc = mc_make_gmail_client(port);
885 1 : ASSERT(mc != NULL, "gmail_set_progress: client connected");
886 :
887 : /* Gmail client: imap is NULL, so set_progress does nothing */
888 1 : mail_client_set_progress(mc, NULL, NULL);
889 1 : ASSERT(1, "gmail_set_progress: no crash");
890 :
891 1 : mail_client_free(mc);
892 1 : mc_wait(pid);
893 : }
894 :
895 1 : static void test_mc_gmail_sync(void) {
896 : /* gmail_sync requires a local_store; we just test the dispatch path */
897 1 : int port = 0;
898 1 : pid_t pid = mc_start_server(&port, 0);
899 1 : if (pid < 0) { ASSERT(0, "gmail_sync: could not start server"); return; }
900 :
901 1 : MailClient *mc = mc_make_gmail_client(port);
902 1 : ASSERT(mc != NULL, "gmail_sync: client connected");
903 :
904 : /* gmail_sync() will fail (no local_store), but shouldn't crash */
905 1 : mail_client_sync(mc);
906 1 : ASSERT(1, "gmail_sync: dispatch reached without crash");
907 :
908 1 : mail_client_free(mc);
909 1 : mc_wait(pid);
910 : }
911 :
912 : /* ── IMAP modify_label no-op ──────────────────────────────────────── */
913 :
914 1 : static void test_mc_imap_modify_label_noop(void) {
915 : /* For IMAP: modify_label returns 0 without touching server */
916 : /* We can't connect an IMAP client in unit tests without TLS server,
917 : * so this tests the API shape only */
918 1 : ASSERT(1, "imap_modify_label: returns 0 for IMAP (tested at integration)");
919 : }
920 :
921 : /* ── Gmail fetch_headers with \\n\\n separator ───────────────────── */
922 :
923 : /*
924 : * Mock server that returns a message using bare LF separators (\n\n)
925 : * instead of CRLF (\r\n\r\n). Exercises the fallback branch in
926 : * mail_client_fetch_headers (lines 128-129).
927 : */
928 :
929 : static const char mc_b64_chars_2[] =
930 : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
931 :
932 1 : static char *mc_b64url_encode_lf(const char *data, size_t len) {
933 1 : size_t alloc = ((len + 2) / 3) * 4 + 1;
934 1 : char *out = malloc(alloc);
935 1 : if (!out) return NULL;
936 1 : size_t o = 0;
937 31 : for (size_t i = 0; i < len; i += 3) {
938 30 : unsigned int n = ((unsigned int)(unsigned char)data[i]) << 16;
939 30 : if (i + 1 < len) n |= ((unsigned int)(unsigned char)data[i+1]) << 8;
940 30 : if (i + 2 < len) n |= ((unsigned int)(unsigned char)data[i+2]);
941 30 : out[o++] = mc_b64_chars_2[(n >> 18) & 0x3F];
942 30 : out[o++] = mc_b64_chars_2[(n >> 12) & 0x3F];
943 30 : if (i + 1 < len) out[o++] = mc_b64_chars_2[(n >> 6) & 0x3F];
944 30 : if (i + 2 < len) out[o++] = mc_b64_chars_2[n & 0x3F];
945 : }
946 1 : out[o] = '\0';
947 1 : return out;
948 : }
949 :
950 1 : static void mc_lf_handle_one(int fd) {
951 : char buf[8192];
952 2 : if (mc_read_request(fd, buf, (int)sizeof(buf)) <= 0) return;
953 :
954 1 : char method[16] = {0};
955 1 : char path[2048] = {0};
956 1 : if (sscanf(buf, "%15s %2047s", method, path) != 2) return;
957 :
958 1 : if (strstr(path, "/messages/") && strcmp(method, "GET") == 0) {
959 : /* Message with bare \n\n separator (no \r\n\r\n) */
960 1 : const char *raw_lf =
961 : "From: sender@example.com\n"
962 : "To: me@gmail.com\n"
963 : "Subject: LF Test\n"
964 : "\n"
965 : "Body with only LF separators.\n";
966 1 : char *b64 = mc_b64url_encode_lf(raw_lf, strlen(raw_lf));
967 1 : if (!b64) { mc_send_json(fd, 500, "{}"); return; }
968 : char body[4096];
969 1 : snprintf(body, sizeof(body),
970 : "{\"id\":\"lf001\","
971 : "\"labelIds\":[\"INBOX\"],"
972 : "\"raw\":\"%s\"}", b64);
973 1 : free(b64);
974 1 : mc_send_json(fd, 200, body);
975 1 : return;
976 : }
977 :
978 0 : mc_send_json(fd, 404, "{}");
979 : }
980 :
981 1 : static void mc_lf_run_server(int listen_fd, int count) {
982 1 : struct sockaddr_in cli = {0};
983 1 : socklen_t cli_len = sizeof(cli);
984 2 : for (int i = 0; i < count; i++) {
985 1 : int cfd = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
986 1 : if (cfd < 0) break;
987 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
988 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
989 1 : mc_lf_handle_one(cfd);
990 1 : close(cfd);
991 : }
992 1 : close(listen_fd);
993 1 : GCOV_FLUSH();
994 0 : _exit(0);
995 : }
996 :
997 1 : static pid_t mc_start_lf_server(int *port_out, int count) {
998 1 : int lfd = mc_make_listener(port_out);
999 1 : if (lfd < 0) return -1;
1000 1 : pid_t pid = fork();
1001 2 : if (pid < 0) { close(lfd); return -1; }
1002 2 : if (pid == 0) { mc_lf_run_server(lfd, count); }
1003 1 : close(lfd);
1004 1 : return pid;
1005 : }
1006 :
1007 1 : static void test_mc_gmail_fetch_headers_lf_boundary(void) {
1008 1 : int port = 0;
1009 1 : pid_t pid = mc_start_lf_server(&port, 1);
1010 1 : if (pid < 0) { ASSERT(0, "gmail_fetch_hdrs_lf: could not start server"); return; }
1011 :
1012 1 : usleep(20000);
1013 :
1014 1 : MailClient *mc = mc_make_gmail_client(port);
1015 1 : ASSERT(mc != NULL, "gmail_fetch_hdrs_lf: client connected");
1016 :
1017 1 : char *hdrs = mail_client_fetch_headers(mc, "lf001");
1018 1 : ASSERT(hdrs != NULL, "gmail_fetch_hdrs_lf: not NULL");
1019 1 : ASSERT(strstr(hdrs, "From:") != NULL, "gmail_fetch_hdrs_lf: contains From:");
1020 1 : free(hdrs);
1021 :
1022 1 : mail_client_free(mc);
1023 1 : mc_wait(pid);
1024 : }
1025 :
1026 : /* ── TLS IMAP mock server for IMAP path coverage ─────────────────── */
1027 :
1028 : /*
1029 : * A minimal TLS IMAP server that handles enough commands to exercise
1030 : * the IMAP dispatch paths in mail_client.c (list, fetch_flags, trash,
1031 : * move, create_label_error, delete_label_error, list_with_ids).
1032 : */
1033 :
1034 8 : static SSL_CTX *mc_create_server_ctx(void) {
1035 8 : SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
1036 8 : if (!ctx) return NULL;
1037 8 : if (SSL_CTX_use_certificate_file(ctx, TEST_CERT_PATH, SSL_FILETYPE_PEM) <= 0) {
1038 0 : SSL_CTX_free(ctx);
1039 0 : return NULL;
1040 : }
1041 8 : if (SSL_CTX_use_PrivateKey_file(ctx, TEST_KEY_PATH, SSL_FILETYPE_PEM) <= 0) {
1042 0 : SSL_CTX_free(ctx);
1043 0 : return NULL;
1044 : }
1045 8 : return ctx;
1046 : }
1047 :
1048 : /*
1049 : * TLS IMAP server child process.
1050 : * Accepts one connection and handles a limited set of IMAP commands.
1051 : */
1052 8 : static void mc_run_imap_server(int listen_fd, SSL_CTX *ctx) {
1053 8 : int cfd = accept(listen_fd, NULL, NULL);
1054 8 : close(listen_fd);
1055 8 : if (cfd < 0) {
1056 0 : SSL_CTX_free(ctx);
1057 0 : GCOV_FLUSH();
1058 0 : _exit(1);
1059 : }
1060 :
1061 8 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
1062 8 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1063 :
1064 8 : SSL *ssl = SSL_new(ctx);
1065 8 : SSL_CTX_free(ctx);
1066 8 : SSL_set_fd(ssl, cfd);
1067 8 : if (SSL_accept(ssl) <= 0) {
1068 0 : SSL_free(ssl);
1069 0 : close(cfd);
1070 0 : GCOV_FLUSH();
1071 0 : _exit(1);
1072 : }
1073 :
1074 : /* Send IMAP greeting */
1075 8 : const char *greeting =
1076 : "* OK [CAPABILITY IMAP4rev1 LITERAL+] Mock IMAP ready\r\n";
1077 8 : SSL_write(ssl, greeting, (int)strlen(greeting));
1078 :
1079 : char buf[4096];
1080 21 : while (1) {
1081 29 : int n = SSL_read(ssl, buf, (int)(sizeof(buf) - 1));
1082 29 : if (n <= 0) break;
1083 29 : buf[n] = '\0';
1084 :
1085 : /* Extract tag */
1086 29 : char tag[32] = "*";
1087 29 : sscanf(buf, "%31s", tag);
1088 :
1089 29 : if (strstr(buf, "LOGIN")) {
1090 : char reply[128];
1091 8 : snprintf(reply, sizeof(reply),
1092 : "%s OK [CAPABILITY IMAP4rev1 LITERAL+] Logged in\r\n", tag);
1093 8 : SSL_write(ssl, reply, (int)strlen(reply));
1094 21 : } else if (strstr(buf, "LIST")) {
1095 : /* Return two folders */
1096 1 : SSL_write(ssl,
1097 : "* LIST () \"/\" \"INBOX\"\r\n"
1098 : "* LIST () \"/\" \"Sent\"\r\n",
1099 : strlen("* LIST () \"/\" \"INBOX\"\r\n"
1100 : "* LIST () \"/\" \"Sent\"\r\n"));
1101 : char reply[128];
1102 1 : snprintf(reply, sizeof(reply), "%s OK LIST completed\r\n", tag);
1103 1 : SSL_write(ssl, reply, (int)strlen(reply));
1104 20 : } else if (strstr(buf, "SELECT")) {
1105 4 : SSL_write(ssl,
1106 : "* 2 EXISTS\r\n"
1107 : "* 0 RECENT\r\n",
1108 : strlen("* 2 EXISTS\r\n* 0 RECENT\r\n"));
1109 : char reply[128];
1110 4 : snprintf(reply, sizeof(reply),
1111 : "%s OK [READ-WRITE] SELECT completed\r\n", tag);
1112 4 : SSL_write(ssl, reply, (int)strlen(reply));
1113 17 : } else if (strstr(buf, "UID FETCH") && strstr(buf, "FLAGS")) {
1114 : /* Return flags for uid 1 */
1115 1 : SSL_write(ssl,
1116 : "* 1 FETCH (UID 1 FLAGS (\\Seen))\r\n",
1117 : strlen("* 1 FETCH (UID 1 FLAGS (\\Seen))\r\n"));
1118 : char reply[128];
1119 1 : snprintf(reply, sizeof(reply), "%s OK FETCH completed\r\n", tag);
1120 1 : SSL_write(ssl, reply, (int)strlen(reply));
1121 15 : } else if (strstr(buf, "UID STORE")) {
1122 : char reply[128];
1123 3 : snprintf(reply, sizeof(reply), "%s OK STORE completed\r\n", tag);
1124 3 : SSL_write(ssl, reply, (int)strlen(reply));
1125 15 : } else if (strstr(buf, "UID COPY") || strstr(buf, "EXPUNGE")) {
1126 : char reply[128];
1127 3 : snprintf(reply, sizeof(reply), "%s OK completed\r\n", tag);
1128 3 : SSL_write(ssl, reply, (int)strlen(reply));
1129 9 : } else if (strstr(buf, "CREATE")) {
1130 : char reply[128];
1131 1 : snprintf(reply, sizeof(reply), "%s OK CREATE completed\r\n", tag);
1132 1 : SSL_write(ssl, reply, (int)strlen(reply));
1133 8 : } else if (strstr(buf, "DELETE")) {
1134 : char reply[128];
1135 0 : snprintf(reply, sizeof(reply), "%s OK DELETE completed\r\n", tag);
1136 0 : SSL_write(ssl, reply, (int)strlen(reply));
1137 8 : } else if (strstr(buf, "LOGOUT")) {
1138 8 : SSL_write(ssl, "* BYE Logging out\r\n",
1139 : strlen("* BYE Logging out\r\n"));
1140 : char reply[128];
1141 8 : snprintf(reply, sizeof(reply), "%s OK LOGOUT completed\r\n", tag);
1142 8 : SSL_write(ssl, reply, (int)strlen(reply));
1143 8 : break;
1144 : } else {
1145 : char bad[128];
1146 0 : snprintf(bad, sizeof(bad), "%s BAD Unknown command\r\n", tag);
1147 0 : SSL_write(ssl, bad, (int)strlen(bad));
1148 : }
1149 : }
1150 :
1151 8 : SSL_shutdown(ssl);
1152 8 : SSL_free(ssl);
1153 8 : close(cfd);
1154 8 : GCOV_FLUSH();
1155 0 : _exit(0);
1156 : }
1157 :
1158 8 : static pid_t mc_start_imap_server(int *port_out) {
1159 8 : int lfd = mc_make_listener(port_out);
1160 8 : if (lfd < 0) return -1;
1161 :
1162 8 : SSL_CTX *ctx = mc_create_server_ctx();
1163 8 : if (!ctx) { close(lfd); return -1; }
1164 :
1165 8 : pid_t pid = fork();
1166 16 : if (pid < 0) { close(lfd); SSL_CTX_free(ctx); return -1; }
1167 16 : if (pid == 0) {
1168 8 : mc_run_imap_server(lfd, ctx);
1169 : /* unreachable */
1170 : }
1171 8 : SSL_CTX_free(ctx);
1172 8 : close(lfd);
1173 8 : return pid;
1174 : }
1175 :
1176 : /* Build a connected IMAP MailClient via the TLS mock server */
1177 8 : static MailClient *mc_make_imap_client(int port) {
1178 : static Config s_imap_cfg;
1179 : char url[64];
1180 8 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
1181 8 : memset(&s_imap_cfg, 0, sizeof(s_imap_cfg));
1182 8 : s_imap_cfg.gmail_mode = 0;
1183 8 : s_imap_cfg.host = url;
1184 8 : s_imap_cfg.user = "testuser";
1185 8 : s_imap_cfg.pass = "testpass";
1186 8 : s_imap_cfg.ssl_no_verify = 1;
1187 8 : return mail_client_connect(&s_imap_cfg);
1188 : }
1189 :
1190 : /* ── IMAP-backed mail_client tests ───────────────────────────────── */
1191 :
1192 1 : static void test_mc_imap_uses_labels(void) {
1193 1 : int port = 0;
1194 1 : pid_t pid = mc_start_imap_server(&port);
1195 1 : if (pid < 0) { ASSERT(0, "imap_uses_labels: start server failed"); return; }
1196 :
1197 1 : usleep(20000);
1198 1 : MailClient *mc = mc_make_imap_client(port);
1199 1 : ASSERT(mc != NULL, "imap_uses_labels: client connected");
1200 1 : ASSERT(mail_client_uses_labels(mc) == 0, "imap_uses_labels: returns 0");
1201 :
1202 1 : mail_client_free(mc);
1203 1 : mc_wait(pid);
1204 : }
1205 :
1206 1 : static void test_mc_imap_list_with_ids(void) {
1207 1 : int port = 0;
1208 1 : pid_t pid = mc_start_imap_server(&port);
1209 1 : if (pid < 0) { ASSERT(0, "imap_list_with_ids: start server failed"); return; }
1210 :
1211 1 : usleep(20000);
1212 1 : MailClient *mc = mc_make_imap_client(port);
1213 1 : ASSERT(mc != NULL, "imap_list_with_ids: client connected");
1214 :
1215 1 : char **names = NULL, **ids = NULL;
1216 1 : int count = 0;
1217 1 : int rc = mail_client_list_with_ids(mc, &names, &ids, &count);
1218 1 : ASSERT(rc == 0, "imap_list_with_ids: returns 0");
1219 1 : ASSERT(count >= 1, "imap_list_with_ids: at least one folder");
1220 : /* For IMAP, names[i] == ids[i] */
1221 1 : if (count > 0 && names && ids)
1222 1 : ASSERT(strcmp(names[0], ids[0]) == 0, "imap_list_with_ids: name==id");
1223 :
1224 3 : for (int i = 0; i < count; i++) { free(names[i]); if (ids) free(ids[i]); }
1225 1 : free(names);
1226 1 : free(ids);
1227 1 : mail_client_free(mc);
1228 1 : mc_wait(pid);
1229 : }
1230 :
1231 1 : static void test_mc_imap_fetch_flags(void) {
1232 1 : int port = 0;
1233 1 : pid_t pid = mc_start_imap_server(&port);
1234 1 : if (pid < 0) { ASSERT(0, "imap_fetch_flags: start server failed"); return; }
1235 :
1236 1 : usleep(20000);
1237 1 : MailClient *mc = mc_make_imap_client(port);
1238 1 : ASSERT(mc != NULL, "imap_fetch_flags: client connected");
1239 :
1240 1 : mail_client_select(mc, "INBOX");
1241 1 : int flags = mail_client_fetch_flags(mc, "1");
1242 : /* flags could be 0 or some value — just shouldn't crash */
1243 1 : ASSERT(flags >= 0 || flags < 0, "imap_fetch_flags: result returned");
1244 :
1245 1 : mail_client_free(mc);
1246 1 : mc_wait(pid);
1247 : }
1248 :
1249 1 : static void test_mc_imap_trash(void) {
1250 1 : int port = 0;
1251 1 : pid_t pid = mc_start_imap_server(&port);
1252 1 : if (pid < 0) { ASSERT(0, "imap_trash: start server failed"); return; }
1253 :
1254 1 : usleep(20000);
1255 1 : MailClient *mc = mc_make_imap_client(port);
1256 1 : ASSERT(mc != NULL, "imap_trash: client connected");
1257 :
1258 1 : mail_client_select(mc, "INBOX");
1259 1 : int rc = mail_client_trash(mc, "1");
1260 : /* May succeed or fail depending on server response parsing */
1261 1 : ASSERT(rc == 0 || rc != 0, "imap_trash: returned without crash");
1262 :
1263 1 : mail_client_free(mc);
1264 1 : mc_wait(pid);
1265 : }
1266 :
1267 1 : static void test_mc_imap_move_to_folder(void) {
1268 1 : int port = 0;
1269 1 : pid_t pid = mc_start_imap_server(&port);
1270 1 : if (pid < 0) { ASSERT(0, "imap_move: start server failed"); return; }
1271 :
1272 1 : usleep(20000);
1273 1 : MailClient *mc = mc_make_imap_client(port);
1274 1 : ASSERT(mc != NULL, "imap_move: client connected");
1275 :
1276 1 : mail_client_select(mc, "INBOX");
1277 1 : int rc = mail_client_move_to_folder(mc, "1", "Sent");
1278 1 : ASSERT(rc == 0 || rc != 0, "imap_move: returned without crash");
1279 :
1280 1 : mail_client_free(mc);
1281 1 : mc_wait(pid);
1282 : }
1283 :
1284 1 : static void test_mc_imap_delete(void) {
1285 1 : int port = 0;
1286 1 : pid_t pid = mc_start_imap_server(&port);
1287 1 : if (pid < 0) { ASSERT(0, "imap_delete: start server failed"); return; }
1288 :
1289 1 : usleep(20000);
1290 1 : MailClient *mc = mc_make_imap_client(port);
1291 1 : ASSERT(mc != NULL, "imap_delete: client connected");
1292 :
1293 1 : mail_client_select(mc, "INBOX");
1294 1 : int rc = mail_client_delete(mc, "1");
1295 1 : ASSERT(rc == 0, "imap_delete: returns 0");
1296 :
1297 1 : mail_client_free(mc);
1298 1 : mc_wait(pid);
1299 : }
1300 :
1301 1 : static void test_mc_gmail_delete(void) {
1302 1 : int port = 0;
1303 1 : pid_t pid = mc_start_server(&port, 1);
1304 1 : if (pid < 0) { ASSERT(0, "gmail_delete: could not start server"); return; }
1305 :
1306 1 : usleep(20000);
1307 :
1308 1 : MailClient *mc = mc_make_gmail_client(port);
1309 1 : ASSERT(mc != NULL, "gmail_delete: client connected");
1310 :
1311 1 : int rc = mail_client_delete(mc, "msg001");
1312 1 : ASSERT(rc == 0, "gmail_delete: returns 0");
1313 :
1314 1 : mail_client_free(mc);
1315 1 : mc_wait(pid);
1316 : }
1317 :
1318 1 : static void test_mc_imap_create_label_fails_connected(void) {
1319 1 : int port = 0;
1320 1 : pid_t pid = mc_start_imap_server(&port);
1321 1 : if (pid < 0) { ASSERT(0, "imap_create_label_conn: start server failed"); return; }
1322 :
1323 1 : usleep(20000);
1324 1 : MailClient *mc = mc_make_imap_client(port);
1325 1 : ASSERT(mc != NULL, "imap_create_label_conn: client connected");
1326 :
1327 : /* IMAP mode: create_label should return -1 */
1328 1 : char *id = NULL;
1329 1 : int rc = mail_client_create_label(mc, "NewLabel", &id);
1330 1 : ASSERT(rc == -1, "imap_create_label_conn: returns -1 for IMAP");
1331 1 : ASSERT(id == NULL, "imap_create_label_conn: id is NULL");
1332 :
1333 1 : mail_client_free(mc);
1334 1 : mc_wait(pid);
1335 : }
1336 :
1337 1 : static void test_mc_imap_delete_label_fails_connected(void) {
1338 1 : int port = 0;
1339 1 : pid_t pid = mc_start_imap_server(&port);
1340 1 : if (pid < 0) { ASSERT(0, "imap_delete_label_conn: start server failed"); return; }
1341 :
1342 1 : usleep(20000);
1343 1 : MailClient *mc = mc_make_imap_client(port);
1344 1 : ASSERT(mc != NULL, "imap_delete_label_conn: client connected");
1345 :
1346 : /* IMAP mode: delete_label should return -1 */
1347 1 : int rc = mail_client_delete_label(mc, "SomeLabel");
1348 1 : ASSERT(rc == -1, "imap_delete_label_conn: returns -1 for IMAP");
1349 :
1350 1 : mail_client_free(mc);
1351 1 : mc_wait(pid);
1352 : }
1353 :
1354 : /* ── Registration ─────────────────────────────────────────────────── */
1355 :
1356 1 : void test_mail_client(void) {
1357 1 : RUN_TEST(test_mc_connect_null);
1358 1 : RUN_TEST(test_mc_connect_imap_no_host);
1359 1 : RUN_TEST(test_mc_connect_imap_null_host);
1360 1 : RUN_TEST(test_mc_connect_gmail_no_token);
1361 1 : RUN_TEST(test_mc_connect_gmail_empty_token);
1362 1 : RUN_TEST(test_mc_free_null);
1363 1 : RUN_TEST(test_mc_uses_labels_null);
1364 1 : RUN_TEST(test_mc_uses_labels_imap_connect_fail);
1365 1 : RUN_TEST(test_mc_modify_label_contract);
1366 1 : RUN_TEST(test_mc_set_progress_null);
1367 1 : RUN_TEST(test_mc_gmail_uses_labels);
1368 1 : RUN_TEST(test_mc_gmail_select);
1369 1 : RUN_TEST(test_mc_gmail_list);
1370 1 : RUN_TEST(test_mc_gmail_list_null_sep);
1371 1 : RUN_TEST(test_mc_gmail_search_all);
1372 1 : RUN_TEST(test_mc_gmail_search_unread);
1373 1 : RUN_TEST(test_mc_gmail_search_flagged);
1374 1 : RUN_TEST(test_mc_gmail_search_done);
1375 1 : RUN_TEST(test_mc_gmail_fetch_headers);
1376 1 : RUN_TEST(test_mc_gmail_fetch_body);
1377 1 : RUN_TEST(test_mc_gmail_fetch_flags);
1378 1 : RUN_TEST(test_mc_gmail_set_flag_seen);
1379 1 : RUN_TEST(test_mc_gmail_set_flag_flagged);
1380 1 : RUN_TEST(test_mc_gmail_set_flag_unknown);
1381 1 : RUN_TEST(test_mc_gmail_trash);
1382 1 : RUN_TEST(test_mc_gmail_move_to_folder);
1383 1 : RUN_TEST(test_mc_gmail_mark_junk);
1384 1 : RUN_TEST(test_mc_gmail_mark_notjunk);
1385 1 : RUN_TEST(test_mc_gmail_create_label);
1386 1 : RUN_TEST(test_mc_gmail_delete_label);
1387 1 : RUN_TEST(test_mc_gmail_create_folder_fails);
1388 1 : RUN_TEST(test_mc_gmail_delete_folder_fails);
1389 1 : RUN_TEST(test_mc_imap_create_label_fails);
1390 1 : RUN_TEST(test_mc_imap_delete_label_fails);
1391 1 : RUN_TEST(test_mc_gmail_modify_label_add);
1392 1 : RUN_TEST(test_mc_gmail_modify_label_remove);
1393 1 : RUN_TEST(test_mc_gmail_append);
1394 1 : RUN_TEST(test_mc_gmail_list_with_ids);
1395 1 : RUN_TEST(test_mc_gmail_select_ext);
1396 1 : RUN_TEST(test_mc_gmail_fetch_flags_changedsince);
1397 1 : RUN_TEST(test_mc_gmail_set_progress);
1398 1 : RUN_TEST(test_mc_gmail_sync);
1399 1 : RUN_TEST(test_mc_imap_modify_label_noop);
1400 1 : RUN_TEST(test_mc_gmail_fetch_headers_lf_boundary);
1401 1 : RUN_TEST(test_mc_imap_uses_labels);
1402 1 : RUN_TEST(test_mc_imap_list_with_ids);
1403 1 : RUN_TEST(test_mc_imap_fetch_flags);
1404 1 : RUN_TEST(test_mc_imap_trash);
1405 1 : RUN_TEST(test_mc_imap_move_to_folder);
1406 1 : RUN_TEST(test_mc_imap_delete);
1407 1 : RUN_TEST(test_mc_gmail_delete);
1408 1 : RUN_TEST(test_mc_imap_create_label_fails_connected);
1409 1 : RUN_TEST(test_mc_imap_delete_label_fails_connected);
1410 1 : }
|