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 : }
|