LCOV - code coverage report
Current view: top level - libemail/src/infrastructure - imap_client.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 75.8 % 488 370
Test Date: 2026-04-15 21:12:52 Functions: 85.2 % 27 23

            Line data    Source code
       1              : #include "imap_client.h"
       2              : #include "imap_util.h"
       3              : #include "local_store.h"
       4              : #include "logger.h"
       5              : #include "raii.h"
       6              : #include <openssl/ssl.h>
       7              : #include <openssl/err.h>
       8              : #include <sys/types.h>
       9              : #include <sys/socket.h>
      10              : #include <netdb.h>
      11              : #include <unistd.h>
      12              : #include <string.h>
      13              : #include <stdlib.h>
      14              : #include <stdio.h>
      15              : #include <errno.h>
      16              : #include <stdint.h>
      17              : #include <ctype.h>
      18              : #include <time.h>
      19              : 
      20              : /* ── Read buffer ─────────────────────────────────────────────────────── */
      21              : 
      22              : #define RBUF_SIZE 65536
      23              : 
      24              : struct ImapClient {
      25              :     int      fd;
      26              :     SSL_CTX *ctx;
      27              :     SSL     *ssl;
      28              :     int      use_tls;
      29              :     int      tag_num;
      30              : 
      31              :     /* Receive ring buffer */
      32              :     char     rbuf[RBUF_SIZE];
      33              :     size_t   rbuf_pos;  /* read position */
      34              :     size_t   rbuf_len;  /* bytes available */
      35              : 
      36              :     /* Optional download-progress callback (set via imap_set_progress) */
      37              :     ImapProgressFn on_progress;
      38              :     void          *progress_ctx;
      39              : };
      40              : 
      41              : /* ── Low-level I/O ───────────────────────────────────────────────────── */
      42              : 
      43              : /** Read up to `n` bytes from the socket/TLS layer into `buf`.
      44              :  *  Returns bytes read (>0), 0 on EOF, -1 on error. */
      45          788 : static ssize_t net_read(ImapClient *c, char *buf, size_t n) {
      46          788 :     if (c->use_tls) {
      47          788 :         int r = SSL_read(c->ssl, buf, (int)n);
      48          788 :         if (r > 0) return (ssize_t)r;
      49            0 :         int err = SSL_get_error(c->ssl, r);
      50            0 :         if (err == SSL_ERROR_ZERO_RETURN) return 0;
      51            0 :         logger_log(LOG_WARN, "SSL_read error %d", err);
      52            0 :         return -1;
      53              :     }
      54            0 :     ssize_t r = read(c->fd, buf, n);
      55            0 :     if (r < 0 && (errno == EINTR || errno == EAGAIN)) return 0;
      56            0 :     return r;
      57              : }
      58              : 
      59              : /** Write `n` bytes to the socket/TLS layer.
      60              :  *  Returns 0 on success, -1 on error. */
      61          367 : static int net_write(ImapClient *c, const char *buf, size_t n) {
      62          367 :     size_t sent = 0;
      63          734 :     while (sent < n) {
      64              :         ssize_t r;
      65          367 :         if (c->use_tls) {
      66          367 :             int w = SSL_write(c->ssl, buf + sent, (int)(n - sent));
      67          367 :             if (w <= 0) {
      68            0 :                 logger_log(LOG_WARN, "SSL_write error %d",
      69            0 :                            SSL_get_error(c->ssl, w));
      70            0 :                 return -1;
      71              :             }
      72          367 :             r = (ssize_t)w;
      73              :         } else {
      74            0 :             r = write(c->fd, buf + sent, n - sent);
      75            0 :             if (r < 0) {
      76            0 :                 if (errno == EINTR) continue;
      77            0 :                 logger_log(LOG_WARN, "write: %s", strerror(errno));
      78            0 :                 return -1;
      79              :             }
      80              :         }
      81          367 :         sent += (size_t)r;
      82              :     }
      83          367 :     return 0;
      84              : }
      85              : 
      86              : /* ── Buffered byte reader ────────────────────────────────────────────── */
      87              : 
      88              : /** Ensure at least 1 byte is available in rbuf. Returns 0 on success, -1 on EOF/error. */
      89        20660 : static int rbuf_fill(ImapClient *c) {
      90        20660 :     if (c->rbuf_pos < c->rbuf_len) return 0;
      91              :     /* Compact: move unused data to front */
      92          760 :     c->rbuf_pos = 0;
      93          760 :     c->rbuf_len = 0;
      94          760 :     ssize_t r = net_read(c, c->rbuf, RBUF_SIZE);
      95          760 :     if (r <= 0) return -1;
      96          760 :     c->rbuf_len = (size_t)r;
      97          760 :     return 0;
      98              : }
      99              : 
     100              : /** Read exactly `n` bytes into `out`. Returns 0 on success, -1 on error/EOF. */
     101           28 : static int rbuf_read_exact(ImapClient *c, char *out, size_t n) {
     102           28 :     size_t got = 0;
     103           56 :     while (got < n) {
     104           28 :         if (c->rbuf_pos >= c->rbuf_len) {
     105           28 :             c->rbuf_pos = 0;
     106           28 :             c->rbuf_len = 0;
     107           28 :             ssize_t r = net_read(c, c->rbuf, RBUF_SIZE);
     108           28 :             if (r <= 0) return -1;
     109           28 :             c->rbuf_len = (size_t)r;
     110              :         }
     111           28 :         size_t avail = c->rbuf_len - c->rbuf_pos;
     112           28 :         size_t take  = avail < (n - got) ? avail : (n - got);
     113           28 :         memcpy(out + got, c->rbuf + c->rbuf_pos, take);
     114           28 :         c->rbuf_pos += take;
     115           28 :         got         += take;
     116              :     }
     117           28 :     return 0;
     118              : }
     119              : 
     120              : /* ── Dynamic line buffer ─────────────────────────────────────────────── */
     121              : 
     122              : typedef struct {
     123              :     char  *data;
     124              :     size_t len;
     125              :     size_t cap;
     126              : } LineBuf;
     127              : 
     128          449 : static void linebuf_free(LineBuf *lb) { free(lb->data); lb->data = NULL; lb->len = lb->cap = 0; }
     129              : 
     130        18938 : static int linebuf_append(LineBuf *lb, char ch) {
     131        18938 :     if (lb->len + 1 >= lb->cap) {
     132          450 :         size_t ncap = lb->cap ? lb->cap * 2 : 256;
     133          450 :         char *tmp = realloc(lb->data, ncap);
     134          450 :         if (!tmp) return -1;
     135          450 :         lb->data = tmp;
     136          450 :         lb->cap  = ncap;
     137              :     }
     138        18938 :     lb->data[lb->len++] = ch;
     139        18938 :     lb->data[lb->len]   = '\0';
     140        18938 :     return 0;
     141              : }
     142              : 
     143              : /**
     144              :  * Read one CRLF-terminated line from the server into `lb`.
     145              :  * The trailing \r\n is stripped. Returns 0 on success, -1 on error/EOF.
     146              :  */
     147          861 : static int read_line(ImapClient *c, LineBuf *lb) {
     148          861 :     lb->len = 0;
     149        19799 :     for (;;) {
     150        20660 :         if (rbuf_fill(c) != 0) return -1;
     151        20660 :         char ch = c->rbuf[c->rbuf_pos++];
     152        20660 :         if (ch == '\r') continue;          /* skip CR */
     153        19799 :         if (ch == '\n') {
     154          861 :             if (lb->data) lb->data[lb->len] = '\0';
     155          861 :             return 0;
     156              :         }
     157        18938 :         if (linebuf_append(lb, ch) != 0) return -1;
     158              :     }
     159              : }
     160              : 
     161              : /* ── Command dispatch ────────────────────────────────────────────────── */
     162              : 
     163              : /** Send a formatted IMAP command prefixed with the next tag.
     164              :  *  `fmt` should NOT include the tag or trailing CRLF.
     165              :  *  The tag used is written to `tag_out` (capacity >= 16).
     166              :  *  Returns 0 on success, -1 on error. */
     167          365 : static int send_cmd(ImapClient *c, char tag_out[16], const char *fmt, ...) {
     168          365 :     c->tag_num++;
     169          365 :     snprintf(tag_out, 16, "A%04d", c->tag_num);
     170              : 
     171          365 :     char buf[4096];
     172          365 :     va_list ap;
     173          365 :     va_start(ap, fmt);
     174          365 :     int len = vsnprintf(buf, sizeof(buf) - 3, fmt, ap);
     175          365 :     va_end(ap);
     176          365 :     if (len < 0 || (size_t)len >= sizeof(buf) - 3) return -1;
     177              : 
     178              :     /* Append CRLF */
     179          365 :     buf[len]     = '\r';
     180          365 :     buf[len + 1] = '\n';
     181          365 :     buf[len + 2] = '\0';
     182              : 
     183              :     /* Log command — mask password in LOGIN commands */
     184          365 :     if (strncmp(buf, "LOGIN ", 6) == 0) {
     185              :         /* Extract user (first quoted token) and replace password with xxxxx */
     186           54 :         const char *p = buf + 6;
     187              :         /* skip optional leading space */
     188           54 :         while (*p == ' ') p++;
     189              :         /* find end of username token (quoted or unquoted) */
     190           54 :         const char *user_end = NULL;
     191           54 :         if (*p == '"') {
     192           54 :             user_end = strchr(p + 1, '"');
     193           54 :             if (user_end) user_end++;  /* include closing quote */
     194              :         } else {
     195            0 :             user_end = strchr(p, ' ');
     196              :         }
     197           54 :         if (user_end) {
     198           54 :             logger_log(LOG_DEBUG, "IMAP [OUT] %s LOGIN %.*s xxxxx",
     199           54 :                        tag_out, (int)(user_end - p), p);
     200              :         } else {
     201            0 :             logger_log(LOG_DEBUG, "IMAP [OUT] %s LOGIN <redacted>", tag_out);
     202              :         }
     203              :     } else {
     204          311 :         logger_log(LOG_DEBUG, "IMAP [OUT] %s %.*s", tag_out, len, buf);
     205              :     }
     206              : 
     207              :     /* Write tag + space + command + CRLF as a single buffer so the entire
     208              :      * command arrives in one TCP segment and a line-reading server can parse
     209              :      * it correctly (two separate writes would be two packets on loopback). */
     210          365 :     char full[4096 + 32];
     211          365 :     int flen = snprintf(full, sizeof(full), "%s %s", tag_out, buf);
     212          365 :     if (flen < 0 || (size_t)flen >= sizeof(full)) return -1;
     213          365 :     if (net_write(c, full, (size_t)flen) != 0) return -1;
     214          365 :     return 0;
     215              : }
     216              : 
     217              : /* ── Literal reader ──────────────────────────────────────────────────── */
     218              : 
     219              : /* Minimum literal size (bytes) before the progress callback is invoked */
     220              : #define PROGRESS_THRESHOLD (100 * 1024)
     221              : /* Progress is reported every this many bytes */
     222              : #define PROGRESS_CHUNK     (128 * 1024)
     223              : 
     224              : /**
     225              :  * If line ends with `{N}`, read N literal bytes into a heap buffer.
     226              :  * `*lit_out` is set to the allocated buffer (NUL-terminated) and
     227              :  * `*lit_len` to N.  Returns 0 (no literal), N > 0 (literal read), -1 on error.
     228              :  * Calls c->on_progress (if set) every PROGRESS_CHUNK bytes for large literals.
     229              :  */
     230          412 : static long read_literal_if_present(ImapClient *c, const char *line,
     231              :                                     char **lit_out, size_t *lit_len) {
     232          412 :     *lit_out = NULL;
     233          412 :     *lit_len = 0;
     234              : 
     235              :     /* Find trailing {N} */
     236          412 :     const char *p = strrchr(line, '{');
     237          412 :     if (!p) return 0;
     238           28 :     char *end;
     239           28 :     long sz = strtol(p + 1, &end, 10);
     240           28 :     if (*end != '}' || sz < 0) return 0;
     241              : 
     242              :     /* Allocate output buffer */
     243           28 :     char *buf = malloc((size_t)sz + 1);
     244           28 :     if (!buf) return -1;
     245              : 
     246           28 :     if (sz > 0) {
     247           28 :         size_t total = (size_t)sz;
     248              : 
     249           28 :         if (!c->on_progress || total < PROGRESS_THRESHOLD) {
     250              :             /* Small literal or no callback: read all at once */
     251           28 :             if (rbuf_read_exact(c, buf, total) != 0) { free(buf); return -1; }
     252              :         } else {
     253              :             /* Large literal: read in chunks and report progress */
     254            0 :             size_t got = 0;
     255            0 :             size_t next_report = PROGRESS_CHUNK;
     256            0 :             while (got < total) {
     257            0 :                 size_t want = PROGRESS_CHUNK < (total - got)
     258              :                               ? PROGRESS_CHUNK : (total - got);
     259            0 :                 if (rbuf_read_exact(c, buf + got, want) != 0) {
     260            0 :                     free(buf); return -1;
     261              :                 }
     262            0 :                 got += want;
     263            0 :                 if (got >= next_report || got == total) {
     264            0 :                     c->on_progress(got, total, c->progress_ctx);
     265            0 :                     next_report = got + PROGRESS_CHUNK;
     266              :                 }
     267              :             }
     268              :         }
     269              :     }
     270              : 
     271           28 :     buf[sz] = '\0';
     272           28 :     *lit_out = buf;
     273           28 :     *lit_len = (size_t)sz;
     274           28 :     return sz;
     275              : }
     276              : 
     277            0 : void imap_set_progress(ImapClient *c, ImapProgressFn fn, void *ctx) {
     278            0 :     if (!c) return;
     279            0 :     c->on_progress   = fn;
     280            0 :     c->progress_ctx  = ctx;
     281              : }
     282              : 
     283              : /* ── Response reader ─────────────────────────────────────────────────── */
     284              : 
     285              : typedef struct {
     286              :     char  **untagged;  /* heap-allocated array of untagged response strings */
     287              :     int     count;
     288              :     int     cap;
     289              :     char   *literal;  /* the last literal body (first literal wins) */
     290              :     size_t  lit_len;
     291              : } Response;
     292              : 
     293          367 : static void response_free(Response *r) {
     294          779 :     for (int i = 0; i < r->count; i++) free(r->untagged[i]);
     295          367 :     free(r->untagged);
     296          367 :     free(r->literal);
     297          367 :     memset(r, 0, sizeof(*r));
     298          367 : }
     299              : 
     300          412 : static int response_add(Response *r, const char *line) {
     301          412 :     if (r->count == r->cap) {
     302          311 :         int ncap = r->cap ? r->cap * 2 : 16;
     303          311 :         char **tmp = realloc(r->untagged, (size_t)ncap * sizeof(char *));
     304          311 :         if (!tmp) return -1;
     305          311 :         r->untagged = tmp;
     306          311 :         r->cap      = ncap;
     307              :     }
     308          412 :     char *copy = strdup(line);
     309          412 :     if (!copy) return -1;
     310          412 :     r->untagged[r->count++] = copy;
     311          412 :     return 0;
     312              : }
     313              : 
     314              : /**
     315              :  * Read server responses until we see our tagged reply.
     316              :  * Collects untagged lines and the first literal body encountered.
     317              :  * Returns 0 on OK, -1 on NO/BAD/error.
     318              :  */
     319          367 : static int read_response(ImapClient *c, const char *tag, Response *r) {
     320          367 :     LineBuf lb = {NULL, 0, 0};
     321              : 
     322          412 :     for (;;) {
     323          779 :         if (read_line(c, &lb) != 0) {
     324            0 :             linebuf_free(&lb);
     325            0 :             return -1;
     326              :         }
     327              : 
     328          779 :         const char *line = lb.data ? lb.data : "";
     329          779 :         logger_log(LOG_DEBUG, "IMAP [ IN] %s", line);
     330              : 
     331              :         /* Tagged response? */
     332          779 :         size_t tlen = strlen(tag);
     333          779 :         if (strncmp(line, tag, tlen) == 0 && line[tlen] == ' ') {
     334          367 :             const char *status = line + tlen + 1;
     335          367 :             int ok = (strncasecmp(status, "OK", 2) == 0);
     336          367 :             if (!ok)
     337            1 :                 logger_log(LOG_WARN, "IMAP %s", line);
     338          367 :             linebuf_free(&lb);  /* free AFTER all accesses to line/status */
     339          367 :             return ok ? 0 : -1;
     340              :         }
     341              : 
     342              :         /* Untagged: check for literal */
     343          412 :         char  *lit     = NULL;
     344          412 :         size_t lit_len = 0;
     345          412 :         long   lsz     = read_literal_if_present(c, line, &lit, &lit_len);
     346          412 :         if (lsz < 0) { linebuf_free(&lb); return -1; }
     347              : 
     348          412 :         response_add(r, line);
     349              : 
     350          412 :         if (lit) {
     351              :             /* We read the literal; now read the closing line: ")\r\n" or similar */
     352           28 :             if (!r->literal) {
     353           28 :                 r->literal = lit;
     354           28 :                 r->lit_len = lit_len;
     355              :             } else {
     356            0 :                 free(lit);  /* second literal — discard (shouldn't happen) */
     357              :             }
     358              :             /* Read the remainder line after the literal (e.g. ")" or empty) */
     359           28 :             LineBuf trail = {NULL, 0, 0};
     360           28 :             if (read_line(c, &trail) == 0 && trail.data && trail.data[0])
     361           28 :                 logger_log(LOG_DEBUG, "IMAP [ IN] %s", trail.data);
     362           28 :             linebuf_free(&trail);
     363              :         }
     364              :     }
     365              : }
     366              : 
     367              : /* ── URL parser ──────────────────────────────────────────────────────── */
     368              : 
     369              : /**
     370              :  * Parse "imaps://host:port" or "imap://host" into components.
     371              :  * Returns 0 on success.
     372              :  */
     373           55 : static int parse_url(const char *url, char *host, size_t hsize,
     374              :                      char *port, size_t psize, int *use_tls) {
     375           55 :     *use_tls = 0;
     376           55 :     const char *p = url;
     377              : 
     378           55 :     if (strncasecmp(p, "imaps://", 8) == 0) { *use_tls = 1; p += 8; }
     379            0 :     else if (strncasecmp(p, "imap://", 7) == 0) { p += 7; }
     380              :     else {
     381              :         /* Treat as bare hostname, default IMAPS */
     382            0 :         *use_tls = 1;
     383            0 :         snprintf(host, hsize, "%s", url);
     384            0 :         snprintf(port, psize, "993");
     385            0 :         return 0;
     386              :     }
     387              : 
     388              :     /* host[:port] */
     389           55 :     const char *colon = strchr(p, ':');
     390           55 :     if (colon) {
     391           54 :         size_t hlen = (size_t)(colon - p);
     392           54 :         if (hlen >= hsize) return -1;
     393           54 :         memcpy(host, p, hlen);
     394           54 :         host[hlen] = '\0';
     395           54 :         snprintf(port, psize, "%s", colon + 1);
     396              :     } else {
     397            1 :         snprintf(host, hsize, "%s", p);
     398            1 :         snprintf(port, psize, "%s", *use_tls ? "993" : "143");
     399              :     }
     400           55 :     return 0;
     401              : }
     402              : 
     403              : /* ── Connect ─────────────────────────────────────────────────────────── */
     404              : 
     405           55 : ImapClient *imap_connect(const char *host_url, const char *user,
     406              :                          const char *pass, int verify_tls) {
     407           55 :     char host[256], port[16];
     408           55 :     int  use_tls = 1;
     409           55 :     if (parse_url(host_url, host, sizeof(host), port, sizeof(port), &use_tls) != 0) {
     410            0 :         logger_log(LOG_ERROR, "imap_connect: bad URL: %s", host_url);
     411            0 :         return NULL;
     412              :     }
     413              : 
     414              :     /* Hard enforcement: never connect without TLS unless verify_tls == 0
     415              :      * (SSL_NO_VERIFY=1 in config — test/dev environments only). */
     416           55 :     if (!use_tls && verify_tls) {
     417            0 :         logger_log(LOG_ERROR,
     418              :                    "imap_connect: refused to connect to %s without TLS — "
     419              :                    "use imaps:// to protect credentials", host_url);
     420            0 :         fprintf(stderr,
     421              :                 "Error: Refused to connect to %s without TLS.\n"
     422              :                 "Use imaps:// in EMAIL_HOST to protect your password.\n"
     423              :                 "For test environments only: add SSL_NO_VERIFY=1 to config.\n",
     424              :                 host_url);
     425            0 :         return NULL;
     426              :     }
     427              : 
     428              :     /* TCP connect */
     429           55 :     struct addrinfo hints = {0};
     430           55 :     hints.ai_family   = AF_UNSPEC;
     431           55 :     hints.ai_socktype = SOCK_STREAM;
     432           55 :     struct addrinfo *ai = NULL;
     433           55 :     int rc = getaddrinfo(host, port, &hints, &ai);
     434           55 :     if (rc != 0) {
     435            1 :         logger_log(LOG_ERROR, "getaddrinfo(%s:%s): %s", host, port, gai_strerror(rc));
     436            1 :         return NULL;
     437              :     }
     438              : 
     439           54 :     int fd = -1;
     440          105 :     for (struct addrinfo *r = ai; r; r = r->ai_next) {
     441          105 :         fd = socket(r->ai_family, r->ai_socktype, r->ai_protocol);
     442          105 :         if (fd < 0) continue;
     443          105 :         if (connect(fd, r->ai_addr, r->ai_addrlen) == 0) break;
     444           51 :         close(fd);
     445           51 :         fd = -1;
     446              :     }
     447           54 :     freeaddrinfo(ai);
     448              : 
     449           54 :     if (fd < 0) {
     450            0 :         logger_log(LOG_ERROR, "connect to %s:%s failed: %s", host, port, strerror(errno));
     451            0 :         return NULL;
     452              :     }
     453              : 
     454              :     /* Apply a 15-second read/write timeout so blocking ops don't hang forever */
     455              :     {
     456           54 :         struct timeval tv = { .tv_sec = 15, .tv_usec = 0 };
     457           54 :         setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
     458           54 :         setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
     459              :     }
     460              : 
     461           54 :     ImapClient *c = calloc(1, sizeof(ImapClient));
     462           54 :     if (!c) { close(fd); return NULL; }
     463           54 :     c->fd = fd;
     464           54 :     c->use_tls = use_tls;
     465              : 
     466              :     /* TLS handshake */
     467           54 :     if (use_tls) {
     468              :         /* Init OpenSSL (idempotent in OpenSSL 1.1+) */
     469           54 :         SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
     470           54 :         if (!ctx) {
     471            0 :             logger_log(LOG_ERROR, "SSL_CTX_new failed");
     472            0 :             free(c); close(fd);
     473            0 :             return NULL;
     474              :         }
     475           54 :         SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
     476           54 :         if (!verify_tls) {
     477           54 :             SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
     478              :         } else {
     479            0 :             SSL_CTX_set_default_verify_paths(ctx);
     480            0 :             SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
     481              :         }
     482           54 :         SSL *ssl = SSL_new(ctx);
     483           54 :         if (!ssl) {
     484            0 :             logger_log(LOG_ERROR, "SSL_new failed");
     485            0 :             SSL_CTX_free(ctx); free(c); close(fd);
     486            0 :             return NULL;
     487              :         }
     488           54 :         SSL_set_fd(ssl, fd);
     489           54 :         SSL_set_tlsext_host_name(ssl, host);  /* SNI */
     490           54 :         if (SSL_connect(ssl) != 1) {
     491            0 :             logger_log(LOG_ERROR, "SSL handshake failed with %s", host);
     492            0 :             SSL_free(ssl); SSL_CTX_free(ctx); free(c); close(fd);
     493            0 :             return NULL;
     494              :         }
     495           54 :         c->ctx = ctx;
     496           54 :         c->ssl = ssl;
     497           54 :         logger_log(LOG_DEBUG, "IMAP TLS handshake OK with %s (TLS/%s)",
     498              :                    host, SSL_get_version(ssl));
     499              :     } else {
     500            0 :         logger_log(LOG_WARN,
     501              :                    "IMAP connecting to %s:%s WITHOUT TLS — "
     502              :                    "credentials will be sent in plaintext!", host, port);
     503              :     }
     504              : 
     505              :     /* Read server greeting */
     506           54 :     LineBuf lb = {NULL, 0, 0};
     507           54 :     if (read_line(c, &lb) != 0) {
     508            0 :         logger_log(LOG_ERROR, "No greeting from %s", host);
     509            0 :         goto fail;
     510              :     }
     511           54 :     logger_log(LOG_DEBUG, "IMAP [ IN] %s", lb.data ? lb.data : "");
     512           54 :     linebuf_free(&lb);
     513              : 
     514              :     /* LOGIN */
     515           54 :     char tag[16];
     516              :     /* Send LOGIN with literal username/password to handle special chars correctly.
     517              :      * Simple approach: just quote them (assume no embedded DQUOTE or backslash). */
     518           54 :     if (send_cmd(c, tag, "LOGIN \"%s\" \"%s\"", user, pass) != 0)
     519            0 :         goto fail;
     520              : 
     521           54 :     Response resp = {0};
     522           54 :     rc = read_response(c, tag, &resp);
     523           54 :     response_free(&resp);
     524           54 :     if (rc != 0) {
     525            1 :         logger_log(LOG_ERROR, "LOGIN failed for user %s on %s", user, host);
     526            1 :         goto fail;
     527              :     }
     528              : 
     529           53 :     logger_log(LOG_DEBUG, "IMAP connected and authenticated: %s@%s", user, host);
     530           53 :     return c;
     531              : 
     532            1 : fail:
     533            1 :     if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); }
     534            1 :     if (c->ctx) SSL_CTX_free(c->ctx);
     535            1 :     close(c->fd);
     536            1 :     free(c);
     537            1 :     return NULL;
     538              : }
     539              : 
     540              : /* ── Disconnect ──────────────────────────────────────────────────────── */
     541              : 
     542           54 : void imap_disconnect(ImapClient *c) {
     543           54 :     if (!c) return;
     544              :     /* Send LOGOUT (ignore errors — we're closing anyway) */
     545           53 :     char tag[16];
     546           53 :     send_cmd(c, tag, "LOGOUT");
     547           53 :     Response r = {0};
     548           53 :     read_response(c, tag, &r);
     549           53 :     response_free(&r);
     550              : 
     551           53 :     if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); }
     552           53 :     if (c->ctx) SSL_CTX_free(c->ctx);
     553           53 :     if (c->fd >= 0) close(c->fd);
     554           53 :     free(c);
     555              : }
     556              : 
     557              : /* ── LIST ────────────────────────────────────────────────────────────── */
     558              : 
     559              : /**
     560              :  * Parse one `* LIST (\flags) "sep" "name"` or `* LIST (\flags) "sep" name` line.
     561              :  * Returns heap-allocated folder name (Modified UTF-7, not yet decoded), or NULL.
     562              :  * Sets *sep_out to the separator character.
     563              :  */
     564            4 : static char *parse_list_line(const char *line, char *sep_out) {
     565              :     /* Skip "* LIST " */
     566            4 :     if (strncasecmp(line, "* LIST ", 7) != 0) return NULL;
     567            4 :     const char *p = line + 7;
     568              : 
     569              :     /* Skip flags: (...) */
     570            4 :     if (*p == '(') {
     571            4 :         p = strchr(p, ')');
     572            4 :         if (!p) return NULL;
     573            4 :         p++;
     574              :     }
     575            8 :     while (*p == ' ') p++;
     576              : 
     577              :     /* Separator: "." or "/" or NIL */
     578            4 :     if (*p == '"') {
     579            4 :         p++;
     580            4 :         if (*p && *(p + 1) == '"') {
     581            4 :             *sep_out = *p;
     582            4 :             p += 2;
     583            0 :         } else if (*p == '"') {
     584              :             /* empty separator */
     585            0 :             p++;
     586              :         }
     587            0 :     } else if (strncasecmp(p, "NIL", 3) == 0) {
     588            0 :         *sep_out = '.';
     589            0 :         p += 3;
     590              :     }
     591            8 :     while (*p == ' ') p++;
     592              : 
     593              :     /* Folder name: quoted or unquoted */
     594            4 :     if (*p == '"') {
     595            4 :         p++;
     596            4 :         const char *end = strchr(p, '"');
     597            4 :         if (!end) return NULL;
     598            4 :         size_t len = (size_t)(end - p);
     599            4 :         char *name = malloc(len + 1);
     600            4 :         if (!name) return NULL;
     601            4 :         memcpy(name, p, len);
     602            4 :         name[len] = '\0';
     603            4 :         return name;
     604              :     } else {
     605              :         /* Unquoted: until end of line */
     606            0 :         size_t len = strlen(p);
     607            0 :         while (len > 0 && (p[len - 1] == ' ' || p[len - 1] == '\r')) len--;
     608            0 :         char *name = malloc(len + 1);
     609            0 :         if (!name) return NULL;
     610            0 :         memcpy(name, p, len);
     611            0 :         name[len] = '\0';
     612            0 :         return name;
     613              :     }
     614              : }
     615              : 
     616            1 : int imap_list(ImapClient *c, char ***folders_out, int *count_out, char *sep_out) {
     617            1 :     *folders_out = NULL;
     618            1 :     *count_out   = 0;
     619            1 :     if (sep_out) *sep_out = '.';
     620              : 
     621            1 :     char tag[16];
     622            1 :     if (send_cmd(c, tag, "LIST \"\" \"*\"") != 0) return -1;
     623              : 
     624            1 :     Response resp = {0};
     625            1 :     if (read_response(c, tag, &resp) != 0) {
     626            0 :         response_free(&resp);
     627            0 :         return -1;
     628              :     }
     629              : 
     630            1 :     int count = 0, cap = 0;
     631            1 :     char **folders = NULL;
     632            1 :     char  sep = '.';
     633              : 
     634            5 :     for (int i = 0; i < resp.count; i++) {
     635            4 :         char got_sep = '.';
     636            4 :         char *raw = parse_list_line(resp.untagged[i], &got_sep);
     637            4 :         if (!raw) continue;
     638            4 :         sep = got_sep;
     639            4 :         char *name = imap_utf7_decode(raw);
     640            4 :         free(raw);
     641            4 :         if (!name) continue;
     642              : 
     643            4 :         if (count == cap) {
     644            1 :             cap = cap ? cap * 2 : 16;
     645            1 :             char **tmp = realloc(folders, (size_t)cap * sizeof(char *));
     646            1 :             if (!tmp) { free(name); break; }
     647            1 :             folders = tmp;
     648              :         }
     649            4 :         folders[count++] = name;
     650              :     }
     651              : 
     652            1 :     response_free(&resp);
     653            1 :     *folders_out = folders;
     654            1 :     *count_out   = count;
     655            1 :     if (sep_out) *sep_out = sep;
     656            1 :     return 0;
     657              : }
     658              : 
     659              : /* ── SELECT ──────────────────────────────────────────────────────────── */
     660              : 
     661           49 : int imap_select(ImapClient *c, const char *folder) {
     662           49 :     char *utf7 = imap_utf7_encode(folder);
     663           49 :     const char *name = utf7 ? utf7 : folder;
     664              : 
     665           49 :     char tag[16];
     666              :     int rc;
     667              :     /* Quote the folder name */
     668           49 :     rc = send_cmd(c, tag, "SELECT \"%s\"", name);
     669           49 :     free(utf7);
     670           49 :     if (rc != 0) return -1;
     671              : 
     672           49 :     Response resp = {0};
     673           49 :     rc = read_response(c, tag, &resp);
     674           49 :     response_free(&resp);
     675           49 :     return rc;
     676              : }
     677              : 
     678              : /* ── UID SEARCH ──────────────────────────────────────────────────────── */
     679              : 
     680          180 : int imap_uid_search(ImapClient *c, const char *criteria,
     681              :                     int **uids_out, int *count_out) {
     682          180 :     *uids_out  = NULL;
     683          180 :     *count_out = 0;
     684              : 
     685          180 :     char tag[16];
     686          180 :     if (send_cmd(c, tag, "UID SEARCH %s", criteria) != 0) return -1;
     687              : 
     688          180 :     Response resp = {0};
     689          180 :     if (read_response(c, tag, &resp) != 0) {
     690            0 :         response_free(&resp);
     691            0 :         return -1;
     692              :     }
     693              : 
     694              :     /* Parse "* SEARCH uid uid uid ..." */
     695          180 :     int cap = 32, cnt = 0;
     696          180 :     int *uids = NULL;
     697              : 
     698          360 :     for (int i = 0; i < resp.count; i++) {
     699          180 :         const char *line = resp.untagged[i];
     700          180 :         if (strncasecmp(line, "* SEARCH", 8) != 0) continue;
     701          180 :         const char *p = line + 8;
     702          164 :         for (;;) {
     703          508 :             while (*p == ' ') p++;
     704          344 :             if (!*p) break;
     705          164 :             char *e;
     706          164 :             long uid = strtol(p, &e, 10);
     707          164 :             if (e == p) break;
     708          164 :             if (uid > 0) {
     709          164 :                 if (!uids) {
     710          164 :                     uids = malloc((size_t)cap * sizeof(int));
     711          164 :                     if (!uids) { response_free(&resp); return -1; }
     712              :                 }
     713          164 :                 if (cnt == cap) {
     714            0 :                     cap *= 2;
     715            0 :                     int *tmp = realloc(uids, (size_t)cap * sizeof(int));
     716            0 :                     if (!tmp) { free(uids); response_free(&resp); return -1; }
     717            0 :                     uids = tmp;
     718              :                 }
     719          164 :                 uids[cnt++] = (int)uid;
     720              :             }
     721          164 :             p = e;
     722              :         }
     723              :     }
     724              : 
     725          180 :     response_free(&resp);
     726          180 :     *uids_out  = uids;
     727          180 :     *count_out = cnt;
     728          180 :     return 0;
     729              : }
     730              : 
     731              : /* ── UID FETCH ───────────────────────────────────────────────────────── */
     732              : 
     733           28 : static char *uid_fetch_part(ImapClient *c, int uid, const char *section) {
     734           28 :     char tag[16];
     735           28 :     if (send_cmd(c, tag, "UID FETCH %d (UID %s)", uid, section) != 0)
     736            0 :         return NULL;
     737              : 
     738           28 :     Response resp = {0};
     739           28 :     if (read_response(c, tag, &resp) != 0) {
     740            0 :         response_free(&resp);
     741            0 :         return NULL;
     742              :     }
     743              : 
     744           28 :     char *result = NULL;
     745           28 :     if (resp.literal) {
     746           28 :         result = resp.literal;
     747           28 :         resp.literal = NULL;  /* transfer ownership */
     748              :     }
     749           28 :     response_free(&resp);
     750              : 
     751           28 :     if (!result)
     752            0 :         logger_log(LOG_WARN, "UID FETCH %d %s: no literal in response", uid, section);
     753           28 :     return result;
     754              : }
     755              : 
     756           24 : char *imap_uid_fetch_headers(ImapClient *c, int uid) {
     757           24 :     return uid_fetch_part(c, uid, "BODY.PEEK[HEADER]");
     758              : }
     759              : 
     760            4 : char *imap_uid_fetch_body(ImapClient *c, int uid) {
     761            4 :     return uid_fetch_part(c, uid, "BODY.PEEK[]");
     762              : }
     763              : 
     764              : /* ── UID FETCH FLAGS ─────────────────────────────────────────────────── */
     765              : 
     766              : /**
     767              :  * Parse a `* N FETCH (... FLAGS (\Flag1 $keyword ...) ...)` untagged line
     768              :  * and return a MSG_FLAG_* bitmask.
     769              :  */
     770            0 : static int parse_imap_flags(const char *line) {
     771              :     /* Find FLAGS ( ... ) in the line */
     772            0 :     const char *p = strstr(line, "FLAGS (");
     773            0 :     if (!p) return 0;
     774            0 :     p += 7; /* skip "FLAGS (" */
     775            0 :     int flags = 0;
     776              :     /* Check for known flags */
     777            0 :     if (strstr(p, "\\Seen") == NULL) flags |= MSG_FLAG_UNSEEN;  /* absence of \Seen = unseen */
     778            0 :     if (strstr(p, "\\Flagged") != NULL) flags |= MSG_FLAG_FLAGGED;
     779            0 :     if (strstr(p, "$Done")     != NULL) flags |= MSG_FLAG_DONE;
     780            0 :     return flags;
     781              : }
     782              : 
     783            0 : int imap_uid_fetch_flags(ImapClient *c, int uid) {
     784            0 :     char tag[16];
     785            0 :     if (send_cmd(c, tag, "UID FETCH %d (UID FLAGS)", uid) != 0) return -1;
     786              : 
     787            0 :     Response resp = {0};
     788            0 :     if (read_response(c, tag, &resp) != 0) {
     789            0 :         response_free(&resp);
     790            0 :         return -1;
     791              :     }
     792              : 
     793            0 :     int flags = -1;
     794            0 :     for (int i = 0; i < resp.count; i++) {
     795            0 :         if (strstr(resp.untagged[i], "FETCH") && strstr(resp.untagged[i], "FLAGS")) {
     796            0 :             flags = parse_imap_flags(resp.untagged[i]);
     797            0 :             break;
     798              :         }
     799              :     }
     800            0 :     response_free(&resp);
     801            0 :     return flags < 0 ? 0 : flags;
     802              : }
     803              : 
     804              : /* ── UID STORE (set/clear flag) ──────────────────────────────────────── */
     805              : 
     806            0 : int imap_uid_set_flag(ImapClient *c, int uid, const char *flag_name, int add) {
     807            0 :     char tag[16];
     808            0 :     if (send_cmd(c, tag, "UID STORE %d %sFLAGS (%s)",
     809              :                  uid, add ? "+" : "-", flag_name) != 0)
     810            0 :         return -1;
     811            0 :     Response resp = {0};
     812            0 :     int rc = read_response(c, tag, &resp);
     813            0 :     response_free(&resp);
     814            0 :     return rc;
     815              : }
     816              : 
     817            2 : int imap_append(ImapClient *c, const char *folder,
     818              :                 const char *msg, size_t msg_len) {
     819              :     /* Use LITERAL+ (RFC 7888 non-synchronising literal, "{N+}") which all
     820              :      * modern IMAP servers advertise.  This sends the command line and the
     821              :      * message body in a single write, eliminating the two-phase
     822              :      * synchronising-literal handshake that caused Dovecot to wait 120 s
     823              :      * for data that never arrived due to TLS-layer buffering. */
     824            2 :     c->tag_num++;
     825            2 :     char tag[16];
     826            2 :     snprintf(tag, sizeof(tag), "A%04d", c->tag_num);
     827              : 
     828            2 :     char cmd[1024];
     829            2 :     int cmdlen = snprintf(cmd, sizeof(cmd),
     830              :                           "%s APPEND \"%s\" (\\Seen) {%zu+}\r\n",
     831              :                           tag, folder, msg_len);
     832            2 :     if (cmdlen < 0 || (size_t)cmdlen >= sizeof(cmd)) return -1;
     833              : 
     834              :     /* Allocate a single buffer: command line + message body.
     835              :      * Sending them together guarantees they go in one TLS record. */
     836            2 :     char *buf = malloc((size_t)cmdlen + msg_len);
     837            2 :     if (!buf) return -1;
     838            2 :     memcpy(buf,              cmd, (size_t)cmdlen);
     839            2 :     memcpy(buf + cmdlen,     msg, msg_len);
     840              : 
     841            2 :     logger_log(LOG_DEBUG, "IMAP [OUT] %s APPEND \"%s\" (\\Seen) {%zu+}",
     842              :                tag, folder, msg_len);
     843            2 :     logger_log(LOG_DEBUG, "IMAP APPEND: sending %zu-byte literal", msg_len);
     844              : 
     845            2 :     int wrc = net_write(c, buf, (size_t)cmdlen + msg_len);
     846            2 :     free(buf);
     847            2 :     if (wrc != 0) {
     848            0 :         logger_log(LOG_ERROR, "IMAP APPEND: write failed");
     849            0 :         return -1;
     850              :     }
     851              : 
     852              :     /* Wait for the server's tagged response.  APPEND can take longer than
     853              :      * other commands (server-side AV/spam plugins, quota checks).
     854              :      * Temporarily raise SO_RCVTIMEO to 120 s and restore to 15 s after. */
     855              :     {
     856            2 :         struct timeval long_tv  = { .tv_sec = 120, .tv_usec = 0 };
     857            2 :         struct timeval short_tv = { .tv_sec =  15, .tv_usec = 0 };
     858            2 :         setsockopt(c->fd, SOL_SOCKET, SO_RCVTIMEO, &long_tv,  sizeof(long_tv));
     859            2 :         Response resp = {0};
     860            2 :         int rc = read_response(c, tag, &resp);
     861            2 :         response_free(&resp);
     862            2 :         setsockopt(c->fd, SOL_SOCKET, SO_RCVTIMEO, &short_tv, sizeof(short_tv));
     863            2 :         return rc;
     864              :     }
     865              : }
        

Generated by: LCOV version 2.0-1