LCOV - code coverage report
Current view: top level - tests/unit - test_imap_client.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 51.4 % 177 91
Test Date: 2026-04-15 21:12:52 Functions: 85.7 % 7 6

            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 <sys/socket.h>
       7              : #include <sys/wait.h>
       8              : #include <netinet/in.h>
       9              : #include <arpa/inet.h>
      10              : #include <openssl/ssl.h>
      11              : #include <openssl/err.h>
      12              : 
      13              : /* ── Helpers ─────────────────────────────────────────────────────────────── */
      14              : 
      15              : /*
      16              :  * Create a listening TCP socket bound to a random loopback port.
      17              :  * Returns the fd and fills *port_out with the actual port number.
      18              :  * Returns -1 on error.
      19              :  */
      20            3 : static int make_listener(int *port_out) {
      21            3 :     int fd = socket(AF_INET, SOCK_STREAM, 0);
      22            3 :     if (fd < 0) return -1;
      23            3 :     int one = 1;
      24            3 :     setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
      25            3 :     struct sockaddr_in addr = {0};
      26            3 :     addr.sin_family      = AF_INET;
      27            3 :     addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
      28            3 :     addr.sin_port        = 0; /* OS picks a free port */
      29            6 :     if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ||
      30            3 :         listen(fd, 1) < 0) {
      31            0 :         close(fd);
      32            0 :         return -1;
      33              :     }
      34            3 :     socklen_t len = sizeof(addr);
      35            3 :     getsockname(fd, (struct sockaddr *)&addr, &len);
      36            3 :     *port_out = ntohs(addr.sin_port);
      37            3 :     return fd;
      38              : }
      39              : 
      40              : /*
      41              :  * Create a server-side SSL_CTX loaded with the test self-signed certificate.
      42              :  * Returns NULL on failure.
      43              :  */
      44            3 : static SSL_CTX *create_server_ctx(void) {
      45            3 :     SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
      46            3 :     if (!ctx) {
      47            0 :         ERR_print_errors_fp(stderr);
      48            0 :         return NULL;
      49              :     }
      50            3 :     if (SSL_CTX_use_certificate_file(ctx, "tests/certs/test.crt", SSL_FILETYPE_PEM) <= 0) {
      51            0 :         fprintf(stderr, "Failed to load test cert: tests/certs/test.crt\n");
      52            0 :         ERR_print_errors_fp(stderr);
      53            0 :         SSL_CTX_free(ctx);
      54            0 :         return NULL;
      55              :     }
      56            3 :     if (SSL_CTX_use_PrivateKey_file(ctx, "tests/certs/test.key", SSL_FILETYPE_PEM) <= 0) {
      57            0 :         fprintf(stderr, "Failed to load test key: tests/certs/test.key\n");
      58            0 :         ERR_print_errors_fp(stderr);
      59            0 :         SSL_CTX_free(ctx);
      60            0 :         return NULL;
      61              :     }
      62            3 :     return ctx;
      63              : }
      64              : 
      65              : /*
      66              :  * Minimal IMAP server child process (TLS).
      67              :  *
      68              :  * Accepts ONE connection over TLS, sends `greeting`, then loops reading lines
      69              :  * and replies according to the command:
      70              :  *   LOGIN  → sends login_reply
      71              :  *   LOGOUT → sends BYE + OK, exits cleanly
      72              :  *   other  → sends "TAG BAD Unknown"
      73              :  *
      74              :  * After the first LOGOUT (or client disconnect) the child exits.
      75              :  */
      76            0 : static void run_mock_server(int listen_fd,
      77              :                              const char *greeting,
      78              :                              const char *login_reply,
      79              :                              SSL_CTX *ctx) {
      80            0 :     int cfd = accept(listen_fd, NULL, NULL);
      81            0 :     close(listen_fd);
      82            0 :     if (cfd < 0) {
      83            0 :         SSL_CTX_free(ctx);
      84            0 :         _exit(1);
      85              :     }
      86              : 
      87              :     /* timeout so the child never hangs in CI */
      88            0 :     struct timeval tv = {.tv_sec = 3, .tv_usec = 0};
      89            0 :     setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
      90              : 
      91            0 :     SSL *ssl = SSL_new(ctx);
      92            0 :     SSL_CTX_free(ctx);   /* child no longer needs ctx after SSL_new */
      93            0 :     SSL_set_fd(ssl, cfd);
      94            0 :     if (SSL_accept(ssl) <= 0) {
      95            0 :         ERR_print_errors_fp(stderr);
      96            0 :         SSL_free(ssl);
      97            0 :         close(cfd);
      98            0 :         _exit(1);
      99              :     }
     100              : 
     101            0 :     SSL_write(ssl, greeting, (int)strlen(greeting));
     102              : 
     103            0 :     char buf[1024];
     104            0 :     while (1) {
     105            0 :         int n = SSL_read(ssl, buf, (int)(sizeof(buf) - 1));
     106            0 :         if (n <= 0) break;
     107            0 :         buf[n] = '\0';
     108              : 
     109              :         /* Extract tag (first token) */
     110            0 :         char tag[32] = "*";
     111            0 :         sscanf(buf, "%31s", tag);
     112              : 
     113            0 :         if (strstr(buf, "LOGIN")) {
     114            0 :             SSL_write(ssl, login_reply, (int)strlen(login_reply));
     115            0 :         } else if (strstr(buf, "APPEND")) {
     116              :             /* Handle LITERAL+ {N+} and synchronising {N} literals */
     117            0 :             char *lbrace = strrchr(buf, '{');
     118            0 :             long lsize = 0;
     119            0 :             int  sync  = 1;
     120            0 :             if (lbrace) {
     121            0 :                 char *end = NULL;
     122            0 :                 lsize = strtol(lbrace + 1, &end, 10);
     123            0 :                 if (end && *end == '+') sync = 0;
     124              :             }
     125            0 :             if (lsize <= 0) {
     126            0 :                 char bad2[64];
     127            0 :                 snprintf(bad2, sizeof(bad2), "%s BAD Missing size\r\n", tag);
     128            0 :                 SSL_write(ssl, bad2, (int)strlen(bad2));
     129              :             } else {
     130            0 :                 if (sync) SSL_write(ssl, "+ OK\r\n", 6);
     131              :                 /* Drain the literal bytes already in buf + remaining reads */
     132            0 :                 char *ptr = lbrace ? strchr(lbrace, '}') : NULL;
     133            0 :                 long already = 0;
     134            0 :                 if (ptr) {
     135              :                     /* skip past '}' and optional '+' and '\r\n' */
     136            0 :                     ptr++;
     137            0 :                     if (*ptr == '+') ptr++;
     138            0 :                     if (*ptr == '\r') ptr++;
     139            0 :                     if (*ptr == '\n') ptr++;
     140            0 :                     already = (long)(buf + n - ptr);
     141            0 :                     if (already > lsize) already = lsize;
     142              :                 }
     143            0 :                 long remaining = lsize - already;
     144            0 :                 char tmp[512];
     145            0 :                 while (remaining > 0) {
     146            0 :                     int r2 = SSL_read(ssl, tmp,
     147              :                                      remaining > (long)sizeof(tmp) ?
     148              :                                      (int)sizeof(tmp) : (int)remaining);
     149            0 :                     if (r2 <= 0) break;
     150            0 :                     remaining -= r2;
     151              :                 }
     152            0 :                 char ok2[80];
     153            0 :                 snprintf(ok2, sizeof(ok2),
     154              :                          "%s OK [APPENDUID 1 99] APPEND completed\r\n", tag);
     155            0 :                 SSL_write(ssl, ok2, (int)strlen(ok2));
     156              :             }
     157            0 :         } else if (strstr(buf, "LOGOUT")) {
     158            0 :             const char *bye = "* BYE Logging out\r\n";
     159            0 :             SSL_write(ssl, bye, (int)strlen(bye));
     160            0 :             char ok[64];
     161            0 :             snprintf(ok, sizeof(ok), "%s OK LOGOUT completed\r\n", tag);
     162            0 :             SSL_write(ssl, ok, (int)strlen(ok));
     163            0 :             break;
     164              :         } else {
     165            0 :             char bad[64];
     166            0 :             snprintf(bad, sizeof(bad), "%s BAD Unknown\r\n", tag);
     167            0 :             SSL_write(ssl, bad, (int)strlen(bad));
     168              :         }
     169              :     }
     170            0 :     SSL_shutdown(ssl);
     171            0 :     SSL_free(ssl);
     172            0 :     close(cfd);
     173            0 :     _exit(0);
     174              : }
     175              : 
     176              : /* ── Test: imap_connect / read_response ─────────────────────────────────── */
     177              : 
     178              : /*
     179              :  * Regression test for the use-after-free bug in read_response().
     180              :  *
     181              :  * Previously, linebuf_free() was called before strncasecmp(status, "OK"),
     182              :  * so if the allocator reused the memory the check would fail, returning -1
     183              :  * even though the server sent "TAG OK Logged in".  This caused spurious
     184              :  * "LOGIN failed" errors in production.
     185              :  *
     186              :  * The test verifies that imap_connect() returns non-NULL (login accepted)
     187              :  * when the server genuinely responds with OK — including the long
     188              :  * [CAPABILITY ...] inline text that Dovecot sends, which increases the
     189              :  * chance of the allocator reusing freed memory.
     190              :  */
     191            1 : void test_imap_connect_login_ok(void) {
     192            1 :     int port = 0;
     193            1 :     int lfd  = make_listener(&port);
     194            1 :     ASSERT(lfd >= 0, "make_listener: could not bind");
     195              : 
     196            1 :     SSL_CTX *ctx = create_server_ctx();
     197            1 :     ASSERT(ctx != NULL, "create_server_ctx failed");
     198              : 
     199              :     /* Simulate a real Dovecot-style OK with a long CAPABILITY string so the
     200              :      * allocator is more likely to reuse memory if a bug exists. */
     201            1 :     const char *greeting =
     202              :         "* OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE"
     203              :         " LITERAL+ AUTH=PLAIN AUTH=LOGIN] Dovecot ready.\r\n";
     204            1 :     char login_reply[512];
     205            1 :     snprintf(login_reply, sizeof(login_reply),
     206              :              "A0001 OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE"
     207              :              " IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS"
     208              :              " THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT"
     209              :              " CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE"
     210              :              " QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH"
     211              :              " LIST-STATUS BINARY MOVE SNIPPET=FUZZY PREVIEW=FUZZY PREVIEW"
     212              :              " STATUS=SIZE SAVEDATE LITERAL+ NOTIFY SPECIAL-USE QUOTA]"
     213              :              " Logged in\r\n");
     214              : 
     215            1 :     pid_t pid = fork();
     216            1 :     ASSERT(pid >= 0, "fork failed");
     217              : 
     218            1 :     if (pid == 0) {
     219            0 :         run_mock_server(lfd, greeting, login_reply, ctx);
     220              :         /* _exit is called inside run_mock_server */
     221              :     }
     222            1 :     close(lfd);
     223            1 :     SSL_CTX_free(ctx);
     224              : 
     225            1 :     char url[64];
     226            1 :     snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
     227            1 :     ImapClient *c = imap_connect(url, "user", "pass", 0);
     228              : 
     229            1 :     ASSERT(c != NULL,
     230              :            "imap_connect must return non-NULL when server responds OK "
     231              :            "(regression: use-after-free in read_response caused OK check to fail)");
     232              : 
     233            1 :     imap_disconnect(c);
     234              : 
     235            1 :     int status = 0;
     236            1 :     waitpid(pid, &status, 0);
     237              : }
     238              : 
     239              : /*
     240              :  * Verify that imap_connect() returns NULL when the server explicitly
     241              :  * rejects the login (NO / BAD response).
     242              :  */
     243            1 : void test_imap_connect_login_rejected(void) {
     244            1 :     int port = 0;
     245            1 :     int lfd  = make_listener(&port);
     246            1 :     ASSERT(lfd >= 0, "make_listener: could not bind");
     247              : 
     248            1 :     SSL_CTX *ctx = create_server_ctx();
     249            1 :     ASSERT(ctx != NULL, "create_server_ctx failed");
     250              : 
     251            1 :     const char *greeting     = "* OK Mock ready\r\n";
     252            1 :     const char *login_reply  = "A0001 NO [AUTHENTICATIONFAILED] Invalid credentials\r\n";
     253              : 
     254            1 :     pid_t pid = fork();
     255            1 :     ASSERT(pid >= 0, "fork failed");
     256              : 
     257            1 :     if (pid == 0) {
     258            0 :         run_mock_server(lfd, greeting, login_reply, ctx);
     259              :     }
     260            1 :     close(lfd);
     261            1 :     SSL_CTX_free(ctx);
     262              : 
     263            1 :     char url[64];
     264            1 :     snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
     265            1 :     ImapClient *c = imap_connect(url, "bad_user", "bad_pass", 0);
     266              : 
     267            1 :     ASSERT(c == NULL, "imap_connect must return NULL when server says NO");
     268              : 
     269            1 :     int status = 0;
     270            1 :     waitpid(pid, &status, 0);
     271              : }
     272              : 
     273              : /*
     274              :  * Verify that imap_append() correctly uses LITERAL+ (non-synchronising
     275              :  * literal, "{N+}") and the server returns OK after receiving the message.
     276              :  */
     277            1 : void test_imap_append_literal_plus(void) {
     278            1 :     int port = 0;
     279            1 :     int lfd  = make_listener(&port);
     280            1 :     ASSERT(lfd >= 0, "make_listener: could not bind");
     281              : 
     282            1 :     SSL_CTX *ctx = create_server_ctx();
     283            1 :     ASSERT(ctx != NULL, "create_server_ctx failed");
     284              : 
     285            1 :     const char *greeting    = "* OK Mock ready\r\n";
     286            1 :     const char *login_reply =
     287              :         "A0001 OK [CAPABILITY IMAP4rev1 LITERAL+] Logged in\r\n";
     288              : 
     289            1 :     pid_t pid = fork();
     290            1 :     ASSERT(pid >= 0, "fork failed");
     291              : 
     292            1 :     if (pid == 0) {
     293            0 :         run_mock_server(lfd, greeting, login_reply, ctx);
     294              :     }
     295            1 :     close(lfd);
     296            1 :     SSL_CTX_free(ctx);
     297              : 
     298            1 :     char url[64];
     299            1 :     snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
     300            1 :     ImapClient *c = imap_connect(url, "user", "pass", 0);
     301            1 :     ASSERT(c != NULL, "imap_append test: imap_connect must succeed");
     302              : 
     303            1 :     const char *msg = "From: a@b.com\r\nSubject: Test\r\n\r\nHello.\r\n";
     304            1 :     int rc = imap_append(c, "Sent", msg, strlen(msg));
     305            1 :     ASSERT(rc == 0, "imap_append must return 0 (OK) with LITERAL+");
     306              : 
     307            1 :     imap_disconnect(c);
     308              : 
     309            1 :     int status = 0;
     310            1 :     waitpid(pid, &status, 0);
     311              : }
     312              : 
     313              : /* ── Test suite entry point ──────────────────────────────────────────────── */
     314              : 
     315            1 : void test_imap_client(void) {
     316              :     /* Verify that a NULL pointer is handled gracefully */
     317            1 :     imap_disconnect(NULL);
     318              : 
     319              :     /* imap_connect with a bad host must return NULL, not crash */
     320            1 :     ImapClient *c = imap_connect("imaps://invalid.host.example.invalid",
     321              :                                   "user", "pass", 1);
     322            1 :     ASSERT(c == NULL, "imap_connect to invalid host should return NULL");
     323              : 
     324            1 :     test_imap_connect_login_ok();
     325            1 :     test_imap_connect_login_rejected();
     326            1 :     test_imap_append_literal_plus();
     327              : }
        

Generated by: LCOV version 2.0-1