Line data Source code
1 : #include "test_helpers.h"
2 : #include "imap_client.h"
3 : #include <stdlib.h>
4 : #include <string.h>
5 : #include <unistd.h>
6 : #include <signal.h>
7 : #include <time.h>
8 : #include <sys/socket.h>
9 : #include <sys/wait.h>
10 : #include <netinet/in.h>
11 : #include <arpa/inet.h>
12 : #include <openssl/ssl.h>
13 : #include <openssl/err.h>
14 : #ifdef ENABLE_GCOV
15 : extern void __gcov_dump(void);
16 : # define GCOV_FLUSH() __gcov_dump()
17 : #else
18 : # define GCOV_FLUSH() ((void)0)
19 : #endif
20 :
21 : /* ── Helpers ─────────────────────────────────────────────────────────────── */
22 :
23 : /* Poll with WNOHANG for up to 10 s, then SIGKILL — prevents Valgrind hang. */
24 8 : static void wait_child(pid_t pid) {
25 16 : for (int i = 0; i < 100; i++) {
26 : int st;
27 16 : pid_t r = waitpid(pid, &st, WNOHANG);
28 16 : if (r > 0 || r < 0) return;
29 8 : struct timespec ts = {.tv_sec = 0, .tv_nsec = 100000000L}; /* 100 ms */
30 8 : nanosleep(&ts, NULL);
31 : }
32 0 : kill(pid, SIGKILL);
33 0 : int st; waitpid(pid, &st, 0);
34 : }
35 :
36 : /*
37 : * Create a listening TCP socket bound to a random loopback port.
38 : * Returns the fd and fills *port_out with the actual port number.
39 : * Returns -1 on error.
40 : */
41 8 : static int make_listener(int *port_out) {
42 8 : int fd = socket(AF_INET, SOCK_STREAM, 0);
43 8 : if (fd < 0) return -1;
44 8 : int one = 1;
45 8 : setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
46 : /* 3-second accept() timeout so server children exit cleanly if the test
47 : * returns early (ASSERT failure) before making a connection. */
48 8 : struct timeval acc_tv = {.tv_sec = 3, .tv_usec = 0};
49 8 : setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &acc_tv, sizeof(acc_tv));
50 8 : struct sockaddr_in addr = {0};
51 8 : addr.sin_family = AF_INET;
52 8 : addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
53 8 : addr.sin_port = 0; /* OS picks a free port */
54 16 : if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ||
55 8 : listen(fd, 1) < 0) {
56 0 : close(fd);
57 0 : return -1;
58 : }
59 8 : socklen_t len = sizeof(addr);
60 8 : getsockname(fd, (struct sockaddr *)&addr, &len);
61 8 : *port_out = ntohs(addr.sin_port);
62 8 : return fd;
63 : }
64 :
65 : /*
66 : * Create a server-side SSL_CTX loaded with the test self-signed certificate.
67 : * Returns NULL on failure.
68 : */
69 7 : static SSL_CTX *create_server_ctx(void) {
70 7 : SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
71 7 : if (!ctx) {
72 0 : ERR_print_errors_fp(stderr);
73 0 : return NULL;
74 : }
75 7 : if (SSL_CTX_use_certificate_file(ctx, TEST_CERT_PATH, SSL_FILETYPE_PEM) <= 0) {
76 0 : fprintf(stderr, "Failed to load test cert: %s\n", TEST_CERT_PATH);
77 0 : ERR_print_errors_fp(stderr);
78 0 : SSL_CTX_free(ctx);
79 0 : return NULL;
80 : }
81 7 : if (SSL_CTX_use_PrivateKey_file(ctx, TEST_KEY_PATH, SSL_FILETYPE_PEM) <= 0) {
82 0 : fprintf(stderr, "Failed to load test key: %s\n", TEST_KEY_PATH);
83 0 : ERR_print_errors_fp(stderr);
84 0 : SSL_CTX_free(ctx);
85 0 : return NULL;
86 : }
87 7 : return ctx;
88 : }
89 :
90 : /*
91 : * Minimal IMAP server child process (TLS).
92 : *
93 : * Accepts ONE connection over TLS, sends `greeting`, then loops reading lines
94 : * and replies according to the command:
95 : * LOGIN → sends login_reply
96 : * LOGOUT → sends BYE + OK, exits cleanly
97 : * other → sends "TAG BAD Unknown"
98 : *
99 : * After the first LOGOUT (or client disconnect) the child exits.
100 : */
101 3 : static void run_mock_server(int listen_fd,
102 : const char *greeting,
103 : const char *login_reply,
104 : SSL_CTX *ctx) {
105 3 : int cfd = accept(listen_fd, NULL, NULL);
106 3 : close(listen_fd);
107 3 : if (cfd < 0) {
108 0 : SSL_CTX_free(ctx);
109 0 : GCOV_FLUSH();
110 0 : _exit(1);
111 : }
112 :
113 : /* timeout so the child never hangs in CI */
114 3 : struct timeval tv = {.tv_sec = 3, .tv_usec = 0};
115 3 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
116 :
117 3 : SSL *ssl = SSL_new(ctx);
118 3 : SSL_CTX_free(ctx); /* child no longer needs ctx after SSL_new */
119 3 : SSL_set_fd(ssl, cfd);
120 3 : if (SSL_accept(ssl) <= 0) {
121 0 : ERR_print_errors_fp(stderr);
122 0 : SSL_free(ssl);
123 0 : close(cfd);
124 0 : GCOV_FLUSH();
125 0 : _exit(1);
126 : }
127 :
128 3 : SSL_write(ssl, greeting, (int)strlen(greeting));
129 :
130 : char buf[1024];
131 5 : while (1) {
132 8 : int n = SSL_read(ssl, buf, (int)(sizeof(buf) - 1));
133 8 : if (n <= 0) break;
134 7 : buf[n] = '\0';
135 :
136 : /* Extract tag (first token) */
137 7 : char tag[32] = "*";
138 7 : sscanf(buf, "%31s", tag);
139 :
140 7 : if (strstr(buf, "LOGIN")) {
141 3 : SSL_write(ssl, login_reply, (int)strlen(login_reply));
142 4 : } else if (strstr(buf, "APPEND")) {
143 : /* Handle LITERAL+ {N+} and synchronising {N} literals */
144 1 : char *lbrace = strrchr(buf, '{');
145 1 : long lsize = 0;
146 1 : int sync = 1;
147 1 : if (lbrace) {
148 1 : char *end = NULL;
149 1 : lsize = strtol(lbrace + 1, &end, 10);
150 1 : if (end && *end == '+') sync = 0;
151 : }
152 1 : if (lsize <= 0) {
153 : char bad2[64];
154 0 : snprintf(bad2, sizeof(bad2), "%s BAD Missing size\r\n", tag);
155 0 : SSL_write(ssl, bad2, (int)strlen(bad2));
156 : } else {
157 1 : if (sync) SSL_write(ssl, "+ OK\r\n", 6);
158 : /* Drain the literal bytes already in buf + remaining reads */
159 1 : char *ptr = lbrace ? strchr(lbrace, '}') : NULL;
160 1 : long already = 0;
161 1 : if (ptr) {
162 : /* skip past '}' and optional '+' and '\r\n' */
163 1 : ptr++;
164 1 : if (*ptr == '+') ptr++;
165 1 : if (*ptr == '\r') ptr++;
166 1 : if (*ptr == '\n') ptr++;
167 1 : already = (long)(buf + n - ptr);
168 1 : if (already > lsize) already = lsize;
169 : }
170 1 : long remaining = lsize - already;
171 : char tmp[512];
172 2 : while (remaining > 0) {
173 1 : int r2 = SSL_read(ssl, tmp,
174 : remaining > (long)sizeof(tmp) ?
175 : (int)sizeof(tmp) : (int)remaining);
176 1 : if (r2 <= 0) break;
177 1 : remaining -= r2;
178 : }
179 : /* RFC 3501: command = ... CRLF — the command-terminating \r\n
180 : * comes AFTER the literal body and must not be skipped. */
181 1 : char trail[4] = {0};
182 1 : int tr = SSL_read(ssl, trail, 2);
183 1 : int good_trail = (tr == 2 && trail[0] == '\r' && trail[1] == '\n');
184 : char ok2[128];
185 1 : if (good_trail) {
186 1 : snprintf(ok2, sizeof(ok2),
187 : "%s OK [APPENDUID 1 99] APPEND completed\r\n", tag);
188 : } else {
189 0 : snprintf(ok2, sizeof(ok2),
190 : "%s BAD Missing command-terminating CRLF after literal\r\n", tag);
191 : }
192 1 : SSL_write(ssl, ok2, (int)strlen(ok2));
193 : }
194 3 : } else if (strstr(buf, "LOGOUT")) {
195 2 : const char *bye = "* BYE Logging out\r\n";
196 2 : SSL_write(ssl, bye, (int)strlen(bye));
197 : char ok[64];
198 2 : snprintf(ok, sizeof(ok), "%s OK LOGOUT completed\r\n", tag);
199 2 : SSL_write(ssl, ok, (int)strlen(ok));
200 2 : break;
201 : } else {
202 : char bad[64];
203 1 : snprintf(bad, sizeof(bad), "%s BAD Unknown\r\n", tag);
204 1 : SSL_write(ssl, bad, (int)strlen(bad));
205 : }
206 : }
207 3 : SSL_shutdown(ssl);
208 3 : SSL_free(ssl);
209 3 : close(cfd);
210 3 : GCOV_FLUSH();
211 0 : _exit(0);
212 : }
213 :
214 : /* ── Test: imap_connect / read_response ─────────────────────────────────── */
215 :
216 : /*
217 : * Regression test for the use-after-free bug in read_response().
218 : *
219 : * Previously, linebuf_free() was called before strncasecmp(status, "OK"),
220 : * so if the allocator reused the memory the check would fail, returning -1
221 : * even though the server sent "TAG OK Logged in". This caused spurious
222 : * "LOGIN failed" errors in production.
223 : *
224 : * The test verifies that imap_connect() returns non-NULL (login accepted)
225 : * when the server genuinely responds with OK — including the long
226 : * [CAPABILITY ...] inline text that Dovecot sends, which increases the
227 : * chance of the allocator reusing freed memory.
228 : */
229 1 : void test_imap_connect_login_ok(void) {
230 1 : int port = 0;
231 1 : int lfd = make_listener(&port);
232 1 : ASSERT(lfd >= 0, "make_listener: could not bind");
233 :
234 1 : SSL_CTX *ctx = create_server_ctx();
235 1 : ASSERT(ctx != NULL, "create_server_ctx failed");
236 :
237 : /* Simulate a real Dovecot-style OK with a long CAPABILITY string so the
238 : * allocator is more likely to reuse memory if a bug exists. */
239 1 : const char *greeting =
240 : "* OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE"
241 : " LITERAL+ AUTH=PLAIN AUTH=LOGIN] Dovecot ready.\r\n";
242 : char login_reply[512];
243 1 : snprintf(login_reply, sizeof(login_reply),
244 : "A0001 OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE"
245 : " IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS"
246 : " THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT"
247 : " CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE"
248 : " QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH"
249 : " LIST-STATUS BINARY MOVE SNIPPET=FUZZY PREVIEW=FUZZY PREVIEW"
250 : " STATUS=SIZE SAVEDATE LITERAL+ NOTIFY SPECIAL-USE QUOTA]"
251 : " Logged in\r\n");
252 :
253 1 : pid_t pid = fork();
254 2 : ASSERT(pid >= 0, "fork failed");
255 :
256 2 : if (pid == 0) {
257 1 : run_mock_server(lfd, greeting, login_reply, ctx);
258 : /* _exit is called inside run_mock_server */
259 : }
260 1 : close(lfd);
261 1 : SSL_CTX_free(ctx);
262 :
263 : char url[64];
264 1 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
265 1 : ImapClient *c = imap_connect(url, "user", "pass", 0);
266 :
267 1 : ASSERT(c != NULL,
268 : "imap_connect must return non-NULL when server responds OK "
269 : "(regression: use-after-free in read_response caused OK check to fail)");
270 :
271 1 : imap_disconnect(c);
272 :
273 1 : wait_child(pid);
274 : }
275 :
276 : /*
277 : * Verify that imap_connect() returns NULL when the server explicitly
278 : * rejects the login (NO / BAD response).
279 : */
280 1 : void test_imap_connect_login_rejected(void) {
281 1 : int port = 0;
282 1 : int lfd = make_listener(&port);
283 1 : ASSERT(lfd >= 0, "make_listener: could not bind");
284 :
285 1 : SSL_CTX *ctx = create_server_ctx();
286 1 : ASSERT(ctx != NULL, "create_server_ctx failed");
287 :
288 1 : const char *greeting = "* OK Mock ready\r\n";
289 1 : const char *login_reply = "A0001 NO [AUTHENTICATIONFAILED] Invalid credentials\r\n";
290 :
291 1 : pid_t pid = fork();
292 2 : ASSERT(pid >= 0, "fork failed");
293 :
294 2 : if (pid == 0) {
295 1 : run_mock_server(lfd, greeting, login_reply, ctx);
296 : }
297 1 : close(lfd);
298 1 : SSL_CTX_free(ctx);
299 :
300 : char url[64];
301 1 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
302 1 : ImapClient *c = imap_connect(url, "bad_user", "bad_pass", 0);
303 :
304 1 : ASSERT(c == NULL, "imap_connect must return NULL when server says NO");
305 :
306 1 : wait_child(pid);
307 : }
308 :
309 : /*
310 : * Verify that imap_append() correctly uses LITERAL+ (non-synchronising
311 : * literal, "{N+}") and the server returns OK after receiving the message.
312 : */
313 1 : void test_imap_append_literal_plus(void) {
314 1 : int port = 0;
315 1 : int lfd = make_listener(&port);
316 1 : ASSERT(lfd >= 0, "make_listener: could not bind");
317 :
318 1 : SSL_CTX *ctx = create_server_ctx();
319 1 : ASSERT(ctx != NULL, "create_server_ctx failed");
320 :
321 1 : const char *greeting = "* OK Mock ready\r\n";
322 1 : const char *login_reply =
323 : "A0001 OK [CAPABILITY IMAP4rev1 LITERAL+] Logged in\r\n";
324 :
325 1 : pid_t pid = fork();
326 2 : ASSERT(pid >= 0, "fork failed");
327 :
328 2 : if (pid == 0) {
329 1 : run_mock_server(lfd, greeting, login_reply, ctx);
330 : }
331 1 : close(lfd);
332 1 : SSL_CTX_free(ctx);
333 :
334 : char url[64];
335 1 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
336 1 : ImapClient *c = imap_connect(url, "user", "pass", 0);
337 1 : ASSERT(c != NULL, "imap_append test: imap_connect must succeed");
338 :
339 1 : const char *msg = "From: a@b.com\r\nSubject: Test\r\n\r\nHello.\r\n";
340 1 : int rc = imap_append(c, "Sent", msg, strlen(msg));
341 1 : ASSERT(rc == 0, "imap_append must return 0 (OK) with LITERAL+");
342 :
343 1 : imap_disconnect(c);
344 :
345 1 : wait_child(pid);
346 : }
347 :
348 : /* ── Extended mock server: SELECT, SEARCH, FETCH, STORE, LIST, etc. ──── */
349 :
350 : /*
351 : * Stateful mock server that handles a fixed command sequence:
352 : * LOGIN → SELECT → SEARCH → FETCH(body) → FETCH(hdrs) → FETCH(flags)
353 : * → STORE → LIST → CREATE → DELETE → LOGOUT.
354 : * Each command is matched by keyword; the tag is extracted from the request.
355 : */
356 1 : static void run_mock_server_full(int listen_fd, SSL_CTX *ctx) {
357 1 : int cfd = accept(listen_fd, NULL, NULL);
358 1 : close(listen_fd);
359 1 : if (cfd < 0) { SSL_CTX_free(ctx);
360 0 : GCOV_FLUSH();
361 0 : _exit(1);
362 : }
363 :
364 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
365 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
366 :
367 1 : SSL *ssl = SSL_new(ctx);
368 1 : SSL_CTX_free(ctx);
369 1 : SSL_set_fd(ssl, cfd);
370 1 : if (SSL_accept(ssl) <= 0) {
371 0 : ERR_print_errors_fp(stderr);
372 0 : SSL_free(ssl); close(cfd);
373 0 : GCOV_FLUSH();
374 0 : _exit(1);
375 : }
376 :
377 : /* Greeting */
378 1 : const char *greet = "* OK Mock ready\r\n";
379 1 : SSL_write(ssl, greet, (int)strlen(greet));
380 :
381 : /* Message body used for FETCH responses */
382 1 : const char *msg_body = "From: a@b.com\r\nSubject: Test\r\n\r\nHello.\r\n";
383 1 : int msg_len = (int)strlen(msg_body);
384 1 : const char *hdrs = "From: a@b.com\r\nSubject: Test\r\n\r\n";
385 1 : int hdr_len = (int)strlen(hdrs);
386 :
387 : char buf[4096];
388 10 : while (1) {
389 11 : int n = SSL_read(ssl, buf, (int)(sizeof(buf) - 1));
390 11 : if (n <= 0) break;
391 11 : buf[n] = '\0';
392 :
393 11 : char tag[32] = "*";
394 11 : sscanf(buf, "%31s", tag);
395 :
396 : char resp[1024];
397 11 : if (strstr(buf, "LOGIN")) {
398 1 : snprintf(resp, sizeof(resp),
399 : "%s OK [CAPABILITY IMAP4rev1 LITERAL+] Logged in\r\n", tag);
400 1 : SSL_write(ssl, resp, (int)strlen(resp));
401 10 : } else if (strstr(buf, "SELECT")) {
402 1 : snprintf(resp, sizeof(resp),
403 : "* 3 EXISTS\r\n* 0 RECENT\r\n%s OK [READ-WRITE] SELECT completed\r\n",
404 : tag);
405 1 : SSL_write(ssl, resp, (int)strlen(resp));
406 9 : } else if (strstr(buf, "UID SEARCH")) {
407 1 : snprintf(resp, sizeof(resp),
408 : "* SEARCH 1 2 3\r\n%s OK SEARCH completed\r\n", tag);
409 1 : SSL_write(ssl, resp, (int)strlen(resp));
410 8 : } else if (strstr(buf, "BODY.PEEK[]")) {
411 : /* FETCH body literal */
412 1 : snprintf(resp, sizeof(resp),
413 : "* 1 FETCH (UID 1 BODY[] {%d}\r\n", msg_len);
414 1 : SSL_write(ssl, resp, (int)strlen(resp));
415 1 : SSL_write(ssl, msg_body, msg_len);
416 1 : snprintf(resp, sizeof(resp), ")\r\n%s OK FETCH completed\r\n", tag);
417 1 : SSL_write(ssl, resp, (int)strlen(resp));
418 7 : } else if (strstr(buf, "BODY.PEEK[HEADER]")) {
419 : /* FETCH headers literal */
420 1 : snprintf(resp, sizeof(resp),
421 : "* 1 FETCH (UID 1 BODY[HEADER] {%d}\r\n", hdr_len);
422 1 : SSL_write(ssl, resp, (int)strlen(resp));
423 1 : SSL_write(ssl, hdrs, hdr_len);
424 1 : snprintf(resp, sizeof(resp), ")\r\n%s OK FETCH completed\r\n", tag);
425 1 : SSL_write(ssl, resp, (int)strlen(resp));
426 6 : } else if (strstr(buf, "UID FETCH") && strstr(buf, "FLAGS")) {
427 1 : snprintf(resp, sizeof(resp),
428 : "* 1 FETCH (UID 1 FLAGS (\\Seen))\r\n%s OK FETCH completed\r\n",
429 : tag);
430 1 : SSL_write(ssl, resp, (int)strlen(resp));
431 5 : } else if (strstr(buf, "UID STORE")) {
432 1 : snprintf(resp, sizeof(resp),
433 : "* 1 FETCH (FLAGS (\\Seen \\Flagged))\r\n%s OK STORE completed\r\n",
434 : tag);
435 1 : SSL_write(ssl, resp, (int)strlen(resp));
436 4 : } else if (strstr(buf, "LIST")) {
437 1 : snprintf(resp, sizeof(resp),
438 : "* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"
439 : "* LIST (\\HasNoChildren) \".\" \"INBOX.Sent\"\r\n"
440 : "%s OK LIST completed\r\n", tag);
441 1 : SSL_write(ssl, resp, (int)strlen(resp));
442 3 : } else if (strstr(buf, "CREATE")) {
443 1 : snprintf(resp, sizeof(resp), "%s OK CREATE completed\r\n", tag);
444 1 : SSL_write(ssl, resp, (int)strlen(resp));
445 2 : } else if (strstr(buf, "DELETE")) {
446 1 : snprintf(resp, sizeof(resp), "%s OK DELETE completed\r\n", tag);
447 1 : SSL_write(ssl, resp, (int)strlen(resp));
448 1 : } else if (strstr(buf, "APPEND")) {
449 : /* Handle literal APPEND as before */
450 0 : char *lbrace = strrchr(buf, '{');
451 0 : long lsize = 0;
452 0 : int sync = 1;
453 0 : if (lbrace) {
454 0 : char *end = NULL;
455 0 : lsize = strtol(lbrace + 1, &end, 10);
456 0 : if (end && *end == '+') sync = 0;
457 : }
458 0 : if (lsize <= 0) {
459 0 : snprintf(resp, sizeof(resp), "%s BAD Missing size\r\n", tag);
460 0 : SSL_write(ssl, resp, (int)strlen(resp));
461 : } else {
462 0 : if (sync) SSL_write(ssl, "+ OK\r\n", 6);
463 0 : char *ptr = lbrace ? strchr(lbrace, '}') : NULL;
464 0 : long already = 0;
465 0 : if (ptr) {
466 0 : ptr++;
467 0 : if (*ptr == '+') ptr++;
468 0 : if (*ptr == '\r') ptr++;
469 0 : if (*ptr == '\n') ptr++;
470 0 : already = (long)(buf + n - ptr);
471 0 : if (already > lsize) already = lsize;
472 : }
473 0 : long remaining = lsize - already;
474 : char tmp[512];
475 0 : while (remaining > 0) {
476 0 : int r2 = SSL_read(ssl, tmp,
477 : remaining > (long)sizeof(tmp) ?
478 : (int)sizeof(tmp) : (int)remaining);
479 0 : if (r2 <= 0) break;
480 0 : remaining -= r2;
481 : }
482 0 : snprintf(resp, sizeof(resp),
483 : "%s OK [APPENDUID 1 99] APPEND completed\r\n", tag);
484 0 : SSL_write(ssl, resp, (int)strlen(resp));
485 : }
486 1 : } else if (strstr(buf, "LOGOUT")) {
487 1 : SSL_write(ssl, "* BYE Logging out\r\n", 19);
488 1 : snprintf(resp, sizeof(resp), "%s OK LOGOUT completed\r\n", tag);
489 1 : SSL_write(ssl, resp, (int)strlen(resp));
490 1 : break;
491 : } else {
492 0 : snprintf(resp, sizeof(resp), "%s BAD Unknown\r\n", tag);
493 0 : SSL_write(ssl, resp, (int)strlen(resp));
494 : }
495 : }
496 1 : SSL_shutdown(ssl);
497 1 : SSL_free(ssl);
498 1 : close(cfd);
499 1 : GCOV_FLUSH();
500 0 : _exit(0);
501 : }
502 :
503 : /*
504 : * Test that imap_select, imap_uid_search, imap_uid_fetch_body,
505 : * imap_uid_fetch_headers, imap_uid_fetch_flags, imap_uid_set_flag,
506 : * imap_list, imap_create_folder, imap_delete_folder, and
507 : * imap_set_progress all work against the extended mock server.
508 : */
509 1 : void test_imap_full_operations(void) {
510 1 : int port = 0;
511 1 : int lfd = make_listener(&port);
512 1 : ASSERT(lfd >= 0, "make_listener for full ops test");
513 :
514 1 : SSL_CTX *ctx = create_server_ctx();
515 1 : ASSERT(ctx != NULL, "create_server_ctx for full ops test");
516 :
517 1 : pid_t pid = fork();
518 2 : ASSERT(pid >= 0, "fork for full ops test");
519 2 : if (pid == 0) {
520 1 : run_mock_server_full(lfd, ctx);
521 : /* _exit called inside */
522 : }
523 1 : close(lfd);
524 1 : SSL_CTX_free(ctx);
525 :
526 : char url[64];
527 1 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
528 1 : ImapClient *c = imap_connect(url, "user", "pass", 0);
529 1 : ASSERT(c != NULL, "imap_full_ops: imap_connect must succeed");
530 :
531 : /* imap_set_progress: NULL guard + set callback to NULL */
532 1 : imap_set_progress(NULL, NULL, NULL); /* NULL client — no crash */
533 1 : imap_set_progress(c, NULL, NULL); /* clear callback */
534 :
535 : /* imap_select */
536 1 : int rc = imap_select(c, "INBOX");
537 1 : ASSERT(rc == 0, "imap_full_ops: imap_select returns 0");
538 :
539 : /* imap_uid_search */
540 1 : char (*uids)[17] = NULL;
541 1 : int count = 0;
542 1 : rc = imap_uid_search(c, "ALL", &uids, &count);
543 1 : ASSERT(rc == 0, "imap_full_ops: imap_uid_search returns 0");
544 1 : ASSERT(count == 3, "imap_full_ops: imap_uid_search found 3 UIDs");
545 1 : free(uids);
546 :
547 : /* imap_uid_fetch_body */
548 1 : char *body = imap_uid_fetch_body(c, "0000000000000001");
549 1 : ASSERT(body != NULL, "imap_full_ops: imap_uid_fetch_body returns non-NULL");
550 1 : free(body);
551 :
552 : /* imap_uid_fetch_headers */
553 1 : char *hdr = imap_uid_fetch_headers(c, "0000000000000001");
554 1 : ASSERT(hdr != NULL, "imap_full_ops: imap_uid_fetch_headers returns non-NULL");
555 1 : free(hdr);
556 :
557 : /* imap_uid_fetch_flags */
558 1 : int flags = imap_uid_fetch_flags(c, "0000000000000001");
559 : /* Server returns "FLAGS (\Seen)" → Seen set means NOT unseen */
560 1 : ASSERT(flags >= 0, "imap_full_ops: imap_uid_fetch_flags returns >= 0");
561 :
562 : /* imap_uid_set_flag */
563 1 : rc = imap_uid_set_flag(c, "0000000000000001", "\\Flagged", 1);
564 1 : ASSERT(rc == 0, "imap_full_ops: imap_uid_set_flag returns 0");
565 :
566 : /* imap_list */
567 1 : char **folders = NULL;
568 1 : int fc = 0;
569 1 : char sep = '.';
570 1 : rc = imap_list(c, &folders, &fc, &sep);
571 1 : ASSERT(rc == 0, "imap_full_ops: imap_list returns 0");
572 1 : ASSERT(fc == 2, "imap_full_ops: imap_list found 2 folders");
573 3 : for (int i = 0; i < fc; i++) free(folders[i]);
574 1 : free(folders);
575 1 : ASSERT(sep == '.', "imap_full_ops: separator is '.'");
576 :
577 : /* imap_create_folder */
578 1 : rc = imap_create_folder(c, "TestFolder");
579 1 : ASSERT(rc == 0, "imap_full_ops: imap_create_folder returns 0");
580 :
581 : /* imap_delete_folder */
582 1 : rc = imap_delete_folder(c, "TestFolder");
583 1 : ASSERT(rc == 0, "imap_full_ops: imap_delete_folder returns 0");
584 :
585 1 : imap_disconnect(c);
586 :
587 1 : wait_child(pid);
588 : }
589 :
590 : /* ── Extended mock server: QRESYNC + CONDSTORE + UID MOVE + CHANGEDSINCE ── */
591 :
592 : /*
593 : * Mock server that handles:
594 : * LOGIN → CAPABILITY → ENABLE QRESYNC → SELECT(CONDSTORE) → SELECT(QRESYNC)
595 : * → UID FETCH(CHANGEDSINCE) → UID COPY → UID STORE → EXPUNGE → LOGOUT
596 : *
597 : * Also exercises NIL separator in LIST, unquoted folder names, and
598 : * [ALREADYEXISTS] response for CREATE.
599 : */
600 1 : static void run_mock_server_ext(int listen_fd, SSL_CTX *ctx) {
601 1 : int cfd = accept(listen_fd, NULL, NULL);
602 1 : close(listen_fd);
603 1 : if (cfd < 0) { SSL_CTX_free(ctx); GCOV_FLUSH(); _exit(1); }
604 :
605 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
606 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
607 :
608 1 : SSL *ssl = SSL_new(ctx);
609 1 : SSL_CTX_free(ctx);
610 1 : SSL_set_fd(ssl, cfd);
611 1 : if (SSL_accept(ssl) <= 0) {
612 0 : ERR_print_errors_fp(stderr);
613 0 : SSL_free(ssl); close(cfd); GCOV_FLUSH(); _exit(1);
614 : }
615 :
616 1 : SSL_write(ssl, "* OK Mock ready\r\n", 17);
617 :
618 : char buf[4096];
619 12 : while (1) {
620 13 : int n = SSL_read(ssl, buf, (int)(sizeof(buf) - 1));
621 13 : if (n <= 0) break;
622 13 : buf[n] = '\0';
623 :
624 13 : char tag[32] = "*";
625 13 : sscanf(buf, "%31s", tag);
626 : char resp[2048];
627 :
628 13 : if (strstr(buf, "LOGIN")) {
629 1 : snprintf(resp, sizeof(resp),
630 : "%s OK [CAPABILITY IMAP4rev1 CONDSTORE QRESYNC LITERAL+] Logged in\r\n",
631 : tag);
632 1 : SSL_write(ssl, resp, (int)strlen(resp));
633 12 : } else if (strstr(buf, "CAPABILITY")) {
634 1 : snprintf(resp, sizeof(resp),
635 : "* CAPABILITY IMAP4rev1 CONDSTORE QRESYNC\r\n%s OK CAPABILITY completed\r\n",
636 : tag);
637 1 : SSL_write(ssl, resp, (int)strlen(resp));
638 11 : } else if (strstr(buf, "ENABLE")) {
639 1 : snprintf(resp, sizeof(resp),
640 : "* ENABLED QRESYNC\r\n%s OK ENABLE completed\r\n", tag);
641 1 : SSL_write(ssl, resp, (int)strlen(resp));
642 10 : } else if (strstr(buf, "SELECT") && strstr(buf, "QRESYNC")) {
643 : /* SELECT with QRESYNC: return VANISHED (EARLIER) + HIGHESTMODSEQ */
644 1 : snprintf(resp, sizeof(resp),
645 : "* 5 EXISTS\r\n"
646 : "* OK [UIDVALIDITY 12345] UIDs valid\r\n"
647 : "* OK [HIGHESTMODSEQ 999] Highest\r\n"
648 : "* VANISHED (EARLIER) 3:4\r\n"
649 : "%s OK [READ-WRITE] SELECT completed\r\n", tag);
650 1 : SSL_write(ssl, resp, (int)strlen(resp));
651 9 : } else if (strstr(buf, "SELECT") && strstr(buf, "CONDSTORE")) {
652 : /* SELECT with CONDSTORE: return HIGHESTMODSEQ in tagged OK */
653 1 : snprintf(resp, sizeof(resp),
654 : "* 5 EXISTS\r\n"
655 : "* OK [UIDVALIDITY 12345] UIDs valid\r\n"
656 : "%s OK [HIGHESTMODSEQ 42] SELECT completed\r\n", tag);
657 1 : SSL_write(ssl, resp, (int)strlen(resp));
658 8 : } else if (strstr(buf, "SELECT")) {
659 0 : snprintf(resp, sizeof(resp),
660 : "* 5 EXISTS\r\n%s OK [READ-WRITE] SELECT completed\r\n", tag);
661 0 : SSL_write(ssl, resp, (int)strlen(resp));
662 8 : } else if (strstr(buf, "CHANGEDSINCE")) {
663 : /* UID FETCH ... CHANGEDSINCE: return two flag-updated messages */
664 1 : snprintf(resp, sizeof(resp),
665 : "* 2 FETCH (UID 2 FLAGS (\\Seen))\r\n"
666 : "* 3 FETCH (UID 3 FLAGS (\\Seen \\Flagged))\r\n"
667 : "%s OK FETCH completed\r\n", tag);
668 1 : SSL_write(ssl, resp, (int)strlen(resp));
669 7 : } else if (strstr(buf, "UID FETCH") && strstr(buf, "FLAGS")) {
670 : /* plain UID FETCH FLAGS */
671 0 : snprintf(resp, sizeof(resp),
672 : "* 1 FETCH (UID 1 FLAGS (\\Seen))\r\n%s OK FETCH completed\r\n",
673 : tag);
674 0 : SSL_write(ssl, resp, (int)strlen(resp));
675 7 : } else if (strstr(buf, "UID COPY")) {
676 1 : snprintf(resp, sizeof(resp), "%s OK COPY completed\r\n", tag);
677 1 : SSL_write(ssl, resp, (int)strlen(resp));
678 6 : } else if (strstr(buf, "UID STORE")) {
679 1 : snprintf(resp, sizeof(resp),
680 : "* 1 FETCH (FLAGS (\\Deleted))\r\n%s OK STORE completed\r\n", tag);
681 1 : SSL_write(ssl, resp, (int)strlen(resp));
682 5 : } else if (strstr(buf, "EXPUNGE")) {
683 1 : snprintf(resp, sizeof(resp),
684 : "* 1 EXPUNGE\r\n%s OK EXPUNGE completed\r\n", tag);
685 1 : SSL_write(ssl, resp, (int)strlen(resp));
686 4 : } else if (strstr(buf, "LIST")) {
687 : /* Return one folder with NIL separator, one unquoted */
688 1 : snprintf(resp, sizeof(resp),
689 : "* LIST (\\HasNoChildren) NIL INBOX\r\n"
690 : "* LIST (\\HasNoChildren) \"/\" Archive\r\n"
691 : "%s OK LIST completed\r\n", tag);
692 1 : SSL_write(ssl, resp, (int)strlen(resp));
693 3 : } else if (strstr(buf, "CREATE")) {
694 : /* Return [ALREADYEXISTS] to exercise that branch */
695 2 : snprintf(resp, sizeof(resp),
696 : "%s NO [ALREADYEXISTS] Mailbox already exists\r\n", tag);
697 2 : SSL_write(ssl, resp, (int)strlen(resp));
698 1 : } else if (strstr(buf, "LOGOUT")) {
699 1 : SSL_write(ssl, "* BYE Logging out\r\n", 19);
700 1 : snprintf(resp, sizeof(resp), "%s OK LOGOUT completed\r\n", tag);
701 1 : SSL_write(ssl, resp, (int)strlen(resp));
702 1 : break;
703 : } else {
704 0 : snprintf(resp, sizeof(resp), "%s BAD Unknown\r\n", tag);
705 0 : SSL_write(ssl, resp, (int)strlen(resp));
706 : }
707 : }
708 1 : SSL_shutdown(ssl);
709 1 : SSL_free(ssl);
710 1 : close(cfd);
711 1 : GCOV_FLUSH();
712 0 : _exit(0);
713 : }
714 :
715 : /*
716 : * Test CONDSTORE/QRESYNC capabilities, uid_move, flags_changedsince,
717 : * LIST with NIL separator and unquoted names, CREATE [ALREADYEXISTS].
718 : */
719 1 : void test_imap_extended_operations(void) {
720 1 : int port = 0;
721 1 : int lfd = make_listener(&port);
722 1 : ASSERT(lfd >= 0, "make_listener for extended ops test");
723 :
724 1 : SSL_CTX *ctx = create_server_ctx();
725 1 : ASSERT(ctx != NULL, "create_server_ctx for extended ops test");
726 :
727 1 : pid_t pid = fork();
728 2 : ASSERT(pid >= 0, "fork for extended ops test");
729 2 : if (pid == 0) {
730 1 : run_mock_server_ext(lfd, ctx);
731 : /* _exit called inside */
732 : }
733 1 : close(lfd);
734 1 : SSL_CTX_free(ctx);
735 :
736 : char url[64];
737 1 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
738 1 : ImapClient *c = imap_connect(url, "user", "pass", 0);
739 1 : ASSERT(c != NULL, "imap_ext: imap_connect must succeed");
740 :
741 : /* imap_get_caps — queries CAPABILITY command */
742 1 : int caps = imap_get_caps(c);
743 1 : ASSERT((caps & IMAP_CAP_CONDSTORE) != 0, "imap_ext: CONDSTORE capability detected");
744 1 : ASSERT((caps & IMAP_CAP_QRESYNC) != 0, "imap_ext: QRESYNC capability detected");
745 :
746 : /* Calling again returns cached value (caps_queried path) */
747 1 : int caps2 = imap_get_caps(c);
748 1 : ASSERT(caps2 == caps, "imap_ext: imap_get_caps cached second call");
749 :
750 : /* imap_select_condstore — parses HIGHESTMODSEQ from tagged OK */
751 : ImapSelectResult res;
752 1 : int rc = imap_select_condstore(c, "INBOX", &res);
753 1 : ASSERT(rc == 0, "imap_ext: imap_select_condstore returns 0");
754 1 : ASSERT(res.highestmodseq == 42, "imap_ext: HIGHESTMODSEQ parsed from tagged OK");
755 1 : ASSERT(res.uidvalidity == 12345, "imap_ext: UIDVALIDITY parsed");
756 :
757 : /* imap_select_qresync — parses HIGHESTMODSEQ + VANISHED */
758 1 : memset(&res, 0, sizeof(res));
759 1 : rc = imap_select_qresync(c, "INBOX", 12345, 42, &res);
760 1 : ASSERT(rc == 0, "imap_ext: imap_select_qresync returns 0");
761 1 : ASSERT(res.highestmodseq == 999, "imap_ext: qresync HIGHESTMODSEQ=999");
762 1 : ASSERT(res.vanished_count >= 0, "imap_ext: vanished_count is set");
763 1 : free(res.vanished_uids);
764 :
765 : /* imap_uid_fetch_flags_changedsince */
766 1 : ImapFlagUpdate *updates = NULL;
767 1 : int upd_count = 0;
768 1 : rc = imap_uid_fetch_flags_changedsince(c, 10, &updates, &upd_count);
769 1 : ASSERT(rc == 0, "imap_ext: imap_uid_fetch_flags_changedsince returns 0");
770 1 : ASSERT(upd_count == 2, "imap_ext: changedsince returned 2 updates");
771 : /* UID 2 is Seen → no UNSEEN flag */
772 1 : ASSERT((updates[0].flags & 1) == 0, "imap_ext: UID 2 Seen — UNSEEN clear");
773 : /* UID 3 is Seen + Flagged */
774 1 : ASSERT((updates[1].flags & 2) != 0, "imap_ext: UID 3 Flagged");
775 1 : free(updates);
776 :
777 : /* imap_uid_move — uses UID COPY + UID STORE \\Deleted + EXPUNGE */
778 1 : rc = imap_uid_move(c, "0000000000000001", "Archive");
779 1 : ASSERT(rc == 0, "imap_ext: imap_uid_move returns 0");
780 :
781 : /* imap_list with NIL separator and unquoted name */
782 1 : char **folders = NULL;
783 1 : int fc = 0;
784 1 : char sep = '.';
785 1 : rc = imap_list(c, &folders, &fc, &sep);
786 1 : ASSERT(rc == 0, "imap_ext: imap_list (NIL sep) returns 0");
787 1 : ASSERT(fc == 2, "imap_ext: imap_list found 2 folders (unquoted)");
788 3 : for (int i = 0; i < fc; i++) free(folders[i]);
789 1 : free(folders);
790 :
791 : /* imap_create_folder with [ALREADYEXISTS] response — must return 0 */
792 1 : rc = imap_create_folder(c, "INBOX");
793 1 : ASSERT(rc == 0, "imap_ext: imap_create_folder [ALREADYEXISTS] treated as success");
794 :
795 1 : imap_disconnect(c);
796 :
797 1 : wait_child(pid);
798 : }
799 :
800 : /* ── Test: imap_connect with bare host URL and imap:// refusal ──────────── */
801 :
802 : /*
803 : * The imap_client always sends LOGIN with quoted user/pass (send_cmd uses
804 : * "LOGIN \"%s\" \"%s\""), so line 200 (unquoted username path) cannot be
805 : * reached through the public API. We test the other uncovered URL-parsing
806 : * branches instead:
807 : * • imap_connect with bare host URL (no scheme → IMAPS default)
808 : * • imap_connect with imap:// + verify_tls=1 must be refused (no TLS)
809 : */
810 1 : void test_imap_connect_bare_host(void) {
811 : /* A bare hostname (no imaps:// prefix) should be treated as IMAPS on
812 : * port 993. Since 127.0.0.1:993 is almost certainly not listening we
813 : * just verify that the function returns NULL without crashing. */
814 1 : ImapClient *c = imap_connect("127.0.0.1", "u", "p", 0);
815 : /* May succeed or fail depending on whether port 993 is open; either is fine.
816 : * The important thing is no crash and the URL is parsed correctly. */
817 1 : if (c) imap_disconnect(c);
818 1 : ASSERT(1, "bare host: no crash");
819 : }
820 :
821 1 : void test_imap_connect_refused_without_tls(void) {
822 : /* imap:// (non-TLS) with verify_tls=1 must be refused */
823 1 : ImapClient *c = imap_connect("imap://127.0.0.1:19143", "u", "p", 1);
824 1 : ASSERT(c == NULL, "imap:// without verify_tls=0 must be refused");
825 : }
826 :
827 : /* ── Test: plain (non-TLS) imap:// connection path ─────────────────────── */
828 :
829 : /*
830 : * Plain (non-TLS) IMAP server for testing the plain-socket I/O path.
831 : * Opened on a TCP socket without SSL — exercises net_read/net_write plain paths.
832 : */
833 1 : static void run_plain_imap_server(int listen_fd) {
834 1 : int cfd = accept(listen_fd, NULL, NULL);
835 1 : close(listen_fd);
836 1 : if (cfd < 0) { GCOV_FLUSH(); _exit(1); }
837 :
838 1 : struct timeval tv = {.tv_sec = 3, .tv_usec = 0};
839 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
840 :
841 : /* Plain greeting */
842 1 : const char *greet = "* OK Mock plain IMAP ready\r\n";
843 1 : ssize_t _wr; _wr = write(cfd, greet, strlen(greet)); (void)_wr;
844 :
845 : char buf[2048];
846 4 : while (1) {
847 5 : ssize_t n = read(cfd, buf, sizeof(buf) - 1);
848 5 : if (n <= 0) break;
849 5 : buf[n] = '\0';
850 5 : char tag[32] = "*";
851 5 : sscanf(buf, "%31s", tag);
852 : char resp[256];
853 5 : if (strstr(buf, "LOGIN")) {
854 1 : snprintf(resp, sizeof(resp), "%s OK Logged in\r\n", tag);
855 1 : _wr = write(cfd, resp, strlen(resp)); (void)_wr;
856 4 : } else if (strstr(buf, "LIST")) {
857 1 : snprintf(resp, sizeof(resp),
858 : "* LIST (\\HasNoChildren) \".\" INBOX\r\n"
859 : "%s OK LIST completed\r\n", tag);
860 1 : _wr = write(cfd, resp, strlen(resp)); (void)_wr;
861 3 : } else if (strstr(buf, "SELECT")) {
862 1 : snprintf(resp, sizeof(resp),
863 : "* 0 EXISTS\r\n%s OK SELECT completed\r\n", tag);
864 1 : _wr = write(cfd, resp, strlen(resp)); (void)_wr;
865 2 : } else if (strstr(buf, "UID SEARCH")) {
866 1 : snprintf(resp, sizeof(resp),
867 : "* SEARCH\r\n%s OK SEARCH completed\r\n", tag);
868 1 : _wr = write(cfd, resp, strlen(resp)); (void)_wr;
869 1 : } else if (strstr(buf, "LOGOUT")) {
870 1 : _wr = write(cfd, "* BYE Bye\r\n", 11); (void)_wr;
871 1 : snprintf(resp, sizeof(resp), "%s OK LOGOUT completed\r\n", tag);
872 1 : _wr = write(cfd, resp, strlen(resp)); (void)_wr;
873 1 : break;
874 : } else {
875 0 : snprintf(resp, sizeof(resp), "%s BAD Unknown\r\n", tag);
876 0 : _wr = write(cfd, resp, strlen(resp)); (void)_wr;
877 : }
878 : }
879 1 : close(cfd);
880 1 : GCOV_FLUSH();
881 0 : _exit(0);
882 : }
883 :
884 : /*
885 : * Connect with imap:// (no TLS) and verify_tls=0 so the plain socket code
886 : * paths (net_read plain, net_write plain) are exercised.
887 : */
888 1 : void test_imap_plain_socket_ops(void) {
889 1 : int port = 0;
890 1 : int lfd = make_listener(&port);
891 1 : ASSERT(lfd >= 0, "plain: make_listener");
892 :
893 1 : pid_t pid = fork();
894 2 : ASSERT(pid >= 0, "plain: fork");
895 2 : if (pid == 0) {
896 1 : run_plain_imap_server(lfd);
897 : /* _exit inside */
898 : }
899 1 : close(lfd);
900 :
901 : char url[64];
902 1 : snprintf(url, sizeof(url), "imap://127.0.0.1:%d", port);
903 1 : ImapClient *c = imap_connect(url, "user", "pass", 0);
904 1 : ASSERT(c != NULL, "plain: imap_connect (no TLS, verify=0) must succeed");
905 :
906 : /* Exercise plain net_write path via SELECT */
907 1 : int rc = imap_select(c, "INBOX");
908 1 : ASSERT(rc == 0, "plain: imap_select returns 0");
909 :
910 : /* Exercise plain net_write + net_read via UID SEARCH */
911 1 : char (*uids)[17] = NULL;
912 1 : int count = 0;
913 1 : rc = imap_uid_search(c, "ALL", &uids, &count);
914 1 : ASSERT(rc == 0, "plain: imap_uid_search returns 0");
915 1 : free(uids);
916 :
917 : /* imap_list exercises more plain I/O */
918 1 : char **folders = NULL;
919 1 : int fc = 0;
920 1 : char sep = '.';
921 1 : rc = imap_list(c, &folders, &fc, &sep);
922 1 : ASSERT(rc == 0, "plain: imap_list returns 0");
923 2 : for (int i = 0; i < fc; i++) free(folders[i]);
924 1 : free(folders);
925 :
926 1 : imap_disconnect(c);
927 :
928 1 : wait_child(pid);
929 : }
930 :
931 : /* ── Test: imap_list with empty (quoted "") separator ───────────────────── */
932 :
933 1 : static void run_mock_server_empty_sep(int listen_fd, SSL_CTX *ctx) {
934 1 : int cfd = accept(listen_fd, NULL, NULL);
935 1 : close(listen_fd);
936 1 : if (cfd < 0) { SSL_CTX_free(ctx); GCOV_FLUSH(); _exit(1); }
937 :
938 1 : struct timeval tv = {.tv_sec = 3, .tv_usec = 0};
939 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
940 :
941 1 : SSL *ssl = SSL_new(ctx);
942 1 : SSL_CTX_free(ctx);
943 1 : SSL_set_fd(ssl, cfd);
944 1 : if (SSL_accept(ssl) <= 0) {
945 0 : SSL_free(ssl); close(cfd); GCOV_FLUSH(); _exit(1);
946 : }
947 :
948 1 : SSL_write(ssl, "* OK Mock ready\r\n", 17);
949 :
950 : char buf[2048];
951 2 : while (1) {
952 3 : int n = SSL_read(ssl, buf, (int)(sizeof(buf) - 1));
953 3 : if (n <= 0) break;
954 3 : buf[n] = '\0';
955 3 : char tag[32] = "*";
956 3 : sscanf(buf, "%31s", tag);
957 : char resp[512];
958 3 : if (strstr(buf, "LOGIN")) {
959 1 : snprintf(resp, sizeof(resp), "%s OK Logged in\r\n", tag);
960 1 : SSL_write(ssl, resp, (int)strlen(resp));
961 2 : } else if (strstr(buf, "LIST")) {
962 : /* Empty separator (quoted "") — exercises the *p=='"' branch */
963 1 : snprintf(resp, sizeof(resp),
964 : "* LIST () \"\" \"INBOX\"\r\n"
965 : "%s OK LIST completed\r\n", tag);
966 1 : SSL_write(ssl, resp, (int)strlen(resp));
967 1 : } else if (strstr(buf, "LOGOUT")) {
968 1 : SSL_write(ssl, "* BYE\r\n", 7);
969 1 : snprintf(resp, sizeof(resp), "%s OK LOGOUT completed\r\n", tag);
970 1 : SSL_write(ssl, resp, (int)strlen(resp));
971 1 : break;
972 : } else {
973 0 : snprintf(resp, sizeof(resp), "%s BAD Unknown\r\n", tag);
974 0 : SSL_write(ssl, resp, (int)strlen(resp));
975 : }
976 : }
977 1 : SSL_shutdown(ssl);
978 1 : SSL_free(ssl);
979 1 : close(cfd);
980 1 : GCOV_FLUSH();
981 0 : _exit(0);
982 : }
983 :
984 1 : void test_imap_list_empty_separator(void) {
985 1 : int port = 0;
986 1 : int lfd = make_listener(&port);
987 1 : ASSERT(lfd >= 0, "empty_sep: make_listener");
988 :
989 1 : SSL_CTX *ctx = create_server_ctx();
990 1 : ASSERT(ctx != NULL, "empty_sep: create_server_ctx");
991 :
992 1 : pid_t pid = fork();
993 2 : ASSERT(pid >= 0, "empty_sep: fork");
994 2 : if (pid == 0) {
995 1 : run_mock_server_empty_sep(lfd, ctx);
996 : }
997 1 : close(lfd);
998 1 : SSL_CTX_free(ctx);
999 :
1000 : char url[64];
1001 1 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
1002 1 : ImapClient *c = imap_connect(url, "user", "pass", 0);
1003 1 : ASSERT(c != NULL, "empty_sep: imap_connect must succeed");
1004 :
1005 1 : char **folders = NULL;
1006 1 : int fc = 0;
1007 1 : char sep = 'x';
1008 1 : int rc = imap_list(c, &folders, &fc, &sep);
1009 1 : ASSERT(rc == 0, "empty_sep: imap_list returns 0");
1010 1 : ASSERT(fc == 1, "empty_sep: imap_list found 1 folder");
1011 2 : for (int i = 0; i < fc; i++) free(folders[i]);
1012 1 : free(folders);
1013 :
1014 1 : imap_disconnect(c);
1015 :
1016 1 : wait_child(pid);
1017 : }
1018 :
1019 : /* ── Test: QRESYNC VANISHED without (EARLIER) + uid_fetch with no literal ── */
1020 :
1021 : /*
1022 : * Mock server that:
1023 : * 1. On SELECT+QRESYNC: sends VANISHED without "(EARLIER)" — covers lines 1052-1053
1024 : * 2. On UID FETCH BODY.PEEK[]: sends OK with no literal — covers line 798
1025 : */
1026 1 : static void run_mock_server_vanished_noearlier(int listen_fd, SSL_CTX *ctx) {
1027 1 : int cfd = accept(listen_fd, NULL, NULL);
1028 1 : close(listen_fd);
1029 1 : if (cfd < 0) { SSL_CTX_free(ctx); GCOV_FLUSH(); _exit(1); }
1030 :
1031 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
1032 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1033 :
1034 1 : SSL *ssl = SSL_new(ctx);
1035 1 : SSL_CTX_free(ctx);
1036 1 : SSL_set_fd(ssl, cfd);
1037 1 : if (SSL_accept(ssl) <= 0) {
1038 0 : SSL_free(ssl); close(cfd); GCOV_FLUSH(); _exit(1);
1039 : }
1040 :
1041 1 : SSL_write(ssl, "* OK Mock ready\r\n", 17);
1042 :
1043 : char buf[4096];
1044 4 : while (1) {
1045 5 : int n = SSL_read(ssl, buf, (int)(sizeof(buf) - 1));
1046 5 : if (n <= 0) break;
1047 5 : buf[n] = '\0';
1048 5 : char tag[32] = "*";
1049 5 : sscanf(buf, "%31s", tag);
1050 : char resp[1024];
1051 5 : if (strstr(buf, "LOGIN")) {
1052 1 : snprintf(resp, sizeof(resp),
1053 : "%s OK [CAPABILITY IMAP4rev1 CONDSTORE QRESYNC] Logged in\r\n", tag);
1054 1 : SSL_write(ssl, resp, (int)strlen(resp));
1055 4 : } else if (strstr(buf, "ENABLE")) {
1056 1 : snprintf(resp, sizeof(resp),
1057 : "* ENABLED QRESYNC\r\n%s OK ENABLE completed\r\n", tag);
1058 1 : SSL_write(ssl, resp, (int)strlen(resp));
1059 3 : } else if (strstr(buf, "SELECT") && strstr(buf, "QRESYNC")) {
1060 : /* VANISHED without "(EARLIER)" — hits the else branch in parse */
1061 1 : snprintf(resp, sizeof(resp),
1062 : "* 2 EXISTS\r\n"
1063 : "* OK [HIGHESTMODSEQ 500]\r\n"
1064 : "* VANISHED 5:7\r\n" /* no (EARLIER) */
1065 : "%s OK SELECT completed\r\n", tag);
1066 1 : SSL_write(ssl, resp, (int)strlen(resp));
1067 2 : } else if (strstr(buf, "BODY.PEEK[]")) {
1068 : /* Return OK but NO literal body — exercises line 798 logger_log */
1069 1 : snprintf(resp, sizeof(resp),
1070 : "* 1 FETCH (UID 1)\r\n%s OK FETCH completed\r\n", tag);
1071 1 : SSL_write(ssl, resp, (int)strlen(resp));
1072 1 : } else if (strstr(buf, "LOGOUT")) {
1073 1 : SSL_write(ssl, "* BYE\r\n", 7);
1074 1 : snprintf(resp, sizeof(resp), "%s OK LOGOUT completed\r\n", tag);
1075 1 : SSL_write(ssl, resp, (int)strlen(resp));
1076 1 : break;
1077 : } else {
1078 0 : snprintf(resp, sizeof(resp), "%s BAD Unknown\r\n", tag);
1079 0 : SSL_write(ssl, resp, (int)strlen(resp));
1080 : }
1081 : }
1082 1 : SSL_shutdown(ssl);
1083 1 : SSL_free(ssl);
1084 1 : close(cfd);
1085 1 : GCOV_FLUSH();
1086 0 : _exit(0);
1087 : }
1088 :
1089 1 : void test_imap_qresync_vanished_no_earlier(void) {
1090 1 : int port = 0;
1091 1 : int lfd = make_listener(&port);
1092 1 : ASSERT(lfd >= 0, "vanished_no_earlier: make_listener");
1093 :
1094 1 : SSL_CTX *ctx = create_server_ctx();
1095 1 : ASSERT(ctx != NULL, "vanished_no_earlier: create_server_ctx");
1096 :
1097 1 : pid_t pid = fork();
1098 2 : ASSERT(pid >= 0, "vanished_no_earlier: fork");
1099 2 : if (pid == 0) {
1100 1 : run_mock_server_vanished_noearlier(lfd, ctx);
1101 : }
1102 1 : close(lfd);
1103 1 : SSL_CTX_free(ctx);
1104 :
1105 : char url[64];
1106 1 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
1107 1 : ImapClient *c = imap_connect(url, "user", "pass", 0);
1108 1 : ASSERT(c != NULL, "vanished_no_earlier: imap_connect must succeed");
1109 :
1110 : /* imap_select_qresync with VANISHED without (EARLIER) */
1111 : ImapSelectResult res;
1112 1 : memset(&res, 0, sizeof(res));
1113 1 : int rc = imap_select_qresync(c, "INBOX", 1000, 50, &res);
1114 1 : ASSERT(rc == 0, "vanished_no_earlier: imap_select_qresync returns 0");
1115 : /* VANISHED 5:7 expands to UIDs 5, 6, 7 */
1116 1 : ASSERT(res.vanished_count >= 0, "vanished_no_earlier: vanished_count set");
1117 1 : free(res.vanished_uids);
1118 :
1119 : /* imap_uid_fetch_body with no literal in response — returns NULL, logs warning */
1120 1 : char *body = imap_uid_fetch_body(c, "0000000000000001");
1121 1 : ASSERT(body == NULL, "vanished_no_earlier: uid_fetch_body NULL when no literal");
1122 :
1123 1 : imap_disconnect(c);
1124 :
1125 1 : wait_child(pid);
1126 : }
1127 :
1128 : /* ── Test suite entry point ──────────────────────────────────────────────── */
1129 :
1130 1 : void test_imap_client(void) {
1131 : /* Verify that a NULL pointer is handled gracefully */
1132 1 : imap_disconnect(NULL);
1133 :
1134 : /* imap_connect with a bad host must return NULL, not crash */
1135 1 : ImapClient *c = imap_connect("imaps://invalid.host.example.invalid",
1136 : "user", "pass", 1);
1137 1 : ASSERT(c == NULL, "imap_connect to invalid host should return NULL");
1138 :
1139 1 : test_imap_connect_login_ok();
1140 1 : test_imap_connect_login_rejected();
1141 1 : test_imap_append_literal_plus();
1142 1 : test_imap_full_operations();
1143 1 : test_imap_extended_operations();
1144 1 : test_imap_connect_bare_host();
1145 1 : test_imap_connect_refused_without_tls();
1146 1 : test_imap_plain_socket_ops();
1147 1 : test_imap_list_empty_separator();
1148 1 : test_imap_qresync_vanished_no_earlier();
1149 : }
|