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