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