LCOV - code coverage report
Current view: top level - libemail/src/infrastructure - imap_client.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 79.0 % 623 492
Test Date: 2026-05-07 15:53:08 Functions: 91.9 % 37 34

            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              :     /* Cached capability flags (IMAP_CAP_*) */
      41              :     int      caps;
      42              :     int      caps_queried;
      43              :     int      qresync_enabled;  /* 1 after ENABLE QRESYNC sent */
      44              : };
      45              : 
      46              : /* ── Low-level I/O ───────────────────────────────────────────────────── */
      47              : 
      48              : /** Read up to `n` bytes from the socket/TLS layer into `buf`.
      49              :  *  Returns bytes read (>0), 0 on EOF, -1 on error. */
      50         6619 : static ssize_t net_read(ImapClient *c, char *buf, size_t n) {
      51         6619 :     if (c->use_tls) {
      52         6619 :         int r = SSL_read(c->ssl, buf, (int)n);
      53         6619 :         if (r > 0) return (ssize_t)r;
      54            0 :         int err = SSL_get_error(c->ssl, r);
      55            0 :         if (err == SSL_ERROR_ZERO_RETURN) return 0;
      56            0 :         logger_log(LOG_WARN, "SSL_read error %d", err);
      57            0 :         return -1;
      58              :     }
      59            0 :     ssize_t r = read(c->fd, buf, n);
      60            0 :     if (r < 0 && (errno == EINTR || errno == EAGAIN)) return 0;
      61            0 :     return r;
      62              : }
      63              : 
      64              : /** Write `n` bytes to the socket/TLS layer.
      65              :  *  Returns 0 on success, -1 on error. */
      66         2623 : static int net_write(ImapClient *c, const char *buf, size_t n) {
      67         2623 :     size_t sent = 0;
      68         5246 :     while (sent < n) {
      69              :         ssize_t r;
      70         2623 :         if (c->use_tls) {
      71         2623 :             int w = SSL_write(c->ssl, buf + sent, (int)(n - sent));
      72         2623 :             if (w <= 0) {
      73            0 :                 logger_log(LOG_WARN, "SSL_write error %d",
      74            0 :                            SSL_get_error(c->ssl, w));
      75            0 :                 return -1;
      76              :             }
      77         2623 :             r = (ssize_t)w;
      78              :         } else {
      79            0 :             r = write(c->fd, buf + sent, n - sent);
      80            0 :             if (r < 0) {
      81            0 :                 if (errno == EINTR) continue;
      82            0 :                 logger_log(LOG_WARN, "write: %s", strerror(errno));
      83            0 :                 return -1;
      84              :             }
      85              :         }
      86         2623 :         sent += (size_t)r;
      87              :     }
      88         2623 :     return 0;
      89              : }
      90              : 
      91              : /* ── Buffered byte reader ────────────────────────────────────────────── */
      92              : 
      93              : /** Ensure at least 1 byte is available in rbuf. Returns 0 on success, -1 on EOF/error. */
      94       168407 : static int rbuf_fill(ImapClient *c) {
      95       168407 :     if (c->rbuf_pos < c->rbuf_len) return 0;
      96              :     /* Compact: move unused data to front */
      97         5965 :     c->rbuf_pos = 0;
      98         5965 :     c->rbuf_len = 0;
      99         5965 :     ssize_t r = net_read(c, c->rbuf, RBUF_SIZE);
     100         5965 :     if (r <= 0) return -1;
     101         5965 :     c->rbuf_len = (size_t)r;
     102         5965 :     return 0;
     103              : }
     104              : 
     105              : /** Read exactly `n` bytes into `out`. Returns 0 on success, -1 on error/EOF. */
     106          654 : static int rbuf_read_exact(ImapClient *c, char *out, size_t n) {
     107          654 :     size_t got = 0;
     108         1308 :     while (got < n) {
     109          654 :         if (c->rbuf_pos >= c->rbuf_len) {
     110          654 :             c->rbuf_pos = 0;
     111          654 :             c->rbuf_len = 0;
     112          654 :             ssize_t r = net_read(c, c->rbuf, RBUF_SIZE);
     113          654 :             if (r <= 0) return -1;
     114          654 :             c->rbuf_len = (size_t)r;
     115              :         }
     116          654 :         size_t avail = c->rbuf_len - c->rbuf_pos;
     117          654 :         size_t take  = avail < (n - got) ? avail : (n - got);
     118          654 :         memcpy(out + got, c->rbuf + c->rbuf_pos, take);
     119          654 :         c->rbuf_pos += take;
     120          654 :         got         += take;
     121              :     }
     122          654 :     return 0;
     123              : }
     124              : 
     125              : /* ── Dynamic line buffer ─────────────────────────────────────────────── */
     126              : 
     127              : typedef struct {
     128              :     char  *data;
     129              :     size_t len;
     130              :     size_t cap;
     131              : } LineBuf;
     132              : 
     133         3514 : static void linebuf_free(LineBuf *lb) { free(lb->data); lb->data = NULL; lb->len = lb->cap = 0; }
     134              : 
     135       154789 : static int linebuf_append(LineBuf *lb, char ch) {
     136       154789 :     if (lb->len + 1 >= lb->cap) {
     137         3544 :         size_t ncap = lb->cap ? lb->cap * 2 : 256;
     138         3544 :         char *tmp = realloc(lb->data, ncap);
     139         3544 :         if (!tmp) return -1;
     140         3544 :         lb->data = tmp;
     141         3544 :         lb->cap  = ncap;
     142              :     }
     143       154789 :     lb->data[lb->len++] = ch;
     144       154789 :     lb->data[lb->len]   = '\0';
     145       154789 :     return 0;
     146              : }
     147              : 
     148              : /**
     149              :  * Read one CRLF-terminated line from the server into `lb`.
     150              :  * The trailing \r\n is stripped. Returns 0 on success, -1 on error/EOF.
     151              :  */
     152         6809 : static int read_line(ImapClient *c, LineBuf *lb) {
     153         6809 :     lb->len = 0;
     154       161598 :     for (;;) {
     155       168407 :         if (rbuf_fill(c) != 0) return -1;
     156       168407 :         char ch = c->rbuf[c->rbuf_pos++];
     157       168407 :         if (ch == '\r') continue;          /* skip CR */
     158       161598 :         if (ch == '\n') {
     159         6809 :             if (lb->data) lb->data[lb->len] = '\0';
     160         6809 :             return 0;
     161              :         }
     162       154789 :         if (linebuf_append(lb, ch) != 0) return -1;
     163              :     }
     164              : }
     165              : 
     166              : /* ── Command dispatch ────────────────────────────────────────────────── */
     167              : 
     168              : /** Send a formatted IMAP command prefixed with the next tag.
     169              :  *  `fmt` should NOT include the tag or trailing CRLF.
     170              :  *  The tag used is written to `tag_out` (capacity >= 16).
     171              :  *  Returns 0 on success, -1 on error. */
     172         2620 : static int send_cmd(ImapClient *c, char tag_out[16], const char *fmt, ...) {
     173         2620 :     c->tag_num++;
     174         2620 :     snprintf(tag_out, 16, "A%04d", c->tag_num);
     175              : 
     176              :     char buf[4096];
     177              :     va_list ap;
     178         2620 :     va_start(ap, fmt);
     179         2620 :     int len = vsnprintf(buf, sizeof(buf) - 3, fmt, ap);
     180         2620 :     va_end(ap);
     181         2620 :     if (len < 0 || (size_t)len >= sizeof(buf) - 3) return -1;
     182              : 
     183              :     /* Append CRLF */
     184         2620 :     buf[len]     = '\r';
     185         2620 :     buf[len + 1] = '\n';
     186         2620 :     buf[len + 2] = '\0';
     187              : 
     188              :     /* Log command — mask password in LOGIN commands */
     189         2620 :     if (strncmp(buf, "LOGIN ", 6) == 0) {
     190              :         /* Extract user (first quoted token) and replace password with xxxxx */
     191          239 :         const char *p = buf + 6;
     192              :         /* skip optional leading space */
     193          239 :         while (*p == ' ') p++;
     194              :         /* find end of username token (quoted or unquoted) */
     195          239 :         const char *user_end = NULL;
     196          239 :         if (*p == '"') {
     197          239 :             user_end = strchr(p + 1, '"');
     198          239 :             if (user_end) user_end++;  /* include closing quote */
     199              :         } else {
     200            0 :             user_end = strchr(p, ' ');
     201              :         }
     202          239 :         if (user_end) {
     203          239 :             logger_log(LOG_DEBUG, "IMAP [OUT] %s LOGIN %.*s xxxxx",
     204          239 :                        tag_out, (int)(user_end - p), p);
     205              :         } else {
     206            0 :             logger_log(LOG_DEBUG, "IMAP [OUT] %s LOGIN <redacted>", tag_out);
     207              :         }
     208              :     } else {
     209         2381 :         logger_log(LOG_DEBUG, "IMAP [OUT] %s %.*s", tag_out, len, buf);
     210              :     }
     211              : 
     212              :     /* Write tag + space + command + CRLF as a single buffer so the entire
     213              :      * command arrives in one TCP segment and a line-reading server can parse
     214              :      * it correctly (two separate writes would be two packets on loopback). */
     215              :     char full[4096 + 32];
     216         2620 :     int flen = snprintf(full, sizeof(full), "%s %s", tag_out, buf);
     217         2620 :     if (flen < 0 || (size_t)flen >= sizeof(full)) return -1;
     218         2620 :     if (net_write(c, full, (size_t)flen) != 0) return -1;
     219         2620 :     return 0;
     220              : }
     221              : 
     222              : /* ── Literal reader ──────────────────────────────────────────────────── */
     223              : 
     224              : /* Minimum literal size (bytes) before the progress callback is invoked */
     225              : #define PROGRESS_THRESHOLD (100 * 1024)
     226              : /* Progress is reported every this many bytes */
     227              : #define PROGRESS_CHUNK     (128 * 1024)
     228              : 
     229              : /**
     230              :  * If line ends with `{N}`, read N literal bytes into a heap buffer.
     231              :  * `*lit_out` is set to the allocated buffer (NUL-terminated) and
     232              :  * `*lit_len` to N.  Returns 0 (no literal), N > 0 (literal read), -1 on error.
     233              :  * Calls c->on_progress (if set) every PROGRESS_CHUNK bytes for large literals.
     234              :  */
     235         3295 : static long read_literal_if_present(ImapClient *c, const char *line,
     236              :                                     char **lit_out, size_t *lit_len) {
     237         3295 :     *lit_out = NULL;
     238         3295 :     *lit_len = 0;
     239              : 
     240              :     /* Find trailing {N} */
     241         3295 :     const char *p = strrchr(line, '{');
     242         3295 :     if (!p) return 0;
     243              :     char *end;
     244          654 :     long sz = strtol(p + 1, &end, 10);
     245          654 :     if (*end != '}' || sz < 0) return 0;
     246              : 
     247              :     /* Allocate output buffer */
     248          654 :     char *buf = malloc((size_t)sz + 1);
     249          654 :     if (!buf) return -1;
     250              : 
     251          654 :     if (sz > 0) {
     252          654 :         size_t total = (size_t)sz;
     253              : 
     254          654 :         if (!c->on_progress || total < PROGRESS_THRESHOLD) {
     255              :             /* Small literal or no callback: read all at once */
     256          654 :             if (rbuf_read_exact(c, buf, total) != 0) { free(buf); return -1; }
     257              :         } else {
     258              :             /* Large literal: read in chunks and report progress */
     259            0 :             size_t got = 0;
     260            0 :             size_t next_report = PROGRESS_CHUNK;
     261            0 :             while (got < total) {
     262            0 :                 size_t want = PROGRESS_CHUNK < (total - got)
     263              :                               ? PROGRESS_CHUNK : (total - got);
     264            0 :                 if (rbuf_read_exact(c, buf + got, want) != 0) {
     265            0 :                     free(buf); return -1;
     266              :                 }
     267            0 :                 got += want;
     268            0 :                 if (got >= next_report || got == total) {
     269            0 :                     c->on_progress(got, total, c->progress_ctx);
     270            0 :                     next_report = got + PROGRESS_CHUNK;
     271              :                 }
     272              :             }
     273              :         }
     274              :     }
     275              : 
     276          654 :     buf[sz] = '\0';
     277          654 :     *lit_out = buf;
     278          654 :     *lit_len = (size_t)sz;
     279          654 :     return sz;
     280              : }
     281              : 
     282          306 : void imap_set_progress(ImapClient *c, ImapProgressFn fn, void *ctx) {
     283          306 :     if (!c) return;
     284          306 :     c->on_progress   = fn;
     285          306 :     c->progress_ctx  = ctx;
     286              : }
     287              : 
     288              : /* ── Response reader ─────────────────────────────────────────────────── */
     289              : 
     290              : typedef struct {
     291              :     char  **untagged;  /* heap-allocated array of untagged response strings */
     292              :     int     count;
     293              :     int     cap;
     294              :     char   *literal;  /* the last literal body (first literal wins) */
     295              :     size_t  lit_len;
     296              :     char   *tagged;   /* the final tagged response line (e.g. "A0001 NO [TRYCREATE] ...") */
     297              : } Response;
     298              : 
     299         2621 : static void response_free(Response *r) {
     300         5916 :     for (int i = 0; i < r->count; i++) free(r->untagged[i]);
     301         2621 :     free(r->untagged);
     302         2621 :     free(r->literal);
     303         2621 :     free(r->tagged);
     304         2621 :     memset(r, 0, sizeof(*r));
     305         2621 : }
     306              : 
     307         3295 : static int response_add(Response *r, const char *line) {
     308         3295 :     if (r->count == r->cap) {
     309         2378 :         int ncap = r->cap ? r->cap * 2 : 16;
     310         2378 :         char **tmp = realloc(r->untagged, (size_t)ncap * sizeof(char *));
     311         2378 :         if (!tmp) return -1;
     312         2378 :         r->untagged = tmp;
     313         2378 :         r->cap      = ncap;
     314              :     }
     315         3295 :     char *copy = strdup(line);
     316         3295 :     if (!copy) return -1;
     317         3295 :     r->untagged[r->count++] = copy;
     318         3295 :     return 0;
     319              : }
     320              : 
     321              : /**
     322              :  * Read server responses until we see our tagged reply.
     323              :  * Collects untagged lines and the first literal body encountered.
     324              :  * Returns 0 on OK, -1 on NO/BAD/error.
     325              :  */
     326         2621 : static int read_response(ImapClient *c, const char *tag, Response *r) {
     327         2621 :     LineBuf lb = {NULL, 0, 0};
     328              : 
     329         3295 :     for (;;) {
     330         5916 :         if (read_line(c, &lb) != 0) {
     331            0 :             linebuf_free(&lb);
     332         2621 :             return -1;
     333              :         }
     334              : 
     335         5916 :         const char *line = lb.data ? lb.data : "";
     336         5916 :         logger_log(LOG_DEBUG, "IMAP [ IN] %s", line);
     337              : 
     338              :         /* Tagged response? */
     339         5916 :         size_t tlen = strlen(tag);
     340         5916 :         if (strncmp(line, tag, tlen) == 0 && line[tlen] == ' ') {
     341         2621 :             const char *status = line + tlen + 1;
     342         2621 :             int ok = (strncasecmp(status, "OK", 2) == 0);
     343         2621 :             if (!ok)
     344            0 :                 logger_log(LOG_WARN, "IMAP %s", line);
     345         2621 :             r->tagged = strdup(line);
     346         2621 :             linebuf_free(&lb);  /* free AFTER all accesses to line/status */
     347         2621 :             return ok ? 0 : -1;
     348              :         }
     349              : 
     350              :         /* Untagged: check for literal */
     351         3295 :         char  *lit     = NULL;
     352         3295 :         size_t lit_len = 0;
     353         3295 :         long   lsz     = read_literal_if_present(c, line, &lit, &lit_len);
     354         3295 :         if (lsz < 0) { linebuf_free(&lb); return -1; }
     355              : 
     356         3295 :         response_add(r, line);
     357              : 
     358         3295 :         if (lit) {
     359              :             /* We read the literal; now read the closing line: ")\r\n" or similar */
     360          654 :             if (!r->literal) {
     361          654 :                 r->literal = lit;
     362          654 :                 r->lit_len = lit_len;
     363              :             } else {
     364            0 :                 free(lit);  /* second literal — discard (shouldn't happen) */
     365              :             }
     366              :             /* Read the remainder line after the literal (e.g. ")" or empty) */
     367          654 :             LineBuf trail = {NULL, 0, 0};
     368          654 :             if (read_line(c, &trail) == 0 && trail.data && trail.data[0])
     369          654 :                 logger_log(LOG_DEBUG, "IMAP [ IN] %s", trail.data);
     370          654 :             linebuf_free(&trail);
     371              :         }
     372              :     }
     373              : }
     374              : 
     375              : /* ── URL parser ──────────────────────────────────────────────────────── */
     376              : 
     377              : /**
     378              :  * Parse "imaps://host:port" or "imap://host" into components.
     379              :  * Returns 0 on success.
     380              :  */
     381          240 : static int parse_url(const char *url, char *host, size_t hsize,
     382              :                      char *port, size_t psize, int *use_tls) {
     383          240 :     *use_tls = 0;
     384          240 :     const char *p = url;
     385              : 
     386          240 :     if (strncasecmp(p, "imaps://", 8) == 0) { *use_tls = 1; p += 8; }
     387            0 :     else if (strncasecmp(p, "imap://", 7) == 0) { p += 7; }
     388              :     else {
     389              :         /* Treat as bare hostname, default IMAPS */
     390            0 :         *use_tls = 1;
     391            0 :         snprintf(host, hsize, "%s", url);
     392            0 :         snprintf(port, psize, "993");
     393            0 :         return 0;
     394              :     }
     395              : 
     396              :     /* host[:port] */
     397          240 :     const char *colon = strchr(p, ':');
     398          240 :     if (colon) {
     399          240 :         size_t hlen = (size_t)(colon - p);
     400          240 :         if (hlen >= hsize) return -1;
     401          240 :         memcpy(host, p, hlen);
     402          240 :         host[hlen] = '\0';
     403          240 :         snprintf(port, psize, "%s", colon + 1);
     404              :     } else {
     405            0 :         snprintf(host, hsize, "%s", p);
     406            0 :         snprintf(port, psize, "%s", *use_tls ? "993" : "143");
     407              :     }
     408          240 :     return 0;
     409              : }
     410              : 
     411              : /* ── Connect ─────────────────────────────────────────────────────────── */
     412              : 
     413          240 : ImapClient *imap_connect(const char *host_url, const char *user,
     414              :                          const char *pass, int verify_tls) {
     415              :     char host[256], port[16];
     416          240 :     int  use_tls = 1;
     417          240 :     if (parse_url(host_url, host, sizeof(host), port, sizeof(port), &use_tls) != 0) {
     418            0 :         logger_log(LOG_ERROR, "imap_connect: bad URL: %s", host_url);
     419            0 :         return NULL;
     420              :     }
     421              : 
     422              :     /* Hard enforcement: never connect without TLS unless verify_tls == 0
     423              :      * (SSL_NO_VERIFY=1 in config — test/dev environments only). */
     424          240 :     if (!use_tls && verify_tls) {
     425            0 :         logger_log(LOG_ERROR,
     426              :                    "imap_connect: refused to connect to %s without TLS — "
     427              :                    "use imaps:// to protect credentials", host_url);
     428            0 :         fprintf(stderr,
     429              :                 "Error: Refused to connect to %s without TLS.\n"
     430              :                 "Use imaps:// in EMAIL_HOST to protect your password.\n"
     431              :                 "For test environments only: add SSL_NO_VERIFY=1 to config.\n",
     432              :                 host_url);
     433            0 :         return NULL;
     434              :     }
     435              : 
     436              :     /* TCP connect */
     437          240 :     struct addrinfo hints = {0};
     438          240 :     hints.ai_family   = AF_UNSPEC;
     439          240 :     hints.ai_socktype = SOCK_STREAM;
     440          240 :     struct addrinfo *ai = NULL;
     441          240 :     int rc = getaddrinfo(host, port, &hints, &ai);
     442          240 :     if (rc != 0) {
     443            0 :         logger_log(LOG_ERROR, "getaddrinfo(%s:%s): %s", host, port, gai_strerror(rc));
     444            0 :         return NULL;
     445              :     }
     446              : 
     447          240 :     int fd = -1;
     448          481 :     for (struct addrinfo *r = ai; r; r = r->ai_next) {
     449          480 :         fd = socket(r->ai_family, r->ai_socktype, r->ai_protocol);
     450          480 :         if (fd < 0) continue;
     451          480 :         if (connect(fd, r->ai_addr, r->ai_addrlen) == 0) break;
     452          241 :         close(fd);
     453          241 :         fd = -1;
     454              :     }
     455          240 :     freeaddrinfo(ai);
     456              : 
     457          240 :     if (fd < 0) {
     458            1 :         logger_log(LOG_ERROR, "connect to %s:%s failed: %s", host, port, strerror(errno));
     459            1 :         return NULL;
     460              :     }
     461              : 
     462              :     /* Apply a 15-second read/write timeout so blocking ops don't hang forever */
     463              :     {
     464          239 :         struct timeval tv = { .tv_sec = 15, .tv_usec = 0 };
     465          239 :         setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
     466          239 :         setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
     467              :     }
     468              : 
     469          239 :     ImapClient *c = calloc(1, sizeof(ImapClient));
     470          239 :     if (!c) { close(fd); return NULL; }
     471          239 :     c->fd = fd;
     472          239 :     c->use_tls = use_tls;
     473              : 
     474              :     /* TLS handshake */
     475          239 :     if (use_tls) {
     476              :         /* Init OpenSSL (idempotent in OpenSSL 1.1+) */
     477          239 :         SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
     478          239 :         if (!ctx) {
     479            0 :             logger_log(LOG_ERROR, "SSL_CTX_new failed");
     480            0 :             free(c); close(fd);
     481            0 :             return NULL;
     482              :         }
     483          239 :         SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
     484          239 :         SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
     485          239 :         if (!verify_tls) {
     486          239 :             SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
     487              :         } else {
     488            0 :             SSL_CTX_set_default_verify_paths(ctx);
     489            0 :             SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
     490              :         }
     491          239 :         SSL *ssl = SSL_new(ctx);
     492          239 :         if (!ssl) {
     493            0 :             logger_log(LOG_ERROR, "SSL_new failed");
     494            0 :             SSL_CTX_free(ctx); free(c); close(fd);
     495            0 :             return NULL;
     496              :         }
     497          239 :         SSL_set_fd(ssl, fd);
     498          239 :         SSL_set_tlsext_host_name(ssl, host);  /* SNI */
     499          239 :         if (SSL_connect(ssl) != 1) {
     500            0 :             logger_log(LOG_ERROR, "SSL handshake failed with %s", host);
     501            0 :             SSL_free(ssl); SSL_CTX_free(ctx); free(c); close(fd);
     502            0 :             return NULL;
     503              :         }
     504          239 :         c->ctx = ctx;
     505          239 :         c->ssl = ssl;
     506          239 :         logger_log(LOG_DEBUG, "IMAP TLS handshake OK with %s (TLS/%s)",
     507              :                    host, SSL_get_version(ssl));
     508              :     } else {
     509            0 :         logger_log(LOG_WARN,
     510              :                    "IMAP connecting to %s:%s WITHOUT TLS — "
     511              :                    "credentials will be sent in plaintext!", host, port);
     512              :     }
     513              : 
     514              :     /* Read server greeting */
     515          239 :     LineBuf lb = {NULL, 0, 0};
     516          239 :     if (read_line(c, &lb) != 0) {
     517            0 :         logger_log(LOG_ERROR, "No greeting from %s", host);
     518            0 :         goto fail;
     519              :     }
     520          239 :     logger_log(LOG_DEBUG, "IMAP [ IN] %s", lb.data ? lb.data : "");
     521          239 :     linebuf_free(&lb);
     522              : 
     523              :     /* LOGIN */
     524              :     char tag[16];
     525              :     /* Send LOGIN with literal username/password to handle special chars correctly.
     526              :      * Simple approach: just quote them (assume no embedded DQUOTE or backslash). */
     527          239 :     if (send_cmd(c, tag, "LOGIN \"%s\" \"%s\"", user, pass) != 0)
     528            0 :         goto fail;
     529              : 
     530          239 :     Response resp = {0};
     531          239 :     rc = read_response(c, tag, &resp);
     532          239 :     response_free(&resp);
     533          239 :     if (rc != 0) {
     534            0 :         logger_log(LOG_ERROR, "LOGIN failed for user %s on %s", user, host);
     535            0 :         goto fail;
     536              :     }
     537              : 
     538          239 :     logger_log(LOG_DEBUG, "IMAP connected and authenticated: %s@%s", user, host);
     539          239 :     return c;
     540              : 
     541            0 : fail:
     542            0 :     if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); }
     543            0 :     if (c->ctx) SSL_CTX_free(c->ctx);
     544            0 :     close(c->fd);
     545            0 :     free(c);
     546            0 :     return NULL;
     547              : }
     548              : 
     549              : /* ── Disconnect ──────────────────────────────────────────────────────── */
     550              : 
     551          187 : void imap_disconnect(ImapClient *c) {
     552          187 :     if (!c) return;
     553              :     /* Send LOGOUT (ignore errors — we're closing anyway) */
     554              :     char tag[16];
     555          187 :     send_cmd(c, tag, "LOGOUT");
     556          187 :     Response r = {0};
     557          187 :     read_response(c, tag, &r);
     558          187 :     response_free(&r);
     559              : 
     560          187 :     if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); }
     561          187 :     if (c->ctx) SSL_CTX_free(c->ctx);
     562          187 :     if (c->fd >= 0) close(c->fd);
     563          187 :     free(c);
     564              : }
     565              : 
     566              : /* ── LIST ────────────────────────────────────────────────────────────── */
     567              : 
     568              : /**
     569              :  * Parse one `* LIST (\flags) "sep" "name"` or `* LIST (\flags) "sep" name` line.
     570              :  * Returns heap-allocated folder name (Modified UTF-7, not yet decoded), or NULL.
     571              :  * Sets *sep_out to the separator character.
     572              :  */
     573          176 : static char *parse_list_line(const char *line, char *sep_out) {
     574              :     /* Skip "* LIST " */
     575          176 :     if (strncasecmp(line, "* LIST ", 7) != 0) return NULL;
     576          176 :     const char *p = line + 7;
     577              : 
     578              :     /* Skip flags: (...) */
     579          176 :     if (*p == '(') {
     580          176 :         p = strchr(p, ')');
     581          176 :         if (!p) return NULL;
     582          176 :         p++;
     583              :     }
     584          352 :     while (*p == ' ') p++;
     585              : 
     586              :     /* Separator: "." or "/" or NIL */
     587          176 :     if (*p == '"') {
     588          176 :         p++;
     589          176 :         if (*p && *(p + 1) == '"') {
     590          176 :             *sep_out = *p;
     591          176 :             p += 2;
     592            0 :         } else if (*p == '"') {
     593              :             /* empty separator */
     594            0 :             p++;
     595              :         }
     596            0 :     } else if (strncasecmp(p, "NIL", 3) == 0) {
     597            0 :         *sep_out = '.';
     598            0 :         p += 3;
     599              :     }
     600          352 :     while (*p == ' ') p++;
     601              : 
     602              :     /* Folder name: quoted or unquoted */
     603          176 :     if (*p == '"') {
     604          176 :         p++;
     605          176 :         const char *end = strchr(p, '"');
     606          176 :         if (!end) return NULL;
     607          176 :         size_t len = (size_t)(end - p);
     608          176 :         char *name = malloc(len + 1);
     609          176 :         if (!name) return NULL;
     610          176 :         memcpy(name, p, len);
     611          176 :         name[len] = '\0';
     612          176 :         return name;
     613              :     } else {
     614              :         /* Unquoted: until end of line */
     615            0 :         size_t len = strlen(p);
     616            0 :         while (len > 0 && (p[len - 1] == ' ' || p[len - 1] == '\r')) len--;
     617            0 :         char *name = malloc(len + 1);
     618            0 :         if (!name) return NULL;
     619            0 :         memcpy(name, p, len);
     620            0 :         name[len] = '\0';
     621            0 :         return name;
     622              :     }
     623              : }
     624              : 
     625           22 : int imap_list(ImapClient *c, char ***folders_out, int *count_out, char *sep_out) {
     626           22 :     *folders_out = NULL;
     627           22 :     *count_out   = 0;
     628           22 :     if (sep_out) *sep_out = '.';
     629              : 
     630              :     char tag[16];
     631           22 :     if (send_cmd(c, tag, "LIST \"\" \"*\"") != 0) return -1;
     632              : 
     633           22 :     Response resp = {0};
     634           22 :     if (read_response(c, tag, &resp) != 0) {
     635            0 :         response_free(&resp);
     636            0 :         return -1;
     637              :     }
     638              : 
     639           22 :     int count = 0, cap = 0;
     640           22 :     char **folders = NULL;
     641           22 :     char  sep = '.';
     642              : 
     643          198 :     for (int i = 0; i < resp.count; i++) {
     644          176 :         char got_sep = '.';
     645          176 :         char *raw = parse_list_line(resp.untagged[i], &got_sep);
     646          176 :         if (!raw) continue;
     647          176 :         sep = got_sep;
     648          176 :         char *name = imap_utf7_decode(raw);
     649          176 :         free(raw);
     650          176 :         if (!name) continue;
     651              : 
     652          176 :         if (count == cap) {
     653           22 :             cap = cap ? cap * 2 : 16;
     654           22 :             char **tmp = realloc(folders, (size_t)cap * sizeof(char *));
     655           22 :             if (!tmp) { free(name); break; }
     656           22 :             folders = tmp;
     657              :         }
     658          176 :         folders[count++] = name;
     659              :     }
     660              : 
     661           22 :     response_free(&resp);
     662           22 :     *folders_out = folders;
     663           22 :     *count_out   = count;
     664           22 :     if (sep_out) *sep_out = sep;
     665           22 :     return 0;
     666              : }
     667              : 
     668              : /* ── CREATE / DELETE folder ──────────────────────────────────────────── */
     669              : 
     670            2 : int imap_create_folder(ImapClient *c, const char *name) {
     671            2 :     char *utf7 = imap_utf7_encode(name);
     672            2 :     const char *utf7_name = utf7 ? utf7 : name;
     673              : 
     674              :     char tag[16];
     675            2 :     int rc = send_cmd(c, tag, "CREATE \"%s\"", utf7_name);
     676            2 :     free(utf7);
     677            2 :     if (rc != 0) return -1;
     678              : 
     679            2 :     Response resp = {0};
     680            2 :     rc = read_response(c, tag, &resp);
     681              :     /* Treat [ALREADYEXISTS] as success — the folder is there, which is all we need. */
     682            2 :     if (rc != 0 && resp.tagged &&
     683            0 :         strcasestr(resp.tagged, "[ALREADYEXISTS]") != NULL)
     684            0 :         rc = 0;
     685            2 :     response_free(&resp);
     686            2 :     return rc;
     687              : }
     688              : 
     689            1 : int imap_delete_folder(ImapClient *c, const char *name) {
     690            1 :     char *utf7 = imap_utf7_encode(name);
     691            1 :     const char *utf7_name = utf7 ? utf7 : name;
     692              : 
     693              :     char tag[16];
     694            1 :     int rc = send_cmd(c, tag, "DELETE \"%s\"", utf7_name);
     695            1 :     free(utf7);
     696            1 :     if (rc != 0) return -1;
     697              : 
     698            1 :     Response resp = {0};
     699            1 :     rc = read_response(c, tag, &resp);
     700            1 :     response_free(&resp);
     701            1 :     return rc;
     702              : }
     703              : 
     704              : /* ── SELECT ──────────────────────────────────────────────────────────── */
     705              : 
     706          281 : int imap_select(ImapClient *c, const char *folder) {
     707          281 :     char *utf7 = imap_utf7_encode(folder);
     708          281 :     const char *name = utf7 ? utf7 : folder;
     709              : 
     710              :     char tag[16];
     711              :     int rc;
     712              :     /* Quote the folder name */
     713          281 :     rc = send_cmd(c, tag, "SELECT \"%s\"", name);
     714          281 :     free(utf7);
     715          281 :     if (rc != 0) return -1;
     716              : 
     717          281 :     Response resp = {0};
     718          281 :     rc = read_response(c, tag, &resp);
     719          281 :     response_free(&resp);
     720          281 :     return rc;
     721              : }
     722              : 
     723              : /* ── UID SEARCH ──────────────────────────────────────────────────────── */
     724              : 
     725         1123 : int imap_uid_search(ImapClient *c, const char *criteria,
     726              :                     char (**uids_out)[17], int *count_out) {
     727         1123 :     *uids_out  = NULL;
     728         1123 :     *count_out = 0;
     729              : 
     730              :     char tag[16];
     731         1123 :     if (send_cmd(c, tag, "UID SEARCH %s", criteria) != 0) return -1;
     732              : 
     733         1123 :     Response resp = {0};
     734         1123 :     if (read_response(c, tag, &resp) != 0) {
     735            0 :         response_free(&resp);
     736            0 :         return -1;
     737              :     }
     738              : 
     739              :     /* Parse "* SEARCH uid uid uid ..." */
     740         1123 :     int cap = 32, cnt = 0;
     741         1123 :     char (*uids)[17] = NULL;
     742              : 
     743         2246 :     for (int i = 0; i < resp.count; i++) {
     744         1123 :         const char *line = resp.untagged[i];
     745         1123 :         if (strncasecmp(line, "* SEARCH", 8) != 0) continue;
     746         1123 :         const char *p = line + 8;
     747         5000 :         for (;;) {
     748        11123 :             while (*p == ' ') p++;
     749         6123 :             if (!*p) break;
     750              :             char *e;
     751         5000 :             unsigned long uid = strtoul(p, &e, 10);
     752         5000 :             if (e == p) break;
     753         5000 :             if (uid > 0 && uid <= 4294967295UL) {
     754         5000 :                 if (!uids) {
     755         1083 :                     uids = malloc((size_t)cap * sizeof(char[17]));
     756         1083 :                     if (!uids) { response_free(&resp); return -1; }
     757              :                 }
     758         5000 :                 if (cnt == cap) {
     759           45 :                     cap *= 2;
     760           45 :                     char (*tmp)[17] = realloc(uids, (size_t)cap * sizeof(char[17]));
     761           45 :                     if (!tmp) { free(uids); response_free(&resp); return -1; }
     762           45 :                     uids = tmp;
     763              :                 }
     764         5000 :                 snprintf(uids[cnt], 17, "%016lu", uid);
     765         5000 :                 cnt++;
     766              :             }
     767         5000 :             p = e;
     768              :         }
     769              :     }
     770              : 
     771         1123 :     response_free(&resp);
     772         1123 :     *uids_out  = uids;
     773         1123 :     *count_out = cnt;
     774         1123 :     return 0;
     775              : }
     776              : 
     777              : /* ── UID FETCH ───────────────────────────────────────────────────────── */
     778              : 
     779          654 : static char *uid_fetch_part(ImapClient *c, const char *uid, const char *section) {
     780              :     char tag[16];
     781          654 :     if (send_cmd(c, tag, "UID FETCH %s (UID %s)", uid, section) != 0)
     782            0 :         return NULL;
     783              : 
     784          654 :     Response resp = {0};
     785          654 :     if (read_response(c, tag, &resp) != 0) {
     786            0 :         response_free(&resp);
     787            0 :         return NULL;
     788              :     }
     789              : 
     790          654 :     char *result = NULL;
     791          654 :     if (resp.literal) {
     792          654 :         result = resp.literal;
     793          654 :         resp.literal = NULL;  /* transfer ownership */
     794              :     }
     795          654 :     response_free(&resp);
     796              : 
     797          654 :     if (!result)
     798            0 :         logger_log(LOG_WARN, "UID FETCH %s %s: no literal in response", uid, section);
     799          654 :     return result;
     800              : }
     801              : 
     802          486 : char *imap_uid_fetch_headers(ImapClient *c, const char *uid) {
     803          486 :     return uid_fetch_part(c, uid, "BODY.PEEK[HEADER]");
     804              : }
     805              : 
     806          168 : char *imap_uid_fetch_body(ImapClient *c, const char *uid) {
     807          168 :     return uid_fetch_part(c, uid, "BODY.PEEK[]");
     808              : }
     809              : 
     810              : /* ── UID FETCH FLAGS ─────────────────────────────────────────────────── */
     811              : 
     812              : /**
     813              :  * Parse a `* N FETCH (... FLAGS (\Flag1 $keyword ...) ...)` untagged line
     814              :  * and return a MSG_FLAG_* bitmask.
     815              :  */
     816            7 : static int parse_imap_flags(const char *line) {
     817              :     /* Find FLAGS ( ... ) in the line */
     818            7 :     const char *p = strstr(line, "FLAGS (");
     819            7 :     if (!p) return 0;
     820            7 :     p += 7; /* skip "FLAGS (" */
     821            7 :     int flags = 0;
     822            7 :     if (strstr(p, "\\Seen")     == NULL) flags |= MSG_FLAG_UNSEEN;
     823            7 :     if (strstr(p, "\\Flagged")  != NULL) flags |= MSG_FLAG_FLAGGED;
     824            7 :     if (strstr(p, "$Done")      != NULL) flags |= MSG_FLAG_DONE;
     825            7 :     if (strstr(p, "\\Answered") != NULL) flags |= MSG_FLAG_ANSWERED;
     826            7 :     if (strstr(p, "$Forwarded") != NULL) flags |= MSG_FLAG_FORWARDED;
     827            7 :     if (strstr(p, "$Phishing")  != NULL) flags |= MSG_FLAG_PHISHING;
     828              :     /* $Junk and $NotJunk: $NotJunk wins if both somehow present */
     829            7 :     if (strstr(p, "$Junk")      != NULL) flags |= MSG_FLAG_JUNK;
     830            7 :     if (strstr(p, "$NotJunk")   != NULL) flags &= ~MSG_FLAG_JUNK;
     831            7 :     return flags;
     832              : }
     833              : 
     834            0 : int imap_uid_fetch_flags(ImapClient *c, const char *uid) {
     835              :     char tag[16];
     836            0 :     if (send_cmd(c, tag, "UID FETCH %s (UID FLAGS)", uid) != 0) return -1;
     837              : 
     838            0 :     Response resp = {0};
     839            0 :     if (read_response(c, tag, &resp) != 0) {
     840            0 :         response_free(&resp);
     841            0 :         return -1;
     842              :     }
     843              : 
     844            0 :     int flags = -1;
     845            0 :     for (int i = 0; i < resp.count; i++) {
     846            0 :         if (strstr(resp.untagged[i], "FETCH") && strstr(resp.untagged[i], "FLAGS")) {
     847            0 :             flags = parse_imap_flags(resp.untagged[i]);
     848            0 :             break;
     849              :         }
     850              :     }
     851            0 :     response_free(&resp);
     852            0 :     return flags < 0 ? 0 : flags;
     853              : }
     854              : 
     855              : /* ── UID STORE (set/clear flag) ──────────────────────────────────────── */
     856              : 
     857           19 : int imap_uid_set_flag(ImapClient *c, const char *uid, const char *flag_name, int add) {
     858              :     char tag[16];
     859           19 :     if (send_cmd(c, tag, "UID STORE %s %sFLAGS (%s)",
     860              :                  uid, add ? "+" : "-", flag_name) != 0)
     861            0 :         return -1;
     862           19 :     Response resp = {0};
     863           19 :     int rc = read_response(c, tag, &resp);
     864           19 :     response_free(&resp);
     865           19 :     return rc;
     866              : }
     867              : 
     868            0 : int imap_uid_copy(ImapClient *c, const char *uid, const char *target_folder) {
     869              :     /* Ensure target folder exists first */
     870            0 :     imap_create_folder(c, target_folder);
     871              :     char tag[16];
     872            0 :     if (send_cmd(c, tag, "UID COPY %s \"%s\"", uid, target_folder) != 0)
     873            0 :         return -1;
     874            0 :     Response resp = {0};
     875            0 :     int rc = read_response(c, tag, &resp);
     876            0 :     response_free(&resp);
     877            0 :     return rc;
     878              : }
     879              : 
     880            0 : int imap_uid_move(ImapClient *c, const char *uid, const char *target_folder) {
     881            0 :     if (imap_uid_copy(c, uid, target_folder) != 0) return -1;
     882            0 :     if (imap_uid_set_flag(c, uid, "\\Deleted", 1) != 0) return -1;
     883              :     /* EXPUNGE */
     884              :     char tag[16];
     885            0 :     if (send_cmd(c, tag, "EXPUNGE") != 0) return -1;
     886            0 :     Response resp = {0};
     887            0 :     int rc = read_response(c, tag, &resp);
     888            0 :     response_free(&resp);
     889            0 :     return rc;
     890              : }
     891              : 
     892            1 : int imap_append(ImapClient *c, const char *folder,
     893              :                 const char *msg, size_t msg_len) {
     894              :     /* Strategy: ensure the target folder exists BEFORE sending the literal,
     895              :      * then use a non-synchronising literal "{N+}" (RFC 7888 LITERAL+).
     896              :      *
     897              :      * Why pre-create instead of relying on TRYCREATE retry:
     898              :      *   Dovecot returns NO [TRYCREATE] without consuming the literal bytes
     899              :      *   when the folder is absent.  Those unread bytes corrupt subsequent
     900              :      *   commands on the same connection.  Pre-creating avoids the race.
     901              :      *   imap_create_folder() ignores [ALREADYEXISTS], so this is idempotent.
     902              :      *
     903              :      * Why LITERAL+ instead of synchronising "{N}":
     904              :      *   With "{N}" we had to send the command, wait for "+ OK", and then
     905              :      *   SSL_write the body.  The 15-second SO_RCVTIMEO set for the "+ OK"
     906              :      *   read could fire during SSL_write (OpenSSL reads TLS records internally
     907              :      *   during writes), causing Dovecot to time out and return BAD.
     908              :      *   With "{N+}" the command line and body are sent before any read, so
     909              :      *   SO_RCVTIMEO is only active during the final tagged-response read.
     910              :      *
     911              :      * For servers that do not support LITERAL+ but honour synchronising
     912              :      * semantics: they will send "+ OK" which read_response() harmlessly
     913              :      * treats as an untagged line before reading the true tagged response. */
     914              : 
     915              :     /* Step 1: ensure the target folder exists (idempotent). */
     916            1 :     if (imap_create_folder(c, folder) != 0)
     917            0 :         logger_log(LOG_WARN, "IMAP APPEND: pre-create of '%s' failed, trying anyway",
     918              :                    folder);
     919              : 
     920              :     /* Step 2: send command + literal with LITERAL+. */
     921            1 :     c->tag_num++;
     922              :     char tag[16];
     923            1 :     snprintf(tag, sizeof(tag), "A%04d", c->tag_num);
     924              : 
     925              :     char cmd[1024];
     926            1 :     int cmdlen = snprintf(cmd, sizeof(cmd),
     927              :                           "%s APPEND \"%s\" (\\Seen) {%zu+}\r\n",
     928              :                           tag, folder, msg_len);
     929            1 :     if (cmdlen < 0 || (size_t)cmdlen >= sizeof(cmd)) return -1;
     930              : 
     931            1 :     logger_log(LOG_DEBUG, "IMAP [OUT] %s APPEND \"%s\" (\\Seen) {%zu+}",
     932              :                tag, folder, msg_len);
     933              : 
     934              :     /* Generous 30-second receive timeout for the response. */
     935              :     {
     936            1 :         struct timeval tv = { .tv_sec = 30, .tv_usec = 0 };
     937            1 :         setsockopt(c->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
     938              :     }
     939              : 
     940            1 :     int rc = -1;
     941            2 :     if (net_write(c, cmd, (size_t)cmdlen) != 0 ||
     942            2 :         net_write(c, msg, msg_len) != 0 ||
     943            1 :         net_write(c, "\r\n", 2) != 0) {
     944            0 :         logger_log(LOG_ERROR, "IMAP APPEND: write failed");
     945              :     } else {
     946            1 :         logger_log(LOG_DEBUG, "IMAP APPEND: sent %zu-byte literal", msg_len);
     947            1 :         Response resp = {0};
     948            1 :         rc = read_response(c, tag, &resp);
     949            1 :         response_free(&resp);
     950              :     }
     951              : 
     952              :     /* Restore normal 15-second receive timeout. */
     953              :     {
     954            1 :         struct timeval tv = { .tv_sec = 15, .tv_usec = 0 };
     955            1 :         setsockopt(c->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
     956              :     }
     957            1 :     return rc;
     958              : }
     959              : 
     960              : /* ── CONDSTORE / QRESYNC (RFC 4551 / RFC 5162) ──────────────────────────── */
     961              : 
     962          152 : int imap_get_caps(ImapClient *c) {
     963          152 :     if (c->caps_queried) return c->caps;
     964           19 :     c->caps_queried = 1;
     965              : 
     966              :     char tag[16];
     967           19 :     if (send_cmd(c, tag, "CAPABILITY") != 0) return 0;
     968           19 :     Response resp = {0};
     969           19 :     if (read_response(c, tag, &resp) != 0) { response_free(&resp); return 0; }
     970           38 :     for (int i = 0; i < resp.count; i++) {
     971           19 :         const char *line = resp.untagged[i];
     972           19 :         if (strstr(line, "CONDSTORE")) c->caps |= IMAP_CAP_CONDSTORE;
     973           19 :         if (strstr(line, "QRESYNC"))   c->caps |= IMAP_CAP_QRESYNC;
     974              :     }
     975           19 :     response_free(&resp);
     976           19 :     return c->caps;
     977              : }
     978              : 
     979              : /** Parse UIDVALIDITY and HIGHESTMODSEQ from untagged + tagged SELECT responses. */
     980           64 : static void parse_select_result(const Response *resp, ImapSelectResult *res) {
     981          328 :     for (int i = 0; i < resp->count; i++) {
     982          264 :         const char *line = resp->untagged[i];
     983              :         const char *p;
     984          264 :         p = strstr(line, "[HIGHESTMODSEQ ");
     985          264 :         if (p) res->highestmodseq = (uint64_t)strtoull(p + 15, NULL, 10);
     986          264 :         p = strstr(line, "[UIDVALIDITY ");
     987          264 :         if (p) res->uidvalidity   = (uint32_t)strtoul (p + 13, NULL, 10);
     988              :     }
     989              :     /* Some servers put HIGHESTMODSEQ in the tagged OK response */
     990           64 :     if (resp->tagged) {
     991           64 :         const char *p = strstr(resp->tagged, "[HIGHESTMODSEQ ");
     992           64 :         if (p) res->highestmodseq = (uint64_t)strtoull(p + 15, NULL, 10);
     993              :     }
     994           64 : }
     995              : 
     996           48 : int imap_select_condstore(ImapClient *c, const char *folder, ImapSelectResult *res) {
     997           48 :     memset(res, 0, sizeof(*res));
     998              : 
     999           48 :     char *utf7 = imap_utf7_encode(folder);
    1000           48 :     const char *name = utf7 ? utf7 : folder;
    1001              :     char tag[16];
    1002           48 :     int rc = send_cmd(c, tag, "SELECT \"%s\" (CONDSTORE)", name);
    1003           48 :     free(utf7);
    1004           48 :     if (rc != 0) return -1;
    1005              : 
    1006           48 :     Response resp = {0};
    1007           48 :     rc = read_response(c, tag, &resp);
    1008           48 :     if (rc == 0)
    1009           48 :         parse_select_result(&resp, res);
    1010           48 :     response_free(&resp);
    1011           48 :     return rc;
    1012              : }
    1013              : 
    1014           16 : int imap_select_qresync(ImapClient *c, const char *folder,
    1015              :                          uint32_t known_uidval, uint64_t known_modseq,
    1016              :                          ImapSelectResult *res) {
    1017           16 :     memset(res, 0, sizeof(*res));
    1018              : 
    1019              :     /* ENABLE QRESYNC once per session (RFC 5161/5162) */
    1020           16 :     if (!c->qresync_enabled) {
    1021              :         char entag[16];
    1022            2 :         if (send_cmd(c, entag, "ENABLE QRESYNC") == 0) {
    1023            2 :             Response enr = {0};
    1024            2 :             read_response(c, entag, &enr);  /* ignore errors */
    1025            2 :             response_free(&enr);
    1026              :         }
    1027            2 :         c->qresync_enabled = 1;
    1028              :     }
    1029              : 
    1030           16 :     char *utf7 = imap_utf7_encode(folder);
    1031           16 :     const char *name = utf7 ? utf7 : folder;
    1032              :     char tag[16];
    1033           16 :     int rc = send_cmd(c, tag,
    1034              :                       "SELECT \"%s\" (QRESYNC (%u %llu))",
    1035              :                       name, known_uidval, (unsigned long long)known_modseq);
    1036           16 :     free(utf7);
    1037           16 :     if (rc != 0) return -1;
    1038              : 
    1039           16 :     Response resp = {0};
    1040           16 :     rc = read_response(c, tag, &resp);
    1041           16 :     if (rc == 0) {
    1042           16 :         parse_select_result(&resp, res);
    1043              : 
    1044              :         /* Parse VANISHED (EARLIER) uid-set from untagged responses */
    1045           88 :         for (int i = 0; i < resp.count; i++) {
    1046           72 :             const char *line = resp.untagged[i];
    1047           72 :             if (strncmp(line, "* VANISHED", 10) != 0) continue;
    1048              :             /* Skip past "(EARLIER)" if present */
    1049            8 :             const char *vs = strstr(line, ") ");
    1050            8 :             if (vs) vs += 2;
    1051              :             else {
    1052            0 :                 vs = strstr(line, "VANISHED ");
    1053            0 :                 if (vs) vs += 9;
    1054              :             }
    1055            8 :             if (vs && *vs)
    1056            8 :                 imap_uid_set_expand(vs, &res->vanished_uids, &res->vanished_count);
    1057              :         }
    1058              :     }
    1059           16 :     response_free(&resp);
    1060           16 :     return rc;
    1061              : }
    1062              : 
    1063              : /** Extract UID value from a FETCH response parenthesised data item. */
    1064            7 : static unsigned long parse_fetch_uid(const char *line) {
    1065              :     /* Look for "(UID nnn" or " UID nnn" inside the FETCH data */
    1066            7 :     const char *p = strstr(line, "FETCH (");
    1067            7 :     if (!p) return 0;
    1068            7 :     p += 7;
    1069            7 :     const char *uid_p = strstr(p, "UID ");
    1070            7 :     if (!uid_p) return 0;
    1071              :     char *end;
    1072            7 :     unsigned long uid = strtoul(uid_p + 4, &end, 10);
    1073            7 :     return (end == uid_p + 4) ? 0 : uid;
    1074              : }
    1075              : 
    1076            7 : int imap_uid_fetch_flags_changedsince(ImapClient *c, uint64_t modseq,
    1077              :                                        ImapFlagUpdate **out, int *count_out) {
    1078            7 :     *out       = NULL;
    1079            7 :     *count_out = 0;
    1080              : 
    1081              :     char tag[16];
    1082            7 :     if (send_cmd(c, tag, "UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE %llu)",
    1083              :                  (unsigned long long)modseq) != 0)
    1084            0 :         return -1;
    1085              : 
    1086            7 :     Response resp = {0};
    1087            7 :     if (read_response(c, tag, &resp) != 0) {
    1088            0 :         response_free(&resp);
    1089            0 :         return -1;
    1090              :     }
    1091              : 
    1092            7 :     int cap = 32, cnt = 0;
    1093            7 :     ImapFlagUpdate *updates = NULL;
    1094              : 
    1095           14 :     for (int i = 0; i < resp.count; i++) {
    1096            7 :         const char *line = resp.untagged[i];
    1097            7 :         if (!strstr(line, "FETCH")) continue;
    1098            7 :         if (!strstr(line, "FLAGS")) continue;
    1099              : 
    1100            7 :         unsigned long uid_val = parse_fetch_uid(line);
    1101            7 :         if (!uid_val) continue;
    1102              : 
    1103            7 :         if (!updates) {
    1104            7 :             updates = malloc((size_t)cap * sizeof(ImapFlagUpdate));
    1105            7 :             if (!updates) { response_free(&resp); return -1; }
    1106              :         }
    1107            7 :         if (cnt == cap) {
    1108            0 :             cap *= 2;
    1109            0 :             ImapFlagUpdate *tmp = realloc(updates,
    1110            0 :                                           (size_t)cap * sizeof(ImapFlagUpdate));
    1111            0 :             if (!tmp) { free(updates); response_free(&resp); return -1; }
    1112            0 :             updates = tmp;
    1113              :         }
    1114            7 :         snprintf(updates[cnt].uid, 17, "%016u", (unsigned)uid_val);
    1115            7 :         updates[cnt].flags = parse_imap_flags(line);
    1116            7 :         cnt++;
    1117              :     }
    1118              : 
    1119            7 :     response_free(&resp);
    1120            7 :     *out       = updates;
    1121            7 :     *count_out = cnt;
    1122            7 :     return 0;
    1123              : }
        

Generated by: LCOV version 2.0-1