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 687 : static const char *gmail_api_base(void) {
21 687 : const char *override = getenv("GMAIL_API_BASE_URL");
22 687 : 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 687 : static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) {
43 687 : CurlBuf *buf = userdata;
44 687 : size_t bytes = size * nmemb;
45 687 : if (buf->len + bytes + 1 > buf->cap) {
46 687 : size_t newcap = (buf->cap ? buf->cap * 2 : 4096);
47 704 : while (newcap < buf->len + bytes + 1) newcap *= 2;
48 687 : char *tmp = realloc(buf->data, newcap);
49 687 : if (!tmp) return 0;
50 687 : buf->data = tmp;
51 687 : buf->cap = newcap;
52 : }
53 687 : memcpy(buf->data + buf->len, ptr, bytes);
54 687 : buf->len += bytes;
55 687 : buf->data[buf->len] = '\0';
56 687 : 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 661 : static char *api_get(GmailClient *c, const char *url, long *http_code) {
66 661 : CURL *curl = curl_easy_init();
67 661 : if (!curl) return NULL;
68 :
69 661 : CurlBuf buf = {0};
70 : char auth_hdr[2048];
71 661 : snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", c->access_token);
72 :
73 661 : struct curl_slist *headers = NULL;
74 661 : headers = curl_slist_append(headers, auth_hdr);
75 :
76 661 : curl_easy_setopt(curl, CURLOPT_URL, url);
77 661 : curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
78 661 : curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
79 661 : curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
80 661 : curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
81 661 : curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
82 661 : curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
83 :
84 : /* Disable SSL verification when talking to a plain HTTP test server */
85 661 : if (strncmp(url, "http://", 7) == 0) {
86 661 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
87 661 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
88 : }
89 :
90 661 : CURLcode res = curl_easy_perform(curl);
91 661 : curl_slist_free_all(headers);
92 :
93 661 : if (res != CURLE_OK) {
94 0 : logger_log(LOG_ERROR, "gmail: GET %s failed: %s", url, curl_easy_strerror(res));
95 0 : free(buf.data);
96 0 : curl_easy_cleanup(curl);
97 0 : return NULL;
98 : }
99 :
100 661 : curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code);
101 661 : curl_easy_cleanup(curl);
102 661 : 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 25 : static char *api_post_json(GmailClient *c, const char *url,
110 : const char *json_body, long *http_code) {
111 25 : CURL *curl = curl_easy_init();
112 25 : if (!curl) return NULL;
113 :
114 25 : CurlBuf buf = {0};
115 : char auth_hdr[2048];
116 25 : snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", c->access_token);
117 :
118 25 : struct curl_slist *headers = NULL;
119 25 : headers = curl_slist_append(headers, auth_hdr);
120 25 : headers = curl_slist_append(headers, "Content-Type: application/json");
121 :
122 25 : curl_easy_setopt(curl, CURLOPT_URL, url);
123 25 : curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
124 25 : curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
125 25 : curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
126 25 : curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
127 25 : curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
128 25 : curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
129 :
130 : /* Disable SSL verification when talking to a plain HTTP test server */
131 25 : if (strncmp(url, "http://", 7) == 0) {
132 25 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
133 25 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
134 : }
135 :
136 25 : if (json_body) {
137 25 : curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);
138 : } else {
139 0 : curl_easy_setopt(curl, CURLOPT_POST, 1L);
140 0 : curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L);
141 : }
142 :
143 25 : CURLcode res = curl_easy_perform(curl);
144 25 : curl_slist_free_all(headers);
145 :
146 25 : 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 25 : curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code);
154 25 : curl_easy_cleanup(curl);
155 25 : return buf.data;
156 : }
157 :
158 : /**
159 : * Wrapper that auto-retries once on HTTP 401 (token expired).
160 : */
161 661 : static char *api_get_retry(GmailClient *c, const char *url, long *http_code) {
162 661 : char *resp = api_get(c, url, http_code);
163 661 : 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 661 : return resp;
172 : }
173 :
174 25 : static char *api_post_retry(GmailClient *c, const char *url,
175 : const char *json_body, long *http_code) {
176 25 : char *resp = api_post_json(c, url, json_body, http_code);
177 25 : 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 25 : 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 606 : char *gmail_base64url_decode(const char *input, size_t in_len, size_t *out_len) {
212 606 : size_t alloc = (in_len / 4 + 1) * 3 + 1;
213 606 : char *out = malloc(alloc);
214 606 : if (!out) return NULL;
215 :
216 606 : size_t o = 0;
217 606 : unsigned int acc = 0;
218 606 : int bits = 0;
219 :
220 194059 : for (size_t i = 0; i < in_len; i++) {
221 193453 : unsigned char ch = (unsigned char)input[i];
222 193453 : if (ch == '=' || ch == '\n' || ch == '\r' || ch == ' ') continue;
223 193453 : 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 193453 : if (val == 0 && ch != 'A') continue;
227 193453 : acc = (acc << 6) | (unsigned int)val;
228 193453 : bits += 6;
229 193453 : if (bits >= 8) {
230 144990 : bits -= 8;
231 144990 : out[o++] = (char)((acc >> bits) & 0xFF);
232 : }
233 : }
234 :
235 606 : out[o] = '\0';
236 606 : if (out_len) *out_len = o;
237 606 : 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 0 : char *gmail_base64url_encode(const unsigned char *data, size_t len) {
250 0 : size_t alloc = ((len + 2) / 3) * 4 + 1;
251 0 : char *out = malloc(alloc);
252 0 : if (!out) return NULL;
253 :
254 0 : size_t o = 0;
255 0 : for (size_t i = 0; i < len; i += 3) {
256 0 : unsigned int n = ((unsigned int)data[i]) << 16;
257 0 : if (i + 1 < len) n |= ((unsigned int)data[i + 1]) << 8;
258 0 : if (i + 2 < len) n |= ((unsigned int)data[i + 2]);
259 :
260 0 : out[o++] = b64url_chars[(n >> 18) & 0x3F];
261 0 : out[o++] = b64url_chars[(n >> 12) & 0x3F];
262 0 : if (i + 1 < len) out[o++] = b64url_chars[(n >> 6) & 0x3F];
263 0 : if (i + 2 < len) out[o++] = b64url_chars[n & 0x3F];
264 : }
265 0 : out[o] = '\0';
266 0 : return out;
267 : }
268 :
269 : /* ── Connect / Disconnect ─────────────────────────────────────────── */
270 :
271 57 : GmailClient *gmail_connect(Config *cfg) {
272 57 : if (!cfg || !cfg->gmail_mode) {
273 0 : logger_log(LOG_ERROR, "gmail_connect: not a Gmail account");
274 0 : return NULL;
275 : }
276 :
277 57 : char *token = gmail_auth_refresh(cfg);
278 57 : if (!token) {
279 0 : logger_log(LOG_ERROR, "gmail_connect: failed to obtain access token");
280 0 : return NULL;
281 : }
282 :
283 57 : GmailClient *c = calloc(1, sizeof(*c));
284 57 : if (!c) { free(token); return NULL; }
285 57 : c->access_token = token;
286 57 : c->cfg = cfg;
287 57 : logger_log(LOG_DEBUG, "gmail_connect: connected for %s", cfg->user ? cfg->user : "(unknown)");
288 57 : return c;
289 : }
290 :
291 57 : void gmail_disconnect(GmailClient *c) {
292 57 : if (!c) return;
293 57 : free(c->access_token);
294 57 : free(c);
295 : }
296 :
297 36 : void gmail_set_progress(GmailClient *c, GmailProgressFn fn, void *ctx) {
298 36 : if (!c) return;
299 36 : c->progress_fn = fn;
300 36 : 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 1 : static char *api_delete(GmailClient *c, const char *url, long *http_code) {
310 1 : CURL *curl = curl_easy_init();
311 1 : if (!curl) return NULL;
312 :
313 1 : CurlBuf buf = {0};
314 : char auth_hdr[2048];
315 1 : snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", c->access_token);
316 :
317 1 : struct curl_slist *headers = NULL;
318 1 : headers = curl_slist_append(headers, auth_hdr);
319 :
320 1 : curl_easy_setopt(curl, CURLOPT_URL, url);
321 1 : curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
322 1 : curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
323 1 : curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
324 1 : curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
325 1 : curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
326 1 : curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
327 1 : curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
328 :
329 : /* Disable SSL verification when talking to a plain HTTP test server */
330 1 : if (strncmp(url, "http://", 7) == 0) {
331 1 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
332 1 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
333 : }
334 :
335 1 : CURLcode res = curl_easy_perform(curl);
336 1 : curl_slist_free_all(headers);
337 :
338 1 : 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 1 : curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code);
346 1 : curl_easy_cleanup(curl);
347 : /* Return an empty string (not NULL) so caller can distinguish curl error from HTTP error */
348 1 : if (!buf.data) {
349 0 : char *empty = malloc(1);
350 0 : if (empty) *empty = '\0';
351 0 : return empty;
352 : }
353 1 : return buf.data;
354 : }
355 :
356 1 : static char *api_delete_retry(GmailClient *c, const char *url, long *http_code) {
357 1 : char *resp = api_delete(c, url, http_code);
358 1 : 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 1 : return resp;
367 : }
368 :
369 : /* ── Create / delete label ────────────────────────────────────────── */
370 :
371 1 : int gmail_create_label(GmailClient *c, const char *name, char **id_out) {
372 1 : if (id_out) *id_out = NULL;
373 :
374 1 : RAII_STRING char *url = NULL;
375 1 : if (asprintf(&url, "%s/labels", gmail_api_base()) == -1) return -1;
376 :
377 : char body[1024];
378 1 : snprintf(body, sizeof(body), "{\"name\":\"%s\"}", name);
379 :
380 1 : long code = 0;
381 2 : RAII_STRING char *resp = api_post_retry(c, url, body, &code);
382 1 : if (!resp || code != 200) {
383 0 : logger_log(LOG_ERROR, "gmail_create_label: HTTP %ld", code);
384 0 : return -1;
385 : }
386 :
387 1 : if (id_out) {
388 1 : *id_out = json_get_string(resp, "id");
389 : }
390 1 : return 0;
391 : }
392 :
393 1 : int gmail_delete_label(GmailClient *c, const char *label_id) {
394 1 : RAII_STRING char *url = NULL;
395 1 : if (asprintf(&url, "%s/labels/%s", gmail_api_base(), label_id) == -1) return -1;
396 :
397 1 : long code = 0;
398 2 : RAII_STRING char *resp = api_delete_retry(c, url, &code);
399 1 : if (!resp) {
400 0 : logger_log(LOG_ERROR, "gmail_delete_label: curl error for label %s", label_id);
401 0 : return -1;
402 : }
403 1 : if (code != 204 && code != 200) {
404 0 : logger_log(LOG_ERROR, "gmail_delete_label: HTTP %ld for label %s", code, label_id);
405 0 : return -1;
406 : }
407 1 : 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 198 : static void collect_label(const char *obj, int index, void *ctx) {
420 : (void)index;
421 198 : struct label_ctx *lc = ctx;
422 198 : char *name = json_get_string(obj, "name");
423 198 : char *id = json_get_string(obj, "id");
424 198 : if (!name || !id) { free(name); free(id); return; }
425 :
426 198 : if (lc->count == lc->cap) {
427 22 : int newcap = lc->cap ? lc->cap * 2 : 32;
428 22 : char **nn = realloc(lc->names, (size_t)newcap * sizeof(char *));
429 22 : char **ni = realloc(lc->ids, (size_t)newcap * sizeof(char *));
430 22 : if (!nn || !ni) { free(name); free(id); free(nn); return; }
431 22 : lc->names = nn;
432 22 : lc->ids = ni;
433 22 : lc->cap = newcap;
434 : }
435 198 : lc->names[lc->count] = name;
436 198 : lc->ids[lc->count] = id;
437 198 : lc->count++;
438 : }
439 :
440 22 : int gmail_list_labels(GmailClient *c, char ***names_out,
441 : char ***ids_out, int *count_out) {
442 22 : *names_out = NULL;
443 22 : *ids_out = NULL;
444 22 : *count_out = 0;
445 :
446 22 : RAII_STRING char *url = NULL;
447 22 : if (asprintf(&url, "%s/labels", gmail_api_base()) == -1) return -1;
448 :
449 22 : long code = 0;
450 44 : RAII_STRING char *resp = api_get_retry(c, url, &code);
451 22 : if (!resp || code != 200) {
452 0 : logger_log(LOG_ERROR, "gmail_list_labels: HTTP %ld", code);
453 0 : return -1;
454 : }
455 :
456 22 : struct label_ctx lc = {0};
457 22 : json_foreach_object(resp, "labels", collect_label, &lc);
458 :
459 22 : *names_out = lc.names;
460 22 : *ids_out = lc.ids;
461 22 : *count_out = lc.count;
462 22 : 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 1818 : static void collect_msg_id(const char *obj, int index, void *ctx) {
474 : (void)index;
475 1818 : struct msg_id_ctx *mc = ctx;
476 1818 : char *id = json_get_string(obj, "id");
477 1818 : if (!id) return;
478 :
479 1818 : if (mc->count == mc->cap) {
480 18 : int newcap = mc->cap ? mc->cap * 2 : 256;
481 18 : char (*tmp)[17] = realloc(mc->uids, (size_t)newcap * sizeof(char[17]));
482 18 : if (!tmp) { free(id); return; }
483 18 : mc->uids = tmp;
484 18 : mc->cap = newcap;
485 : }
486 1818 : snprintf(mc->uids[mc->count], 17, "%s", id);
487 1818 : mc->count++;
488 1818 : free(id);
489 : }
490 :
491 18 : 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 18 : *uids_out = NULL;
496 18 : *count_out = 0;
497 18 : if (history_id_out) *history_id_out = NULL;
498 :
499 18 : struct msg_id_ctx mc = {0};
500 18 : char *page_token = NULL;
501 :
502 9 : for (;;) {
503 : /* Build URL with optional query parameters */
504 : char url_buf[2048];
505 27 : int n = snprintf(url_buf, sizeof(url_buf), "%s/messages?maxResults=500", gmail_api_base());
506 27 : if (label_id)
507 0 : n += snprintf(url_buf + n, sizeof(url_buf) - (size_t)n, "&labelIds=%s", label_id);
508 27 : if (query)
509 0 : n += snprintf(url_buf + n, sizeof(url_buf) - (size_t)n, "&q=%s", query);
510 27 : if (page_token)
511 9 : n += snprintf(url_buf + n, sizeof(url_buf) - (size_t)n, "&pageToken=%s", page_token);
512 27 : free(page_token);
513 27 : page_token = NULL;
514 : (void)n;
515 :
516 27 : long code = 0;
517 27 : char *resp = api_get_retry(c, url_buf, &code);
518 27 : if (!resp || code != 200) {
519 0 : free(resp);
520 0 : break;
521 : }
522 :
523 27 : json_foreach_object(resp, "messages", collect_msg_id, &mc);
524 :
525 27 : if (history_id_out) {
526 27 : char *hid = json_get_string(resp, "historyId");
527 27 : if (hid) {
528 27 : free(*history_id_out);
529 27 : *history_id_out = hid;
530 : }
531 : }
532 :
533 27 : page_token = json_get_string(resp, "nextPageToken");
534 27 : free(resp);
535 :
536 27 : if (c->progress_fn)
537 27 : c->progress_fn((size_t)mc.count, 0, c->progress_ctx);
538 :
539 27 : if (!page_token) break;
540 : }
541 18 : free(page_token);
542 :
543 18 : *uids_out = mc.uids;
544 18 : *count_out = mc.count;
545 18 : return mc.uids ? 0 : (mc.count == 0 ? 0 : -1);
546 : }
547 :
548 : /* ── Fetch message (raw + labels) ─────────────────────────────────── */
549 :
550 606 : char *gmail_fetch_message(GmailClient *c, const char *uid,
551 : char ***labels_out, int *label_count_out) {
552 606 : if (labels_out) *labels_out = NULL;
553 606 : if (label_count_out) *label_count_out = 0;
554 :
555 606 : RAII_STRING char *url = NULL;
556 606 : if (asprintf(&url, "%s/messages/%s?format=raw", gmail_api_base(), uid) == -1)
557 0 : return NULL;
558 :
559 606 : long code = 0;
560 1212 : RAII_STRING char *resp = api_get_retry(c, url, &code);
561 606 : if (!resp || code != 200) {
562 0 : if (code == 404) {
563 0 : 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 0 : return NULL;
568 : }
569 :
570 : /* Extract and decode raw message */
571 1212 : RAII_STRING char *raw_b64 = json_get_string(resp, "raw");
572 606 : if (!raw_b64) {
573 0 : logger_log(LOG_ERROR, "gmail_fetch_message %s: no 'raw' field", uid);
574 0 : return NULL;
575 : }
576 :
577 606 : size_t decoded_len = 0;
578 606 : char *decoded = gmail_base64url_decode(raw_b64, strlen(raw_b64), &decoded_len);
579 606 : if (!decoded) return NULL;
580 :
581 : /* Extract labels if requested */
582 606 : if (labels_out && label_count_out) {
583 601 : json_get_string_array(resp, "labelIds", labels_out, label_count_out);
584 : }
585 :
586 606 : return decoded;
587 : }
588 :
589 : /* ── Modify labels ────────────────────────────────────────────────── */
590 :
591 24 : 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 24 : RAII_STRING char *url = NULL;
595 24 : 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 24 : size_t body_cap = 256 + (size_t)(add_count + remove_count) * 80;
601 24 : char *body = malloc(body_cap);
602 24 : if (!body) return -1;
603 :
604 24 : size_t off = 0;
605 24 : off += (size_t)snprintf(body + off, body_cap - off, "{");
606 :
607 24 : if (add_count > 0) {
608 13 : off += (size_t)snprintf(body + off, body_cap - off, "\"addLabelIds\":[");
609 26 : for (int i = 0; i < add_count; i++) {
610 13 : if (i > 0) off += (size_t)snprintf(body + off, body_cap - off, ",");
611 13 : off += (size_t)snprintf(body + off, body_cap - off, "\"%s\"", add_labels[i]);
612 : }
613 13 : off += (size_t)snprintf(body + off, body_cap - off, "]");
614 : }
615 :
616 24 : if (remove_count > 0) {
617 13 : if (add_count > 0) off += (size_t)snprintf(body + off, body_cap - off, ",");
618 13 : off += (size_t)snprintf(body + off, body_cap - off, "\"removeLabelIds\":[");
619 26 : for (int i = 0; i < remove_count; i++) {
620 13 : if (i > 0) off += (size_t)snprintf(body + off, body_cap - off, ",");
621 13 : off += (size_t)snprintf(body + off, body_cap - off, "\"%s\"", remove_labels[i]);
622 : }
623 13 : off += (size_t)snprintf(body + off, body_cap - off, "]");
624 : }
625 :
626 24 : snprintf(body + off, body_cap - off, "}");
627 :
628 24 : long code = 0;
629 24 : char *resp = api_post_retry(c, url, body, &code);
630 24 : free(body);
631 24 : free(resp);
632 :
633 24 : if (code != 200) {
634 0 : logger_log(LOG_ERROR, "gmail_modify_labels %s: HTTP %ld", uid, code);
635 0 : return -1;
636 : }
637 24 : return 0;
638 : }
639 :
640 : /* ── Trash / Untrash ──────────────────────────────────────────────── */
641 :
642 0 : int gmail_trash(GmailClient *c, const char *uid) {
643 0 : RAII_STRING char *url = NULL;
644 0 : if (asprintf(&url, "%s/messages/%s/trash", gmail_api_base(), uid) == -1)
645 0 : return -1;
646 :
647 0 : long code = 0;
648 0 : RAII_STRING char *resp = api_post_retry(c, url, NULL, &code);
649 0 : if (code != 200) {
650 0 : logger_log(LOG_ERROR, "gmail_trash %s: HTTP %ld", uid, code);
651 0 : return -1;
652 : }
653 0 : return 0;
654 : }
655 :
656 0 : int gmail_untrash(GmailClient *c, const char *uid) {
657 0 : RAII_STRING char *url = NULL;
658 0 : if (asprintf(&url, "%s/messages/%s/untrash", gmail_api_base(), uid) == -1)
659 0 : return -1;
660 :
661 0 : long code = 0;
662 0 : RAII_STRING char *resp = api_post_retry(c, url, NULL, &code);
663 0 : if (code != 200) {
664 0 : logger_log(LOG_ERROR, "gmail_untrash %s: HTTP %ld", uid, code);
665 0 : return -1;
666 : }
667 0 : return 0;
668 : }
669 :
670 : /* ── Send ─────────────────────────────────────────────────────────── */
671 :
672 0 : int gmail_send(GmailClient *c, const char *raw_msg, size_t len) {
673 0 : RAII_STRING char *url = NULL;
674 0 : if (asprintf(&url, "%s/messages/send", gmail_api_base()) == -1)
675 0 : return -1;
676 :
677 : /* Base64url encode the raw RFC 2822 message */
678 0 : char *encoded = gmail_base64url_encode((const unsigned char *)raw_msg, len);
679 0 : if (!encoded) return -1;
680 :
681 : /* Build JSON body: {"raw": "<base64url>"} */
682 0 : size_t body_len = strlen(encoded) + 32;
683 0 : char *body = malloc(body_len);
684 0 : if (!body) { free(encoded); return -1; }
685 0 : snprintf(body, body_len, "{\"raw\":\"%s\"}", encoded);
686 0 : free(encoded);
687 :
688 0 : long code = 0;
689 0 : char *resp = api_post_retry(c, url, body, &code);
690 0 : free(body);
691 0 : free(resp);
692 :
693 0 : if (code != 200) {
694 0 : logger_log(LOG_ERROR, "gmail_send: HTTP %ld", code);
695 0 : return -1;
696 : }
697 0 : logger_log(LOG_INFO, "gmail_send: message sent successfully");
698 0 : return 0;
699 : }
700 :
701 : /* ── Profile (historyId) ──────────────────────────────────────────── */
702 :
703 0 : char *gmail_get_history_id(GmailClient *c) {
704 0 : RAII_STRING char *url = NULL;
705 0 : if (asprintf(&url, "%s/profile", gmail_api_base()) == -1) return NULL;
706 :
707 0 : long code = 0;
708 0 : RAII_STRING char *resp = api_get_retry(c, url, &code);
709 0 : if (!resp || code != 200) return NULL;
710 :
711 0 : return json_get_string(resp, "historyId");
712 : }
713 :
714 : /* ── History (incremental sync) ───────────────────────────────────── */
715 :
716 6 : char *gmail_get_history(GmailClient *c, const char *history_id) {
717 6 : RAII_STRING char *url = NULL;
718 6 : 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 6 : long code = 0;
727 6 : char *resp = api_get_retry(c, url, &code);
728 6 : if (!resp) return NULL;
729 :
730 6 : if (code == 404) {
731 2 : fprintf(stderr, " History API: 404 — historyId %s expired.\n", history_id);
732 2 : logger_log(LOG_WARN, "gmail: history %s expired — full resync needed", history_id);
733 2 : free(resp);
734 2 : return NULL;
735 : }
736 4 : if (code != 200) {
737 0 : fprintf(stderr, " History API: HTTP %ld for historyId %s.\n", code, history_id);
738 0 : logger_log(LOG_WARN, "gmail_get_history: HTTP %ld (will retry with full sync)", code);
739 0 : free(resp);
740 0 : return NULL;
741 : }
742 4 : fprintf(stderr, " History API: OK (historyId %s accepted).\n", history_id);
743 :
744 4 : return resp;
745 : }
|