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