LCOV - code coverage report
Current view: top level - tests/unit - test_imap_client.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 87.6 % 663 581
Test Date: 2026-05-07 15:53:07 Functions: 100.0 % 20 20

            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              : }
        

Generated by: LCOV version 2.0-1