Line data Source code
1 : #include "gmail_client.h"
2 : #include "gmail_auth.h"
3 : #include "json_util.h"
4 : #include "logger.h"
5 : #include "raii.h"
6 : #include <curl/curl.h>
7 : #include <stdio.h>
8 : #include <stdlib.h>
9 : #include <string.h>
10 :
11 : /* ── Constants ────────────────────────────────────────────────────── */
12 :
13 : #define GMAIL_API "https://gmail.googleapis.com/gmail/v1/users/me"
14 :
15 : /**
16 : * Return the base URL for all Gmail API calls.
17 : * If the environment variable GMAIL_API_BASE_URL is set and non-empty,
18 : * it overrides the default (useful for pointing at a mock server in tests).
19 : */
20 767 : static const char *gmail_api_base(void) {
21 767 : const char *override = getenv("GMAIL_API_BASE_URL");
22 767 : return (override && override[0]) ? override : GMAIL_API;
23 : }
24 :
25 : /* ── Client struct ────────────────────────────────────────────────── */
26 :
27 : struct GmailClient {
28 : char *access_token;
29 : Config *cfg; /* borrowed, not owned */
30 : GmailProgressFn progress_fn;
31 : void *progress_ctx;
32 : };
33 :
34 : /* ── libcurl write callback ───────────────────────────────────────── */
35 :
36 : typedef struct {
37 : char *data;
38 : size_t len;
39 : size_t cap;
40 : } CurlBuf;
41 :
42 762 : static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) {
43 762 : CurlBuf *buf = userdata;
44 762 : size_t bytes = size * nmemb;
45 762 : if (buf->len + bytes + 1 > buf->cap) {
46 762 : size_t newcap = (buf->cap ? buf->cap * 2 : 4096);
47 779 : while (newcap < buf->len + bytes + 1) newcap *= 2;
48 762 : char *tmp = realloc(buf->data, newcap);
49 762 : if (!tmp) return 0;
50 762 : buf->data = tmp;
51 762 : buf->cap = newcap;
52 : }
53 762 : memcpy(buf->data + buf->len, ptr, bytes);
54 762 : buf->len += bytes;
55 762 : buf->data[buf->len] = '\0';
56 762 : return bytes;
57 : }
58 :
59 : /* ── HTTP helpers ─────────────────────────────────────────────────── */
60 :
61 : /**
62 : * Perform an authenticated GET request.
63 : * Returns heap-allocated response body; sets *http_code.
64 : */
65 714 : static char *api_get(GmailClient *c, const char *url, long *http_code) {
66 714 : CURL *curl = curl_easy_init();
67 714 : if (!curl) return NULL;
68 :
69 714 : CurlBuf buf = {0};
70 : char auth_hdr[2048];
71 714 : snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", c->access_token);
72 :
73 714 : struct curl_slist *headers = NULL;
74 714 : headers = curl_slist_append(headers, auth_hdr);
75 :
76 714 : curl_easy_setopt(curl, CURLOPT_URL, url);
77 714 : curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
78 714 : curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
79 714 : curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
80 714 : curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
81 714 : curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
82 714 : curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
83 :
84 : /* Disable SSL verification when talking to a plain HTTP test server */
85 714 : if (strncmp(url, "http://", 7) == 0) {
86 714 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
87 714 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
88 : }
89 :
90 714 : CURLcode res = curl_easy_perform(curl);
91 714 : curl_slist_free_all(headers);
92 :
93 714 : if (res != CURLE_OK) {
94 3 : logger_log(LOG_ERROR, "gmail: GET %s failed: %s", url, curl_easy_strerror(res));
95 3 : free(buf.data);
96 3 : curl_easy_cleanup(curl);
97 3 : return NULL;
98 : }
99 :
100 711 : curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code);
101 711 : curl_easy_cleanup(curl);
102 711 : return buf.data;
103 : }
104 :
105 : /**
106 : * Perform an authenticated POST with JSON body.
107 : * json_body may be NULL for empty-body POSTs (e.g. trash).
108 : */
109 49 : static char *api_post_json(GmailClient *c, const char *url,
110 : const char *json_body, long *http_code) {
111 49 : CURL *curl = curl_easy_init();
112 49 : if (!curl) return NULL;
113 :
114 49 : CurlBuf buf = {0};
115 : char auth_hdr[2048];
116 49 : snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", c->access_token);
117 :
118 49 : struct curl_slist *headers = NULL;
119 49 : headers = curl_slist_append(headers, auth_hdr);
120 49 : headers = curl_slist_append(headers, "Content-Type: application/json");
121 :
122 49 : curl_easy_setopt(curl, CURLOPT_URL, url);
123 49 : curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
124 49 : curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
125 49 : curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
126 49 : curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
127 49 : curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
128 49 : curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
129 :
130 : /* Disable SSL verification when talking to a plain HTTP test server */
131 49 : if (strncmp(url, "http://", 7) == 0) {
132 49 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
133 49 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
134 : }
135 :
136 49 : if (json_body) {
137 44 : curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);
138 : } else {
139 5 : curl_easy_setopt(curl, CURLOPT_POST, 1L);
140 5 : curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L);
141 : }
142 :
143 49 : CURLcode res = curl_easy_perform(curl);
144 49 : curl_slist_free_all(headers);
145 :
146 49 : if (res != CURLE_OK) {
147 0 : logger_log(LOG_ERROR, "gmail: POST %s failed: %s", url, curl_easy_strerror(res));
148 0 : free(buf.data);
149 0 : curl_easy_cleanup(curl);
150 0 : return NULL;
151 : }
152 :
153 49 : curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code);
154 49 : curl_easy_cleanup(curl);
155 49 : return buf.data;
156 : }
157 :
158 : /**
159 : * Wrapper that auto-retries once on HTTP 401 (token expired).
160 : */
161 714 : static char *api_get_retry(GmailClient *c, const char *url, long *http_code) {
162 714 : char *resp = api_get(c, url, http_code);
163 714 : if (resp && *http_code == 401) {
164 0 : free(resp);
165 0 : char *new_token = gmail_auth_refresh(c->cfg);
166 0 : if (!new_token) return NULL;
167 0 : free(c->access_token);
168 0 : c->access_token = new_token;
169 0 : resp = api_get(c, url, http_code);
170 : }
171 714 : return resp;
172 : }
173 :
174 49 : static char *api_post_retry(GmailClient *c, const char *url,
175 : const char *json_body, long *http_code) {
176 49 : char *resp = api_post_json(c, url, json_body, http_code);
177 49 : if (resp && *http_code == 401) {
178 0 : free(resp);
179 0 : char *new_token = gmail_auth_refresh(c->cfg);
180 0 : if (!new_token) return NULL;
181 0 : free(c->access_token);
182 0 : c->access_token = new_token;
183 0 : resp = api_post_json(c, url, json_body, http_code);
184 : }
185 49 : return resp;
186 : }
187 :
188 : /* ── Base64url decode ─────────────────────────────────────────────── */
189 :
190 : static const signed char b64url_table[256] = {
191 : ['A']=0, ['B']=1, ['C']=2, ['D']=3, ['E']=4, ['F']=5,
192 : ['G']=6, ['H']=7, ['I']=8, ['J']=9, ['K']=10, ['L']=11,
193 : ['M']=12, ['N']=13, ['O']=14, ['P']=15, ['Q']=16, ['R']=17,
194 : ['S']=18, ['T']=19, ['U']=20, ['V']=21, ['W']=22, ['X']=23,
195 : ['Y']=24, ['Z']=25,
196 : ['a']=0+26, ['b']=1+26, ['c']=2+26, ['d']=3+26, ['e']=4+26,
197 : ['f']=5+26, ['g']=6+26, ['h']=7+26, ['i']=8+26, ['j']=9+26,
198 : ['k']=10+26,['l']=11+26,['m']=12+26,['n']=13+26,['o']=14+26,
199 : ['p']=15+26,['q']=16+26,['r']=17+26,['s']=18+26,['t']=19+26,
200 : ['u']=20+26,['v']=21+26,['w']=22+26,['x']=23+26,['y']=24+26,
201 : ['z']=25+26,
202 : ['0']=52, ['1']=53, ['2']=54, ['3']=55, ['4']=56,
203 : ['5']=57, ['6']=58, ['7']=59, ['8']=60, ['9']=61,
204 : ['-']=62, ['_']=63,
205 : };
206 :
207 : /**
208 : * Decode a base64url string (no padding) into a binary buffer.
209 : * Returns heap-allocated NUL-terminated buffer; sets *out_len.
210 : */
211 624 : char *gmail_base64url_decode(const char *input, size_t in_len, size_t *out_len) {
212 624 : size_t alloc = (in_len / 4 + 1) * 3 + 1;
213 624 : char *out = malloc(alloc);
214 624 : if (!out) return NULL;
215 :
216 624 : size_t o = 0;
217 624 : unsigned int acc = 0;
218 624 : int bits = 0;
219 :
220 196574 : for (size_t i = 0; i < in_len; i++) {
221 195950 : unsigned char ch = (unsigned char)input[i];
222 195950 : if (ch == '=' || ch == '\n' || ch == '\r' || ch == ' ') continue;
223 195950 : int val = b64url_table[ch];
224 : /* Non-base64url chars have value 0 in the table; 'A' is also 0.
225 : * For safety, skip obvious non-alphabet chars. */
226 195950 : if (val == 0 && ch != 'A') continue;
227 195950 : acc = (acc << 6) | (unsigned int)val;
228 195950 : bits += 6;
229 195950 : if (bits >= 8) {
230 146859 : bits -= 8;
231 146859 : out[o++] = (char)((acc >> bits) & 0xFF);
232 : }
233 : }
234 :
235 624 : out[o] = '\0';
236 624 : if (out_len) *out_len = o;
237 624 : return out;
238 : }
239 :
240 : /* ── Base64url encode ─────────────────────────────────────────────── */
241 :
242 : static const char b64url_chars[] =
243 : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
244 :
245 : /**
246 : * Encode binary data as base64url (no padding).
247 : * Returns heap-allocated NUL-terminated string.
248 : */
249 7 : char *gmail_base64url_encode(const unsigned char *data, size_t len) {
250 7 : size_t alloc = ((len + 2) / 3) * 4 + 1;
251 7 : char *out = malloc(alloc);
252 7 : if (!out) return NULL;
253 :
254 7 : size_t o = 0;
255 143 : for (size_t i = 0; i < len; i += 3) {
256 136 : unsigned int n = ((unsigned int)data[i]) << 16;
257 136 : if (i + 1 < len) n |= ((unsigned int)data[i + 1]) << 8;
258 136 : if (i + 2 < len) n |= ((unsigned int)data[i + 2]);
259 :
260 136 : out[o++] = b64url_chars[(n >> 18) & 0x3F];
261 136 : out[o++] = b64url_chars[(n >> 12) & 0x3F];
262 136 : if (i + 1 < len) out[o++] = b64url_chars[(n >> 6) & 0x3F];
263 136 : if (i + 2 < len) out[o++] = b64url_chars[n & 0x3F];
264 : }
265 7 : out[o] = '\0';
266 7 : return out;
267 : }
268 :
269 : /* ── Connect / Disconnect ─────────────────────────────────────────── */
270 :
271 142 : GmailClient *gmail_connect(Config *cfg) {
272 142 : if (!cfg || !cfg->gmail_mode) {
273 1 : logger_log(LOG_ERROR, "gmail_connect: not a Gmail account");
274 1 : return NULL;
275 : }
276 :
277 141 : char *token = gmail_auth_refresh(cfg);
278 141 : if (!token) {
279 8 : logger_log(LOG_ERROR, "gmail_connect: failed to obtain access token");
280 8 : return NULL;
281 : }
282 :
283 133 : GmailClient *c = calloc(1, sizeof(*c));
284 133 : if (!c) { free(token); return NULL; }
285 133 : c->access_token = token;
286 133 : c->cfg = cfg;
287 133 : logger_log(LOG_DEBUG, "gmail_connect: connected for %s", cfg->user ? cfg->user : "(unknown)");
288 133 : return c;
289 : }
290 :
291 134 : void gmail_disconnect(GmailClient *c) {
292 134 : if (!c) return;
293 133 : free(c->access_token);
294 133 : free(c);
295 : }
296 :
297 50 : void gmail_set_progress(GmailClient *c, GmailProgressFn fn, void *ctx) {
298 50 : if (!c) return;
299 49 : c->progress_fn = fn;
300 49 : c->progress_ctx = ctx;
301 : }
302 :
303 : /* ── DELETE helper ────────────────────────────────────────────────── */
304 :
305 : /**
306 : * Perform an authenticated DELETE request.
307 : * Absorbs the response body (usually empty for 204 responses).
308 : */
309 4 : static char *api_delete(GmailClient *c, const char *url, long *http_code) {
310 4 : CURL *curl = curl_easy_init();
311 4 : if (!curl) return NULL;
312 :
313 4 : CurlBuf buf = {0};
314 : char auth_hdr[2048];
315 4 : snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", c->access_token);
316 :
317 4 : struct curl_slist *headers = NULL;
318 4 : headers = curl_slist_append(headers, auth_hdr);
319 :
320 4 : curl_easy_setopt(curl, CURLOPT_URL, url);
321 4 : curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
322 4 : curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
323 4 : curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
324 4 : curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
325 4 : curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
326 4 : curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
327 4 : curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
328 :
329 : /* Disable SSL verification when talking to a plain HTTP test server */
330 4 : if (strncmp(url, "http://", 7) == 0) {
331 4 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
332 4 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
333 : }
334 :
335 4 : CURLcode res = curl_easy_perform(curl);
336 4 : curl_slist_free_all(headers);
337 :
338 4 : if (res != CURLE_OK) {
339 0 : logger_log(LOG_ERROR, "gmail: DELETE %s failed: %s", url, curl_easy_strerror(res));
340 0 : free(buf.data);
341 0 : curl_easy_cleanup(curl);
342 0 : return NULL;
343 : }
344 :
345 4 : curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code);
346 4 : curl_easy_cleanup(curl);
347 : /* Return an empty string (not NULL) so caller can distinguish curl error from HTTP error */
348 4 : if (!buf.data) {
349 2 : char *empty = malloc(1);
350 2 : if (empty) *empty = '\0';
351 2 : return empty;
352 : }
353 2 : return buf.data;
354 : }
355 :
356 4 : static char *api_delete_retry(GmailClient *c, const char *url, long *http_code) {
357 4 : char *resp = api_delete(c, url, http_code);
358 4 : if (resp && *http_code == 401) {
359 0 : free(resp);
360 0 : char *new_token = gmail_auth_refresh(c->cfg);
361 0 : if (!new_token) return NULL;
362 0 : free(c->access_token);
363 0 : c->access_token = new_token;
364 0 : resp = api_delete(c, url, http_code);
365 : }
366 4 : return resp;
367 : }
368 :
369 : /* ── Create / delete label ────────────────────────────────────────── */
370 :
371 5 : int gmail_create_label(GmailClient *c, const char *name, char **id_out) {
372 5 : if (id_out) *id_out = NULL;
373 :
374 5 : RAII_STRING char *url = NULL;
375 5 : if (asprintf(&url, "%s/labels", gmail_api_base()) == -1) return -1;
376 :
377 : char body[1024];
378 5 : snprintf(body, sizeof(body), "{\"name\":\"%s\"}", name);
379 :
380 5 : long code = 0;
381 10 : RAII_STRING char *resp = api_post_retry(c, url, body, &code);
382 5 : if (!resp || code != 200) {
383 1 : logger_log(LOG_ERROR, "gmail_create_label: HTTP %ld", code);
384 1 : return -1;
385 : }
386 :
387 4 : if (id_out) {
388 3 : *id_out = json_get_string(resp, "id");
389 : }
390 4 : return 0;
391 : }
392 :
393 4 : int gmail_delete_label(GmailClient *c, const char *label_id) {
394 4 : RAII_STRING char *url = NULL;
395 4 : if (asprintf(&url, "%s/labels/%s", gmail_api_base(), label_id) == -1) return -1;
396 :
397 4 : long code = 0;
398 8 : RAII_STRING char *resp = api_delete_retry(c, url, &code);
399 4 : if (!resp) {
400 0 : logger_log(LOG_ERROR, "gmail_delete_label: curl error for label %s", label_id);
401 0 : return -1;
402 : }
403 4 : if (code != 204 && code != 200) {
404 1 : logger_log(LOG_ERROR, "gmail_delete_label: HTTP %ld for label %s", code, label_id);
405 1 : return -1;
406 : }
407 3 : return 0;
408 : }
409 :
410 : /* ── List labels ──────────────────────────────────────────────────── */
411 :
412 : struct label_ctx {
413 : char **names;
414 : char **ids;
415 : int count;
416 : int cap;
417 : };
418 :
419 223 : static void collect_label(const char *obj, int index, void *ctx) {
420 : (void)index;
421 223 : struct label_ctx *lc = ctx;
422 223 : char *name = json_get_string(obj, "name");
423 223 : char *id = json_get_string(obj, "id");
424 223 : if (!name || !id) { free(name); free(id); return; }
425 :
426 223 : if (lc->count == lc->cap) {
427 32 : int newcap = lc->cap ? lc->cap * 2 : 32;
428 32 : char **nn = realloc(lc->names, (size_t)newcap * sizeof(char *));
429 32 : char **ni = realloc(lc->ids, (size_t)newcap * sizeof(char *));
430 32 : if (!nn || !ni) { free(name); free(id); free(nn); return; }
431 32 : lc->names = nn;
432 32 : lc->ids = ni;
433 32 : lc->cap = newcap;
434 : }
435 223 : lc->names[lc->count] = name;
436 223 : lc->ids[lc->count] = id;
437 223 : lc->count++;
438 : }
439 :
440 35 : int gmail_list_labels(GmailClient *c, char ***names_out,
441 : char ***ids_out, int *count_out) {
442 35 : *names_out = NULL;
443 35 : *ids_out = NULL;
444 35 : *count_out = 0;
445 :
446 35 : RAII_STRING char *url = NULL;
447 35 : if (asprintf(&url, "%s/labels", gmail_api_base()) == -1) return -1;
448 :
449 35 : long code = 0;
450 70 : RAII_STRING char *resp = api_get_retry(c, url, &code);
451 35 : if (!resp || code != 200) {
452 3 : logger_log(LOG_ERROR, "gmail_list_labels: HTTP %ld", code);
453 3 : return -1;
454 : }
455 :
456 32 : struct label_ctx lc = {0};
457 32 : json_foreach_object(resp, "labels", collect_label, &lc);
458 :
459 32 : *names_out = lc.names;
460 32 : *ids_out = lc.ids;
461 32 : *count_out = lc.count;
462 32 : return 0;
463 : }
464 :
465 : /* ── List messages ────────────────────────────────────────────────── */
466 :
467 : struct msg_id_ctx {
468 : char (*uids)[17];
469 : int count;
470 : int cap;
471 : };
472 :
473 1838 : static void collect_msg_id(const char *obj, int index, void *ctx) {
474 : (void)index;
475 1838 : struct msg_id_ctx *mc = ctx;
476 1838 : char *id = json_get_string(obj, "id");
477 1838 : if (!id) return;
478 :
479 1838 : if (mc->count == mc->cap) {
480 28 : int newcap = mc->cap ? mc->cap * 2 : 256;
481 28 : char (*tmp)[17] = realloc(mc->uids, (size_t)newcap * sizeof(char[17]));
482 28 : if (!tmp) { free(id); return; }
483 28 : mc->uids = tmp;
484 28 : mc->cap = newcap;
485 : }
486 1838 : snprintf(mc->uids[mc->count], 17, "%s", id);
487 1838 : mc->count++;
488 1838 : free(id);
489 : }
490 :
491 32 : int gmail_list_messages(GmailClient *c, const char *label_id,
492 : const char *query,
493 : char (**uids_out)[17], int *count_out,
494 : char **history_id_out) {
495 32 : *uids_out = NULL;
496 32 : *count_out = 0;
497 32 : if (history_id_out) *history_id_out = NULL;
498 :
499 32 : struct msg_id_ctx mc = {0};
500 32 : char *page_token = NULL;
501 :
502 9 : for (;;) {
503 : /* Build URL with optional query parameters */
504 : char url_buf[2048];
505 41 : int n = snprintf(url_buf, sizeof(url_buf), "%s/messages?maxResults=500", gmail_api_base());
506 41 : if (label_id)
507 5 : n += snprintf(url_buf + n, sizeof(url_buf) - (size_t)n, "&labelIds=%s", label_id);
508 41 : if (query)
509 3 : n += snprintf(url_buf + n, sizeof(url_buf) - (size_t)n, "&q=%s", query);
510 41 : if (page_token)
511 9 : n += snprintf(url_buf + n, sizeof(url_buf) - (size_t)n, "&pageToken=%s", page_token);
512 41 : free(page_token);
513 41 : page_token = NULL;
514 : (void)n;
515 :
516 41 : long code = 0;
517 41 : char *resp = api_get_retry(c, url_buf, &code);
518 41 : if (!resp || code != 200) {
519 3 : free(resp);
520 3 : break;
521 : }
522 :
523 38 : json_foreach_object(resp, "messages", collect_msg_id, &mc);
524 :
525 38 : if (history_id_out) {
526 33 : char *hid = json_get_string(resp, "historyId");
527 33 : if (hid) {
528 32 : free(*history_id_out);
529 32 : *history_id_out = hid;
530 : }
531 : }
532 :
533 38 : page_token = json_get_string(resp, "nextPageToken");
534 38 : free(resp);
535 :
536 38 : if (c->progress_fn)
537 31 : c->progress_fn((size_t)mc.count, 0, c->progress_ctx);
538 :
539 38 : if (!page_token) break;
540 : }
541 32 : free(page_token);
542 :
543 32 : *uids_out = mc.uids;
544 32 : *count_out = mc.count;
545 32 : return mc.uids ? 0 : (mc.count == 0 ? 0 : -1);
546 : }
547 :
548 : /* ── Fetch message (raw + labels) ─────────────────────────────────── */
549 :
550 622 : char *gmail_fetch_message(GmailClient *c, const char *uid,
551 : char ***labels_out, int *label_count_out) {
552 622 : if (labels_out) *labels_out = NULL;
553 622 : if (label_count_out) *label_count_out = 0;
554 :
555 622 : RAII_STRING char *url = NULL;
556 622 : if (asprintf(&url, "%s/messages/%s?format=raw", gmail_api_base(), uid) == -1)
557 0 : return NULL;
558 :
559 622 : long code = 0;
560 1244 : RAII_STRING char *resp = api_get_retry(c, url, &code);
561 622 : if (!resp || code != 200) {
562 2 : if (code == 404) {
563 2 : logger_log(LOG_WARN, "gmail: message %s not found (deleted?)", uid);
564 : } else {
565 0 : logger_log(LOG_ERROR, "gmail_fetch_message %s: HTTP %ld", uid, code);
566 : }
567 2 : return NULL;
568 : }
569 :
570 : /* Extract and decode raw message */
571 1240 : RAII_STRING char *raw_b64 = json_get_string(resp, "raw");
572 620 : if (!raw_b64) {
573 1 : logger_log(LOG_ERROR, "gmail_fetch_message %s: no 'raw' field", uid);
574 1 : return NULL;
575 : }
576 :
577 619 : size_t decoded_len = 0;
578 619 : char *decoded = gmail_base64url_decode(raw_b64, strlen(raw_b64), &decoded_len);
579 619 : if (!decoded) return NULL;
580 :
581 : /* Extract labels if requested */
582 619 : if (labels_out && label_count_out) {
583 610 : json_get_string_array(resp, "labelIds", labels_out, label_count_out);
584 : }
585 :
586 619 : return decoded;
587 : }
588 :
589 : /* ── Modify labels ────────────────────────────────────────────────── */
590 :
591 36 : int gmail_modify_labels(GmailClient *c, const char *uid,
592 : const char **add_labels, int add_count,
593 : const char **remove_labels, int remove_count) {
594 36 : RAII_STRING char *url = NULL;
595 36 : if (asprintf(&url, "%s/messages/%s/modify", gmail_api_base(), uid) == -1)
596 0 : return -1;
597 :
598 : /* Build JSON body */
599 : /* Worst case: each label ~64 chars + quotes + commas + structure */
600 36 : size_t body_cap = 256 + (size_t)(add_count + remove_count) * 80;
601 36 : char *body = malloc(body_cap);
602 36 : if (!body) return -1;
603 :
604 36 : size_t off = 0;
605 36 : off += (size_t)snprintf(body + off, body_cap - off, "{");
606 :
607 36 : if (add_count > 0) {
608 21 : off += (size_t)snprintf(body + off, body_cap - off, "\"addLabelIds\":[");
609 42 : for (int i = 0; i < add_count; i++) {
610 21 : if (i > 0) off += (size_t)snprintf(body + off, body_cap - off, ",");
611 21 : off += (size_t)snprintf(body + off, body_cap - off, "\"%s\"", add_labels[i]);
612 : }
613 21 : off += (size_t)snprintf(body + off, body_cap - off, "]");
614 : }
615 :
616 36 : if (remove_count > 0) {
617 20 : if (add_count > 0) off += (size_t)snprintf(body + off, body_cap - off, ",");
618 20 : off += (size_t)snprintf(body + off, body_cap - off, "\"removeLabelIds\":[");
619 40 : for (int i = 0; i < remove_count; i++) {
620 20 : if (i > 0) off += (size_t)snprintf(body + off, body_cap - off, ",");
621 20 : off += (size_t)snprintf(body + off, body_cap - off, "\"%s\"", remove_labels[i]);
622 : }
623 20 : off += (size_t)snprintf(body + off, body_cap - off, "]");
624 : }
625 :
626 36 : snprintf(body + off, body_cap - off, "}");
627 :
628 36 : long code = 0;
629 36 : char *resp = api_post_retry(c, url, body, &code);
630 36 : free(body);
631 36 : free(resp);
632 :
633 36 : if (code != 200) {
634 1 : logger_log(LOG_ERROR, "gmail_modify_labels %s: HTTP %ld", uid, code);
635 1 : return -1;
636 : }
637 35 : return 0;
638 : }
639 :
640 : /* ── Trash / Untrash ──────────────────────────────────────────────── */
641 :
642 3 : int gmail_trash(GmailClient *c, const char *uid) {
643 3 : RAII_STRING char *url = NULL;
644 3 : if (asprintf(&url, "%s/messages/%s/trash", gmail_api_base(), uid) == -1)
645 0 : return -1;
646 :
647 3 : long code = 0;
648 6 : RAII_STRING char *resp = api_post_retry(c, url, NULL, &code);
649 3 : if (code != 200) {
650 1 : logger_log(LOG_ERROR, "gmail_trash %s: HTTP %ld", uid, code);
651 1 : return -1;
652 : }
653 2 : return 0;
654 : }
655 :
656 2 : int gmail_untrash(GmailClient *c, const char *uid) {
657 2 : RAII_STRING char *url = NULL;
658 2 : if (asprintf(&url, "%s/messages/%s/untrash", gmail_api_base(), uid) == -1)
659 0 : return -1;
660 :
661 2 : long code = 0;
662 4 : RAII_STRING char *resp = api_post_retry(c, url, NULL, &code);
663 2 : if (code != 200) {
664 1 : logger_log(LOG_ERROR, "gmail_untrash %s: HTTP %ld", uid, code);
665 1 : return -1;
666 : }
667 1 : return 0;
668 : }
669 :
670 : /* ── Send ─────────────────────────────────────────────────────────── */
671 :
672 3 : int gmail_send(GmailClient *c, const char *raw_msg, size_t len) {
673 3 : RAII_STRING char *url = NULL;
674 3 : if (asprintf(&url, "%s/messages/send", gmail_api_base()) == -1)
675 0 : return -1;
676 :
677 : /* Base64url encode the raw RFC 2822 message */
678 3 : char *encoded = gmail_base64url_encode((const unsigned char *)raw_msg, len);
679 3 : if (!encoded) return -1;
680 :
681 : /* Build JSON body: {"raw": "<base64url>"} */
682 3 : size_t body_len = strlen(encoded) + 32;
683 3 : char *body = malloc(body_len);
684 3 : if (!body) { free(encoded); return -1; }
685 3 : snprintf(body, body_len, "{\"raw\":\"%s\"}", encoded);
686 3 : free(encoded);
687 :
688 3 : long code = 0;
689 3 : char *resp = api_post_retry(c, url, body, &code);
690 3 : free(body);
691 3 : free(resp);
692 :
693 3 : if (code != 200) {
694 1 : logger_log(LOG_ERROR, "gmail_send: HTTP %ld", code);
695 1 : return -1;
696 : }
697 2 : logger_log(LOG_INFO, "gmail_send: message sent successfully");
698 2 : return 0;
699 : }
700 :
701 : /* ── Profile (historyId) ──────────────────────────────────────────── */
702 :
703 5 : char *gmail_get_history_id(GmailClient *c) {
704 5 : RAII_STRING char *url = NULL;
705 5 : if (asprintf(&url, "%s/profile", gmail_api_base()) == -1) return NULL;
706 :
707 5 : long code = 0;
708 10 : RAII_STRING char *resp = api_get_retry(c, url, &code);
709 5 : if (!resp || code != 200) return NULL;
710 :
711 2 : return json_get_string(resp, "historyId");
712 : }
713 :
714 : /* ── History (incremental sync) ───────────────────────────────────── */
715 :
716 11 : char *gmail_get_history(GmailClient *c, const char *history_id) {
717 11 : RAII_STRING char *url = NULL;
718 11 : if (asprintf(&url, "%s/history?startHistoryId=%s"
719 : "&historyTypes=messageAdded"
720 : "&historyTypes=messageDeleted"
721 : "&historyTypes=labelAdded"
722 : "&historyTypes=labelRemoved",
723 : gmail_api_base(), history_id) == -1)
724 0 : return NULL;
725 :
726 11 : long code = 0;
727 11 : char *resp = api_get_retry(c, url, &code);
728 11 : if (!resp) return NULL;
729 :
730 11 : if (code == 404) {
731 4 : fprintf(stderr, " History API: 404 — historyId %s expired.\n", history_id);
732 4 : logger_log(LOG_WARN, "gmail: history %s expired — full resync needed", history_id);
733 4 : free(resp);
734 4 : return NULL;
735 : }
736 7 : if (code != 200) {
737 1 : fprintf(stderr, " History API: HTTP %ld for historyId %s.\n", code, history_id);
738 1 : logger_log(LOG_WARN, "gmail_get_history: HTTP %ld (will retry with full sync)", code);
739 1 : free(resp);
740 1 : return NULL;
741 : }
742 6 : fprintf(stderr, " History API: OK (historyId %s accepted).\n", history_id);
743 :
744 6 : return resp;
745 : }
|