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

            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         6725 : static ssize_t net_read(ImapClient *c, char *buf, size_t n) {
      51         6725 :     if (c->use_tls) {
      52         6718 :         int r = SSL_read(c->ssl, buf, (int)n);
      53         6718 :         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            7 :     ssize_t r = read(c->fd, buf, n);
      60            7 :     if (r < 0 && (errno == EINTR || errno == EAGAIN)) return 0;
      61            7 :     return r;
      62              : }
      63              : 
      64              : /** Write `n` bytes to the socket/TLS layer.
      65              :  *  Returns 0 on success, -1 on error. */
      66         2693 : static int net_write(ImapClient *c, const char *buf, size_t n) {
      67         2693 :     size_t sent = 0;
      68         5386 :     while (sent < n) {
      69              :         ssize_t r;
      70         2693 :         if (c->use_tls) {
      71         2688 :             int w = SSL_write(c->ssl, buf + sent, (int)(n - sent));
      72         2688 :             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         2688 :             r = (ssize_t)w;
      78              :         } else {
      79            5 :             r = write(c->fd, buf + sent, n - sent);
      80            5 :             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         2693 :         sent += (size_t)r;
      87              :     }
      88         2693 :     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       172908 : static int rbuf_fill(ImapClient *c) {
      95       172908 :     if (c->rbuf_pos < c->rbuf_len) return 0;
      96              :     /* Compact: move unused data to front */
      97         6069 :     c->rbuf_pos = 0;
      98         6069 :     c->rbuf_len = 0;
      99         6069 :     ssize_t r = net_read(c, c->rbuf, RBUF_SIZE);
     100         6069 :     if (r <= 0) return -1;
     101         6069 :     c->rbuf_len = (size_t)r;
     102         6069 :     return 0;
     103              : }
     104              : 
     105              : /** Read exactly `n` bytes into `out`. Returns 0 on success, -1 on error/EOF. */
     106          656 : static int rbuf_read_exact(ImapClient *c, char *out, size_t n) {
     107          656 :     size_t got = 0;
     108         1312 :     while (got < n) {
     109          656 :         if (c->rbuf_pos >= c->rbuf_len) {
     110          656 :             c->rbuf_pos = 0;
     111          656 :             c->rbuf_len = 0;
     112          656 :             ssize_t r = net_read(c, c->rbuf, RBUF_SIZE);
     113          656 :             if (r <= 0) return -1;
     114          656 :             c->rbuf_len = (size_t)r;
     115              :         }
     116          656 :         size_t avail = c->rbuf_len - c->rbuf_pos;
     117          656 :         size_t take  = avail < (n - got) ? avail : (n - got);
     118          656 :         memcpy(out + got, c->rbuf + c->rbuf_pos, take);
     119          656 :         c->rbuf_pos += take;
     120          656 :         got         += take;
     121              :     }
     122          656 :     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         3599 : static void linebuf_free(LineBuf *lb) { free(lb->data); lb->data = NULL; lb->len = lb->cap = 0; }
     134              : 
     135       159010 : static int linebuf_append(LineBuf *lb, char ch) {
     136       159010 :     if (lb->len + 1 >= lb->cap) {
     137         3630 :         size_t ncap = lb->cap ? lb->cap * 2 : 256;
     138         3630 :         char *tmp = realloc(lb->data, ncap);
     139         3630 :         if (!tmp) return -1;
     140         3630 :         lb->data = tmp;
     141         3630 :         lb->cap  = ncap;
     142              :     }
     143       159010 :     lb->data[lb->len++] = ch;
     144       159010 :     lb->data[lb->len]   = '\0';
     145       159010 :     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         6949 : static int read_line(ImapClient *c, LineBuf *lb) {
     153         6949 :     lb->len = 0;
     154       165959 :     for (;;) {
     155       172908 :         if (rbuf_fill(c) != 0) return -1;
     156       172908 :         char ch = c->rbuf[c->rbuf_pos++];
     157       172908 :         if (ch == '\r') continue;          /* skip CR */
     158       165959 :         if (ch == '\n') {
     159         6949 :             if (lb->data) lb->data[lb->len] = '\0';
     160         6949 :             return 0;
     161              :         }
     162       159010 :         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         2687 : static int send_cmd(ImapClient *c, char tag_out[16], const char *fmt, ...) {
     173         2687 :     c->tag_num++;
     174         2687 :     snprintf(tag_out, 16, "A%04d", c->tag_num);
     175              : 
     176              :     char buf[4096];
     177              :     va_list ap;
     178         2687 :     va_start(ap, fmt);
     179         2687 :     int len = vsnprintf(buf, sizeof(buf) - 3, fmt, ap);
     180         2687 :     va_end(ap);
     181         2687 :     if (len < 0 || (size_t)len >= sizeof(buf) - 3) return -1;
     182              : 
     183              :     /* Append CRLF */
     184         2687 :     buf[len]     = '\r';
     185         2687 :     buf[len + 1] = '\n';
     186         2687 :     buf[len + 2] = '\0';
     187              : 
     188              :     /* Log command — mask password in LOGIN commands */
     189         2687 :     if (strncmp(buf, "LOGIN ", 6) == 0) {
     190              :         /* Extract user (first quoted token) and replace password with xxxxx */
     191          254 :         const char *p = buf + 6;
     192              :         /* skip optional leading space */
     193          254 :         while (*p == ' ') p++;
     194              :         /* find end of username token (quoted or unquoted) */
     195          254 :         const char *user_end = NULL;
     196          254 :         if (*p == '"') {
     197          254 :             user_end = strchr(p + 1, '"');
     198          254 :             if (user_end) user_end++;  /* include closing quote */
     199              :         } else {
     200            0 :             user_end = strchr(p, ' ');
     201              :         }
     202          254 :         if (user_end) {
     203          254 :             logger_log(LOG_DEBUG, "IMAP [OUT] %s LOGIN %.*s xxxxx",
     204          254 :                        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         2433 :         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         2687 :     int flen = snprintf(full, sizeof(full), "%s %s", tag_out, buf);
     217         2687 :     if (flen < 0 || (size_t)flen >= sizeof(full)) return -1;
     218         2687 :     if (net_write(c, full, (size_t)flen) != 0) return -1;
     219         2687 :     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         3350 : static long read_literal_if_present(ImapClient *c, const char *line,
     236              :                                     char **lit_out, size_t *lit_len) {
     237         3350 :     *lit_out = NULL;
     238         3350 :     *lit_len = 0;
     239              : 
     240              :     /* Find trailing {N} */
     241         3350 :     const char *p = strrchr(line, '{');
     242         3350 :     if (!p) return 0;
     243              :     char *end;
     244          656 :     long sz = strtol(p + 1, &end, 10);
     245          656 :     if (*end != '}' || sz < 0) return 0;
     246              : 
     247              :     /* Allocate output buffer */
     248          656 :     char *buf = malloc((size_t)sz + 1);
     249          656 :     if (!buf) return -1;
     250              : 
     251          656 :     if (sz > 0) {
     252          656 :         size_t total = (size_t)sz;
     253              : 
     254          656 :         if (!c->on_progress || total < PROGRESS_THRESHOLD) {
     255              :             /* Small literal or no callback: read all at once */
     256          656 :             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          656 :     buf[sz] = '\0';
     277          656 :     *lit_out = buf;
     278          656 :     *lit_len = (size_t)sz;
     279          656 :     return sz;
     280              : }
     281              : 
     282          308 : void imap_set_progress(ImapClient *c, ImapProgressFn fn, void *ctx) {
     283          308 :     if (!c) return;
     284          307 :     c->on_progress   = fn;
     285          307 :     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         2689 : static void response_free(Response *r) {
     300         6039 :     for (int i = 0; i < r->count; i++) free(r->untagged[i]);
     301         2689 :     free(r->untagged);
     302         2689 :     free(r->literal);
     303         2689 :     free(r->tagged);
     304         2689 :     memset(r, 0, sizeof(*r));
     305         2689 : }
     306              : 
     307         3350 : static int response_add(Response *r, const char *line) {
     308         3350 :     if (r->count == r->cap) {
     309         2419 :         int ncap = r->cap ? r->cap * 2 : 16;
     310         2419 :         char **tmp = realloc(r->untagged, (size_t)ncap * sizeof(char *));
     311         2419 :         if (!tmp) return -1;
     312         2419 :         r->untagged = tmp;
     313         2419 :         r->cap      = ncap;
     314              :     }
     315         3350 :     char *copy = strdup(line);
     316         3350 :     if (!copy) return -1;
     317         3350 :     r->untagged[r->count++] = copy;
     318         3350 :     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         2689 : static int read_response(ImapClient *c, const char *tag, Response *r) {
     327         2689 :     LineBuf lb = {NULL, 0, 0};
     328              : 
     329         3350 :     for (;;) {
     330         6039 :         if (read_line(c, &lb) != 0) {
     331            0 :             linebuf_free(&lb);
     332         2689 :             return -1;
     333              :         }
     334              : 
     335         6039 :         const char *line = lb.data ? lb.data : "";
     336         6039 :         logger_log(LOG_DEBUG, "IMAP [ IN] %s", line);
     337              : 
     338              :         /* Tagged response? */
     339         6039 :         size_t tlen = strlen(tag);
     340         6039 :         if (strncmp(line, tag, tlen) == 0 && line[tlen] == ' ') {
     341         2689 :             const char *status = line + tlen + 1;
     342         2689 :             int ok = (strncasecmp(status, "OK", 2) == 0);
     343         2689 :             if (!ok)
     344            4 :                 logger_log(LOG_WARN, "IMAP %s", line);
     345         2689 :             r->tagged = strdup(line);
     346         2689 :             linebuf_free(&lb);  /* free AFTER all accesses to line/status */
     347         2689 :             return ok ? 0 : -1;
     348              :         }
     349              : 
     350              :         /* Untagged: check for literal */
     351         3350 :         char  *lit     = NULL;
     352         3350 :         size_t lit_len = 0;
     353         3350 :         long   lsz     = read_literal_if_present(c, line, &lit, &lit_len);
     354         3350 :         if (lsz < 0) { linebuf_free(&lb); return -1; }
     355              : 
     356         3350 :         response_add(r, line);
     357              : 
     358         3350 :         if (lit) {
     359              :             /* We read the literal; now read the closing line: ")\r\n" or similar */
     360          656 :             if (!r->literal) {
     361          656 :                 r->literal = lit;
     362          656 :                 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          656 :             LineBuf trail = {NULL, 0, 0};
     368          656 :             if (read_line(c, &trail) == 0 && trail.data && trail.data[0])
     369          656 :                 logger_log(LOG_DEBUG, "IMAP [ IN] %s", trail.data);
     370          656 :             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          263 : static int parse_url(const char *url, char *host, size_t hsize,
     382              :                      char *port, size_t psize, int *use_tls) {
     383          263 :     *use_tls = 0;
     384          263 :     const char *p = url;
     385              : 
     386          263 :     if (strncasecmp(p, "imaps://", 8) == 0) { *use_tls = 1; p += 8; }
     387            3 :     else if (strncasecmp(p, "imap://", 7) == 0) { p += 7; }
     388              :     else {
     389              :         /* Treat as bare hostname, default IMAPS */
     390            1 :         *use_tls = 1;
     391            1 :         snprintf(host, hsize, "%s", url);
     392            1 :         snprintf(port, psize, "993");
     393            1 :         return 0;
     394              :     }
     395              : 
     396              :     /* host[:port] */
     397          262 :     const char *colon = strchr(p, ':');
     398          262 :     if (colon) {
     399          256 :         size_t hlen = (size_t)(colon - p);
     400          256 :         if (hlen >= hsize) return -1;
     401          256 :         memcpy(host, p, hlen);
     402          256 :         host[hlen] = '\0';
     403          256 :         snprintf(port, psize, "%s", colon + 1);
     404              :     } else {
     405            6 :         snprintf(host, hsize, "%s", p);
     406            6 :         snprintf(port, psize, "%s", *use_tls ? "993" : "143");
     407              :     }
     408          262 :     return 0;
     409              : }
     410              : 
     411              : /* ── Connect ─────────────────────────────────────────────────────────── */
     412              : 
     413          263 : ImapClient *imap_connect(const char *host_url, const char *user,
     414              :                          const char *pass, int verify_tls) {
     415              :     char host[256], port[16];
     416          263 :     int  use_tls = 1;
     417          263 :     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          263 :     if (!use_tls && verify_tls) {
     425            1 :         logger_log(LOG_ERROR,
     426              :                    "imap_connect: refused to connect to %s without TLS — "
     427              :                    "use imaps:// to protect credentials", host_url);
     428            1 :         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            1 :         return NULL;
     434              :     }
     435              : 
     436              :     /* TCP connect */
     437          262 :     struct addrinfo hints = {0};
     438          262 :     hints.ai_family   = AF_UNSPEC;
     439          262 :     hints.ai_socktype = SOCK_STREAM;
     440          262 :     struct addrinfo *ai = NULL;
     441          262 :     int rc = getaddrinfo(host, port, &hints, &ai);
     442          262 :     if (rc != 0) {
     443            6 :         logger_log(LOG_ERROR, "getaddrinfo(%s:%s): %s", host, port, gai_strerror(rc));
     444            6 :         return NULL;
     445              :     }
     446              : 
     447          256 :     int fd = -1;
     448          498 :     for (struct addrinfo *r = ai; r; r = r->ai_next) {
     449          496 :         fd = socket(r->ai_family, r->ai_socktype, r->ai_protocol);
     450          496 :         if (fd < 0) continue;
     451          496 :         if (connect(fd, r->ai_addr, r->ai_addrlen) == 0) break;
     452          242 :         close(fd);
     453          242 :         fd = -1;
     454              :     }
     455          256 :     freeaddrinfo(ai);
     456              : 
     457          256 :     if (fd < 0) {
     458            2 :         logger_log(LOG_ERROR, "connect to %s:%s failed: %s", host, port, strerror(errno));
     459            2 :         return NULL;
     460              :     }
     461              : 
     462              :     /* Apply a 15-second read/write timeout so blocking ops don't hang forever */
     463              :     {
     464          254 :         struct timeval tv = { .tv_sec = 15, .tv_usec = 0 };
     465          254 :         setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
     466          254 :         setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
     467              :     }
     468              : 
     469          254 :     ImapClient *c = calloc(1, sizeof(ImapClient));
     470          254 :     if (!c) { close(fd); return NULL; }
     471          254 :     c->fd = fd;
     472          254 :     c->use_tls = use_tls;
     473              : 
     474              :     /* TLS handshake */
     475          254 :     if (use_tls) {
     476              :         /* Init OpenSSL (idempotent in OpenSSL 1.1+) */
     477          253 :         SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
     478          253 :         if (!ctx) {
     479            0 :             logger_log(LOG_ERROR, "SSL_CTX_new failed");
     480            0 :             free(c); close(fd);
     481            0 :             return NULL;
     482              :         }
     483          253 :         SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
     484          253 :         SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
     485          253 :         if (!verify_tls) {
     486          253 :             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          253 :         SSL *ssl = SSL_new(ctx);
     492          253 :         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          253 :         SSL_set_fd(ssl, fd);
     498          253 :         SSL_set_tlsext_host_name(ssl, host);  /* SNI */
     499          253 :         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          253 :         c->ctx = ctx;
     505          253 :         c->ssl = ssl;
     506          253 :         logger_log(LOG_DEBUG, "IMAP TLS handshake OK with %s (TLS/%s)",
     507              :                    host, SSL_get_version(ssl));
     508              :     } else {
     509            1 :         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          254 :     LineBuf lb = {NULL, 0, 0};
     516          254 :     if (read_line(c, &lb) != 0) {
     517            0 :         logger_log(LOG_ERROR, "No greeting from %s", host);
     518            0 :         goto fail;
     519              :     }
     520          254 :     logger_log(LOG_DEBUG, "IMAP [ IN] %s", lb.data ? lb.data : "");
     521          254 :     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          254 :     if (send_cmd(c, tag, "LOGIN \"%s\" \"%s\"", user, pass) != 0)
     528            0 :         goto fail;
     529              : 
     530          254 :     Response resp = {0};
     531          254 :     rc = read_response(c, tag, &resp);
     532          254 :     response_free(&resp);
     533          254 :     if (rc != 0) {
     534            1 :         logger_log(LOG_ERROR, "LOGIN failed for user %s on %s", user, host);
     535            1 :         goto fail;
     536              :     }
     537              : 
     538          253 :     logger_log(LOG_DEBUG, "IMAP connected and authenticated: %s@%s", user, host);
     539          253 :     return c;
     540              : 
     541            1 : fail:
     542            1 :     if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); }
     543            1 :     if (c->ctx) SSL_CTX_free(c->ctx);
     544            1 :     close(c->fd);
     545            1 :     free(c);
     546            1 :     return NULL;
     547              : }
     548              : 
     549              : /* ── Disconnect ──────────────────────────────────────────────────────── */
     550              : 
     551          202 : void imap_disconnect(ImapClient *c) {
     552          202 :     if (!c) return;
     553              :     /* Send LOGOUT (ignore errors — we're closing anyway) */
     554              :     char tag[16];
     555          201 :     send_cmd(c, tag, "LOGOUT");
     556          201 :     Response r = {0};
     557          201 :     read_response(c, tag, &r);
     558          201 :     response_free(&r);
     559              : 
     560          201 :     if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); }
     561          201 :     if (c->ctx) SSL_CTX_free(c->ctx);
     562          201 :     if (c->fd >= 0) close(c->fd);
     563          201 :     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          184 : static char *parse_list_line(const char *line, char *sep_out) {
     574              :     /* Skip "* LIST " */
     575          184 :     if (strncasecmp(line, "* LIST ", 7) != 0) return NULL;
     576          184 :     const char *p = line + 7;
     577              : 
     578              :     /* Skip flags: (...) */
     579          184 :     if (*p == '(') {
     580          184 :         p = strchr(p, ')');
     581          184 :         if (!p) return NULL;
     582          184 :         p++;
     583              :     }
     584          368 :     while (*p == ' ') p++;
     585              : 
     586              :     /* Separator: "." or "/" or NIL */
     587          184 :     if (*p == '"') {
     588          183 :         p++;
     589          183 :         if (*p && *(p + 1) == '"') {
     590          182 :             *sep_out = *p;
     591          182 :             p += 2;
     592            1 :         } else if (*p == '"') {
     593              :             /* empty separator */
     594            1 :             p++;
     595              :         }
     596            1 :     } else if (strncasecmp(p, "NIL", 3) == 0) {
     597            1 :         *sep_out = '.';
     598            1 :         p += 3;
     599              :     }
     600          368 :     while (*p == ' ') p++;
     601              : 
     602              :     /* Folder name: quoted or unquoted */
     603          184 :     if (*p == '"') {
     604          181 :         p++;
     605          181 :         const char *end = strchr(p, '"');
     606          181 :         if (!end) return NULL;
     607          181 :         size_t len = (size_t)(end - p);
     608          181 :         char *name = malloc(len + 1);
     609          181 :         if (!name) return NULL;
     610          181 :         memcpy(name, p, len);
     611          181 :         name[len] = '\0';
     612          181 :         return name;
     613              :     } else {
     614              :         /* Unquoted: until end of line */
     615            3 :         size_t len = strlen(p);
     616            3 :         while (len > 0 && (p[len - 1] == ' ' || p[len - 1] == '\r')) len--;
     617            3 :         char *name = malloc(len + 1);
     618            3 :         if (!name) return NULL;
     619            3 :         memcpy(name, p, len);
     620            3 :         name[len] = '\0';
     621            3 :         return name;
     622              :     }
     623              : }
     624              : 
     625           27 : int imap_list(ImapClient *c, char ***folders_out, int *count_out, char *sep_out) {
     626           27 :     *folders_out = NULL;
     627           27 :     *count_out   = 0;
     628           27 :     if (sep_out) *sep_out = '.';
     629              : 
     630              :     char tag[16];
     631           27 :     if (send_cmd(c, tag, "LIST \"\" \"*\"") != 0) return -1;
     632              : 
     633           27 :     Response resp = {0};
     634           27 :     if (read_response(c, tag, &resp) != 0) {
     635            0 :         response_free(&resp);
     636            0 :         return -1;
     637              :     }
     638              : 
     639           27 :     int count = 0, cap = 0;
     640           27 :     char **folders = NULL;
     641           27 :     char  sep = '.';
     642              : 
     643          211 :     for (int i = 0; i < resp.count; i++) {
     644          184 :         char got_sep = '.';
     645          184 :         char *raw = parse_list_line(resp.untagged[i], &got_sep);
     646          184 :         if (!raw) continue;
     647          184 :         sep = got_sep;
     648          184 :         char *name = imap_utf7_decode(raw);
     649          184 :         free(raw);
     650          184 :         if (!name) continue;
     651              : 
     652          184 :         if (count == cap) {
     653           27 :             cap = cap ? cap * 2 : 16;
     654           27 :             char **tmp = realloc(folders, (size_t)cap * sizeof(char *));
     655           27 :             if (!tmp) { free(name); break; }
     656           27 :             folders = tmp;
     657              :         }
     658          184 :         folders[count++] = name;
     659              :     }
     660              : 
     661           27 :     response_free(&resp);
     662           27 :     *folders_out = folders;
     663           27 :     *count_out   = count;
     664           27 :     if (sep_out) *sep_out = sep;
     665           27 :     return 0;
     666              : }
     667              : 
     668              : /* ── CREATE / DELETE folder ──────────────────────────────────────────── */
     669              : 
     670            7 : int imap_create_folder(ImapClient *c, const char *name) {
     671            7 :     char *utf7 = imap_utf7_encode(name);
     672            7 :     const char *utf7_name = utf7 ? utf7 : name;
     673              : 
     674              :     char tag[16];
     675            7 :     int rc = send_cmd(c, tag, "CREATE \"%s\"", utf7_name);
     676            7 :     free(utf7);
     677            7 :     if (rc != 0) return -1;
     678              : 
     679            7 :     Response resp = {0};
     680            7 :     rc = read_response(c, tag, &resp);
     681              :     /* Treat [ALREADYEXISTS] as success — the folder is there, which is all we need. */
     682            7 :     if (rc != 0 && resp.tagged &&
     683            3 :         strcasestr(resp.tagged, "[ALREADYEXISTS]") != NULL)
     684            2 :         rc = 0;
     685            7 :     response_free(&resp);
     686            7 :     return rc;
     687              : }
     688              : 
     689            2 : int imap_delete_folder(ImapClient *c, const char *name) {
     690            2 :     char *utf7 = imap_utf7_encode(name);
     691            2 :     const char *utf7_name = utf7 ? utf7 : name;
     692              : 
     693              :     char tag[16];
     694            2 :     int rc = send_cmd(c, tag, "DELETE \"%s\"", utf7_name);
     695            2 :     free(utf7);
     696            2 :     if (rc != 0) return -1;
     697              : 
     698            2 :     Response resp = {0};
     699            2 :     rc = read_response(c, tag, &resp);
     700            2 :     response_free(&resp);
     701            2 :     return rc;
     702              : }
     703              : 
     704              : /* ── SELECT ──────────────────────────────────────────────────────────── */
     705              : 
     706          286 : int imap_select(ImapClient *c, const char *folder) {
     707          286 :     char *utf7 = imap_utf7_encode(folder);
     708          286 :     const char *name = utf7 ? utf7 : folder;
     709              : 
     710              :     char tag[16];
     711              :     int rc;
     712              :     /* Quote the folder name */
     713          286 :     rc = send_cmd(c, tag, "SELECT \"%s\"", name);
     714          286 :     free(utf7);
     715          286 :     if (rc != 0) return -1;
     716              : 
     717          286 :     Response resp = {0};
     718          286 :     rc = read_response(c, tag, &resp);
     719          286 :     response_free(&resp);
     720          286 :     return rc;
     721              : }
     722              : 
     723              : /* ── UID SEARCH ──────────────────────────────────────────────────────── */
     724              : 
     725         1125 : int imap_uid_search(ImapClient *c, const char *criteria,
     726              :                     char (**uids_out)[17], int *count_out) {
     727         1125 :     *uids_out  = NULL;
     728         1125 :     *count_out = 0;
     729              : 
     730              :     char tag[16];
     731         1125 :     if (send_cmd(c, tag, "UID SEARCH %s", criteria) != 0) return -1;
     732              : 
     733         1125 :     Response resp = {0};
     734         1125 :     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         1125 :     int cap = 32, cnt = 0;
     741         1125 :     char (*uids)[17] = NULL;
     742              : 
     743         2250 :     for (int i = 0; i < resp.count; i++) {
     744         1125 :         const char *line = resp.untagged[i];
     745         1125 :         if (strncasecmp(line, "* SEARCH", 8) != 0) continue;
     746         1125 :         const char *p = line + 8;
     747         5003 :         for (;;) {
     748        11131 :             while (*p == ' ') p++;
     749         6128 :             if (!*p) break;
     750              :             char *e;
     751         5003 :             unsigned long uid = strtoul(p, &e, 10);
     752         5003 :             if (e == p) break;
     753         5003 :             if (uid > 0 && uid <= 4294967295UL) {
     754         5003 :                 if (!uids) {
     755         1084 :                     uids = malloc((size_t)cap * sizeof(char[17]));
     756         1084 :                     if (!uids) { response_free(&resp); return -1; }
     757              :                 }
     758         5003 :                 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         5003 :                 snprintf(uids[cnt], 17, "%016lu", uid);
     765         5003 :                 cnt++;
     766              :             }
     767         5003 :             p = e;
     768              :         }
     769              :     }
     770              : 
     771         1125 :     response_free(&resp);
     772         1125 :     *uids_out  = uids;
     773         1125 :     *count_out = cnt;
     774         1125 :     return 0;
     775              : }
     776              : 
     777              : /* ── UID FETCH ───────────────────────────────────────────────────────── */
     778              : 
     779          657 : static char *uid_fetch_part(ImapClient *c, const char *uid, const char *section) {
     780              :     char tag[16];
     781          657 :     if (send_cmd(c, tag, "UID FETCH %s (UID %s)", uid, section) != 0)
     782            0 :         return NULL;
     783              : 
     784          657 :     Response resp = {0};
     785          657 :     if (read_response(c, tag, &resp) != 0) {
     786            0 :         response_free(&resp);
     787            0 :         return NULL;
     788              :     }
     789              : 
     790          657 :     char *result = NULL;
     791          657 :     if (resp.literal) {
     792          656 :         result = resp.literal;
     793          656 :         resp.literal = NULL;  /* transfer ownership */
     794              :     }
     795          657 :     response_free(&resp);
     796              : 
     797          657 :     if (!result)
     798            1 :         logger_log(LOG_WARN, "UID FETCH %s %s: no literal in response", uid, section);
     799          657 :     return result;
     800              : }
     801              : 
     802          487 : char *imap_uid_fetch_headers(ImapClient *c, const char *uid) {
     803          487 :     return uid_fetch_part(c, uid, "BODY.PEEK[HEADER]");
     804              : }
     805              : 
     806          170 : char *imap_uid_fetch_body(ImapClient *c, const char *uid) {
     807          170 :     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           11 : static int parse_imap_flags(const char *line) {
     817              :     /* Find FLAGS ( ... ) in the line */
     818           11 :     const char *p = strstr(line, "FLAGS (");
     819           11 :     if (!p) return 0;
     820           11 :     p += 7; /* skip "FLAGS (" */
     821           11 :     int flags = 0;
     822           11 :     if (strstr(p, "\\Seen")     == NULL) flags |= MSG_FLAG_UNSEEN;
     823           11 :     if (strstr(p, "\\Flagged")  != NULL) flags |= MSG_FLAG_FLAGGED;
     824           11 :     if (strstr(p, "$Done")      != NULL) flags |= MSG_FLAG_DONE;
     825           11 :     if (strstr(p, "\\Answered") != NULL) flags |= MSG_FLAG_ANSWERED;
     826           11 :     if (strstr(p, "$Forwarded") != NULL) flags |= MSG_FLAG_FORWARDED;
     827           11 :     if (strstr(p, "$Phishing")  != NULL) flags |= MSG_FLAG_PHISHING;
     828              :     /* $Junk and $NotJunk: $NotJunk wins if both somehow present */
     829           11 :     if (strstr(p, "$Junk")      != NULL) flags |= MSG_FLAG_JUNK;
     830           11 :     if (strstr(p, "$NotJunk")   != NULL) flags &= ~MSG_FLAG_JUNK;
     831           11 :     return flags;
     832              : }
     833              : 
     834            2 : int imap_uid_fetch_flags(ImapClient *c, const char *uid) {
     835              :     char tag[16];
     836            2 :     if (send_cmd(c, tag, "UID FETCH %s (UID FLAGS)", uid) != 0) return -1;
     837              : 
     838            2 :     Response resp = {0};
     839            2 :     if (read_response(c, tag, &resp) != 0) {
     840            0 :         response_free(&resp);
     841            0 :         return -1;
     842              :     }
     843              : 
     844            2 :     int flags = -1;
     845            2 :     for (int i = 0; i < resp.count; i++) {
     846            2 :         if (strstr(resp.untagged[i], "FETCH") && strstr(resp.untagged[i], "FLAGS")) {
     847            2 :             flags = parse_imap_flags(resp.untagged[i]);
     848            2 :             break;
     849              :         }
     850              :     }
     851            2 :     response_free(&resp);
     852            2 :     return flags < 0 ? 0 : flags;
     853              : }
     854              : 
     855              : /* ── UID STORE (set/clear flag) ──────────────────────────────────────── */
     856              : 
     857           23 : int imap_uid_set_flag(ImapClient *c, const char *uid, const char *flag_name, int add) {
     858              :     char tag[16];
     859           23 :     if (send_cmd(c, tag, "UID STORE %s %sFLAGS (%s)",
     860              :                  uid, add ? "+" : "-", flag_name) != 0)
     861            0 :         return -1;
     862           23 :     Response resp = {0};
     863           23 :     int rc = read_response(c, tag, &resp);
     864           23 :     response_free(&resp);
     865           23 :     return rc;
     866              : }
     867              : 
     868            2 : int imap_uid_copy(ImapClient *c, const char *uid, const char *target_folder) {
     869              :     /* Ensure target folder exists first */
     870            2 :     imap_create_folder(c, target_folder);
     871              :     char tag[16];
     872            2 :     if (send_cmd(c, tag, "UID COPY %s \"%s\"", uid, target_folder) != 0)
     873            0 :         return -1;
     874            2 :     Response resp = {0};
     875            2 :     int rc = read_response(c, tag, &resp);
     876            2 :     response_free(&resp);
     877            2 :     return rc;
     878              : }
     879              : 
     880            2 : int imap_uid_move(ImapClient *c, const char *uid, const char *target_folder) {
     881            2 :     if (imap_uid_copy(c, uid, target_folder) != 0) return -1;
     882            2 :     if (imap_uid_set_flag(c, uid, "\\Deleted", 1) != 0) return -1;
     883              :     /* EXPUNGE */
     884              :     char tag[16];
     885            2 :     if (send_cmd(c, tag, "EXPUNGE") != 0) return -1;
     886            2 :     Response resp = {0};
     887            2 :     int rc = read_response(c, tag, &resp);
     888            2 :     response_free(&resp);
     889            2 :     return rc;
     890              : }
     891              : 
     892            2 : 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            2 :     if (imap_create_folder(c, folder) != 0)
     917            1 :         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            2 :     c->tag_num++;
     922              :     char tag[16];
     923            2 :     snprintf(tag, sizeof(tag), "A%04d", c->tag_num);
     924              : 
     925              :     char cmd[1024];
     926            2 :     int cmdlen = snprintf(cmd, sizeof(cmd),
     927              :                           "%s APPEND \"%s\" (\\Seen) {%zu+}\r\n",
     928              :                           tag, folder, msg_len);
     929            2 :     if (cmdlen < 0 || (size_t)cmdlen >= sizeof(cmd)) return -1;
     930              : 
     931            2 :     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            2 :         struct timeval tv = { .tv_sec = 30, .tv_usec = 0 };
     937            2 :         setsockopt(c->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
     938              :     }
     939              : 
     940            2 :     int rc = -1;
     941            4 :     if (net_write(c, cmd, (size_t)cmdlen) != 0 ||
     942            4 :         net_write(c, msg, msg_len) != 0 ||
     943            2 :         net_write(c, "\r\n", 2) != 0) {
     944            0 :         logger_log(LOG_ERROR, "IMAP APPEND: write failed");
     945              :     } else {
     946            2 :         logger_log(LOG_DEBUG, "IMAP APPEND: sent %zu-byte literal", msg_len);
     947            2 :         Response resp = {0};
     948            2 :         rc = read_response(c, tag, &resp);
     949            2 :         response_free(&resp);
     950              :     }
     951              : 
     952              :     /* Restore normal 15-second receive timeout. */
     953              :     {
     954            2 :         struct timeval tv = { .tv_sec = 15, .tv_usec = 0 };
     955            2 :         setsockopt(c->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
     956              :     }
     957            2 :     return rc;
     958              : }
     959              : 
     960              : /* ── CONDSTORE / QRESYNC (RFC 4551 / RFC 5162) ──────────────────────────── */
     961              : 
     962          154 : int imap_get_caps(ImapClient *c) {
     963          154 :     if (c->caps_queried) return c->caps;
     964           20 :     c->caps_queried = 1;
     965              : 
     966              :     char tag[16];
     967           20 :     if (send_cmd(c, tag, "CAPABILITY") != 0) return 0;
     968           20 :     Response resp = {0};
     969           20 :     if (read_response(c, tag, &resp) != 0) { response_free(&resp); return 0; }
     970           40 :     for (int i = 0; i < resp.count; i++) {
     971           20 :         const char *line = resp.untagged[i];
     972           20 :         if (strstr(line, "CONDSTORE")) c->caps |= IMAP_CAP_CONDSTORE;
     973           20 :         if (strstr(line, "QRESYNC"))   c->caps |= IMAP_CAP_QRESYNC;
     974              :     }
     975           20 :     response_free(&resp);
     976           20 :     return c->caps;
     977              : }
     978              : 
     979              : /** Parse UIDVALIDITY and HIGHESTMODSEQ from untagged + tagged SELECT responses. */
     980           67 : static void parse_select_result(const Response *resp, ImapSelectResult *res) {
     981          340 :     for (int i = 0; i < resp->count; i++) {
     982          273 :         const char *line = resp->untagged[i];
     983              :         const char *p;
     984          273 :         p = strstr(line, "[HIGHESTMODSEQ ");
     985          273 :         if (p) res->highestmodseq = (uint64_t)strtoull(p + 15, NULL, 10);
     986          273 :         p = strstr(line, "[UIDVALIDITY ");
     987          273 :         if (p) res->uidvalidity   = (uint32_t)strtoul (p + 13, NULL, 10);
     988              :     }
     989              :     /* Some servers put HIGHESTMODSEQ in the tagged OK response */
     990           67 :     if (resp->tagged) {
     991           67 :         const char *p = strstr(resp->tagged, "[HIGHESTMODSEQ ");
     992           67 :         if (p) res->highestmodseq = (uint64_t)strtoull(p + 15, NULL, 10);
     993              :     }
     994           67 : }
     995              : 
     996           49 : int imap_select_condstore(ImapClient *c, const char *folder, ImapSelectResult *res) {
     997           49 :     memset(res, 0, sizeof(*res));
     998              : 
     999           49 :     char *utf7 = imap_utf7_encode(folder);
    1000           49 :     const char *name = utf7 ? utf7 : folder;
    1001              :     char tag[16];
    1002           49 :     int rc = send_cmd(c, tag, "SELECT \"%s\" (CONDSTORE)", name);
    1003           49 :     free(utf7);
    1004           49 :     if (rc != 0) return -1;
    1005              : 
    1006           49 :     Response resp = {0};
    1007           49 :     rc = read_response(c, tag, &resp);
    1008           49 :     if (rc == 0)
    1009           49 :         parse_select_result(&resp, res);
    1010           49 :     response_free(&resp);
    1011           49 :     return rc;
    1012              : }
    1013              : 
    1014           18 : int imap_select_qresync(ImapClient *c, const char *folder,
    1015              :                          uint32_t known_uidval, uint64_t known_modseq,
    1016              :                          ImapSelectResult *res) {
    1017           18 :     memset(res, 0, sizeof(*res));
    1018              : 
    1019              :     /* ENABLE QRESYNC once per session (RFC 5161/5162) */
    1020           18 :     if (!c->qresync_enabled) {
    1021              :         char entag[16];
    1022            4 :         if (send_cmd(c, entag, "ENABLE QRESYNC") == 0) {
    1023            4 :             Response enr = {0};
    1024            4 :             read_response(c, entag, &enr);  /* ignore errors */
    1025            4 :             response_free(&enr);
    1026              :         }
    1027            4 :         c->qresync_enabled = 1;
    1028              :     }
    1029              : 
    1030           18 :     char *utf7 = imap_utf7_encode(folder);
    1031           18 :     const char *name = utf7 ? utf7 : folder;
    1032              :     char tag[16];
    1033           18 :     int rc = send_cmd(c, tag,
    1034              :                       "SELECT \"%s\" (QRESYNC (%u %llu))",
    1035              :                       name, known_uidval, (unsigned long long)known_modseq);
    1036           18 :     free(utf7);
    1037           18 :     if (rc != 0) return -1;
    1038              : 
    1039           18 :     Response resp = {0};
    1040           18 :     rc = read_response(c, tag, &resp);
    1041           18 :     if (rc == 0) {
    1042           18 :         parse_select_result(&resp, res);
    1043              : 
    1044              :         /* Parse VANISHED (EARLIER) uid-set from untagged responses */
    1045           97 :         for (int i = 0; i < resp.count; i++) {
    1046           79 :             const char *line = resp.untagged[i];
    1047           79 :             if (strncmp(line, "* VANISHED", 10) != 0) continue;
    1048              :             /* Skip past "(EARLIER)" if present */
    1049           10 :             const char *vs = strstr(line, ") ");
    1050           10 :             if (vs) vs += 2;
    1051              :             else {
    1052            1 :                 vs = strstr(line, "VANISHED ");
    1053            1 :                 if (vs) vs += 9;
    1054              :             }
    1055           10 :             if (vs && *vs)
    1056           10 :                 imap_uid_set_expand(vs, &res->vanished_uids, &res->vanished_count);
    1057              :         }
    1058              :     }
    1059           18 :     response_free(&resp);
    1060           18 :     return rc;
    1061              : }
    1062              : 
    1063              : /** Extract UID value from a FETCH response parenthesised data item. */
    1064            9 : static unsigned long parse_fetch_uid(const char *line) {
    1065              :     /* Look for "(UID nnn" or " UID nnn" inside the FETCH data */
    1066            9 :     const char *p = strstr(line, "FETCH (");
    1067            9 :     if (!p) return 0;
    1068            9 :     p += 7;
    1069            9 :     const char *uid_p = strstr(p, "UID ");
    1070            9 :     if (!uid_p) return 0;
    1071              :     char *end;
    1072            9 :     unsigned long uid = strtoul(uid_p + 4, &end, 10);
    1073            9 :     return (end == uid_p + 4) ? 0 : uid;
    1074              : }
    1075              : 
    1076            8 : int imap_uid_fetch_flags_changedsince(ImapClient *c, uint64_t modseq,
    1077              :                                        ImapFlagUpdate **out, int *count_out) {
    1078            8 :     *out       = NULL;
    1079            8 :     *count_out = 0;
    1080              : 
    1081              :     char tag[16];
    1082            8 :     if (send_cmd(c, tag, "UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE %llu)",
    1083              :                  (unsigned long long)modseq) != 0)
    1084            0 :         return -1;
    1085              : 
    1086            8 :     Response resp = {0};
    1087            8 :     if (read_response(c, tag, &resp) != 0) {
    1088            0 :         response_free(&resp);
    1089            0 :         return -1;
    1090              :     }
    1091              : 
    1092            8 :     int cap = 32, cnt = 0;
    1093            8 :     ImapFlagUpdate *updates = NULL;
    1094              : 
    1095           17 :     for (int i = 0; i < resp.count; i++) {
    1096            9 :         const char *line = resp.untagged[i];
    1097            9 :         if (!strstr(line, "FETCH")) continue;
    1098            9 :         if (!strstr(line, "FLAGS")) continue;
    1099              : 
    1100            9 :         unsigned long uid_val = parse_fetch_uid(line);
    1101            9 :         if (!uid_val) continue;
    1102              : 
    1103            9 :         if (!updates) {
    1104            8 :             updates = malloc((size_t)cap * sizeof(ImapFlagUpdate));
    1105            8 :             if (!updates) { response_free(&resp); return -1; }
    1106              :         }
    1107            9 :         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            9 :         snprintf(updates[cnt].uid, 17, "%016u", (unsigned)uid_val);
    1115            9 :         updates[cnt].flags = parse_imap_flags(line);
    1116            9 :         cnt++;
    1117              :     }
    1118              : 
    1119            8 :     response_free(&resp);
    1120            8 :     *out       = updates;
    1121            8 :     *count_out = cnt;
    1122            8 :     return 0;
    1123              : }
        

Generated by: LCOV version 2.0-1