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 1001 : static const char *gmail_api_base(void) {
21 1001 : const char *override = getenv("GMAIL_API_BASE_URL");
22 1001 : 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 1001 : static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) {
43 1001 : CurlBuf *buf = userdata;
44 1001 : size_t bytes = size * nmemb;
45 1001 : if (buf->len + bytes + 1 > buf->cap) {
46 1001 : size_t newcap = (buf->cap ? buf->cap * 2 : 4096);
47 1018 : while (newcap < buf->len + bytes + 1) newcap *= 2;
48 1001 : char *tmp = realloc(buf->data, newcap);
49 1001 : if (!tmp) return 0;
50 1001 : buf->data = tmp;
51 1001 : buf->cap = newcap;
52 : }
53 1001 : memcpy(buf->data + buf->len, ptr, bytes);
54 1001 : buf->len += bytes;
55 1001 : buf->data[buf->len] = '\0';
56 1001 : 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 975 : static char *api_get(GmailClient *c, const char *url, long *http_code) {
66 975 : CURL *curl = curl_easy_init();
67 975 : if (!curl) return NULL;
68 :
69 975 : CurlBuf buf = {0};
70 : char auth_hdr[2048];
71 975 : snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", c->access_token);
72 :
73 975 : struct curl_slist *headers = NULL;
74 975 : headers = curl_slist_append(headers, auth_hdr);
75 :
76 975 : curl_easy_setopt(curl, CURLOPT_URL, url);
77 975 : curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
78 975 : curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
79 975 : curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
80 975 : curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
81 975 : curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
82 975 : curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
83 :
84 : /* Disable SSL verification when talking to a plain HTTP test server */
85 975 : if (strncmp(url, "http://", 7) == 0) {
86 975 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
87 975 : curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
88 : }
89 :
90 975 : CURLcode res = curl_easy_perform(curl);
91 975 : curl_slist_free_all(headers);
92 :
93 975 : 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 975 : curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code);
101 975 : curl_easy_cleanup(curl);
102 975 : 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 975 : static char *api_get_retry(GmailClient *c, const char *url, long *http_code) {
162 975 : char *resp = api_get(c, url, http_code);
163 975 : 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 975 : 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 822 : char *gmail_base64url_decode(const char *input, size_t in_len, size_t *out_len) {
212 822 : size_t alloc = (in_len / 4 + 1) * 3 + 1;
213 822 : char *out = malloc(alloc);
214 822 : if (!out) return NULL;
215 :
216 822 : size_t o = 0;
217 822 : unsigned int acc = 0;
218 822 : int bits = 0;
219 :
220 253675 : for (size_t i = 0; i < in_len; i++) {
221 252853 : unsigned char ch = (unsigned char)input[i];
222 252853 : if (ch == '=' || ch == '\n' || ch == '\r' || ch == ' ') continue;
223 252853 : 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 252853 : if (val == 0 && ch != 'A') continue;
227 252853 : acc = (acc << 6) | (unsigned int)val;
228 252853 : bits += 6;
229 252853 : if (bits >= 8) {
230 189486 : bits -= 8;
231 189486 : out[o++] = (char)((acc >> bits) & 0xFF);
232 : }
233 : }
234 :
235 822 : out[o] = '\0';
236 822 : if (out_len) *out_len = o;
237 822 : 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 105 : GmailClient *gmail_connect(Config *cfg) {
272 105 : if (!cfg || !cfg->gmail_mode) {
273 0 : logger_log(LOG_ERROR, "gmail_connect: not a Gmail account");
274 0 : return NULL;
275 : }
276 :
277 105 : char *token = gmail_auth_refresh(cfg);
278 105 : if (!token) {
279 0 : logger_log(LOG_ERROR, "gmail_connect: failed to obtain access token");
280 0 : return NULL;
281 : }
282 :
283 105 : GmailClient *c = calloc(1, sizeof(*c));
284 105 : if (!c) { free(token); return NULL; }
285 105 : c->access_token = token;
286 105 : c->cfg = cfg;
287 105 : logger_log(LOG_DEBUG, "gmail_connect: connected for %s", cfg->user ? cfg->user : "(unknown)");
288 105 : return c;
289 : }
290 :
291 105 : void gmail_disconnect(GmailClient *c) {
292 105 : if (!c) return;
293 105 : free(c->access_token);
294 105 : free(c);
295 : }
296 :
297 124 : void gmail_set_progress(GmailClient *c, GmailProgressFn fn, void *ctx) {
298 124 : if (!c) return;
299 124 : c->progress_fn = fn;
300 124 : 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 594 : static void collect_label(const char *obj, int index, void *ctx) {
420 : (void)index;
421 594 : struct label_ctx *lc = ctx;
422 594 : char *name = json_get_string(obj, "name");
423 594 : char *id = json_get_string(obj, "id");
424 594 : if (!name || !id) { free(name); free(id); return; }
425 :
426 594 : if (lc->count == lc->cap) {
427 66 : int newcap = lc->cap ? lc->cap * 2 : 32;
428 66 : char **nn = realloc(lc->names, (size_t)newcap * sizeof(char *));
429 66 : char **ni = realloc(lc->ids, (size_t)newcap * sizeof(char *));
430 66 : if (!nn || !ni) { free(name); free(id); free(nn); return; }
431 66 : lc->names = nn;
432 66 : lc->ids = ni;
433 66 : lc->cap = newcap;
434 : }
435 594 : lc->names[lc->count] = name;
436 594 : lc->ids[lc->count] = id;
437 594 : lc->count++;
438 : }
439 :
440 66 : int gmail_list_labels(GmailClient *c, char ***names_out,
441 : char ***ids_out, int *count_out) {
442 66 : *names_out = NULL;
443 66 : *ids_out = NULL;
444 66 : *count_out = 0;
445 :
446 66 : RAII_STRING char *url = NULL;
447 66 : if (asprintf(&url, "%s/labels", gmail_api_base()) == -1) return -1;
448 :
449 66 : long code = 0;
450 132 : RAII_STRING char *resp = api_get_retry(c, url, &code);
451 66 : if (!resp || code != 200) {
452 0 : logger_log(LOG_ERROR, "gmail_list_labels: HTTP %ld", code);
453 0 : return -1;
454 : }
455 :
456 66 : struct label_ctx lc = {0};
457 66 : json_foreach_object(resp, "labels", collect_label, &lc);
458 :
459 66 : *names_out = lc.names;
460 66 : *ids_out = lc.ids;
461 66 : *count_out = lc.count;
462 66 : 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 2143 : static void collect_msg_id(const char *obj, int index, void *ctx) {
474 : (void)index;
475 2143 : struct msg_id_ctx *mc = ctx;
476 2143 : char *id = json_get_string(obj, "id");
477 2143 : if (!id) return;
478 :
479 2143 : if (mc->count == mc->cap) {
480 69 : int newcap = mc->cap ? mc->cap * 2 : 256;
481 69 : char (*tmp)[17] = realloc(mc->uids, (size_t)newcap * sizeof(char[17]));
482 69 : if (!tmp) { free(id); return; }
483 69 : mc->uids = tmp;
484 69 : mc->cap = newcap;
485 : }
486 2143 : snprintf(mc->uids[mc->count], 17, "%s", id);
487 2143 : mc->count++;
488 2143 : free(id);
489 : }
490 :
491 69 : 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 69 : *uids_out = NULL;
496 69 : *count_out = 0;
497 69 : if (history_id_out) *history_id_out = NULL;
498 :
499 69 : struct msg_id_ctx mc = {0};
500 69 : char *page_token = NULL;
501 :
502 9 : for (;;) {
503 : /* Build URL with optional query parameters */
504 : char url_buf[2048];
505 78 : int n = snprintf(url_buf, sizeof(url_buf), "%s/messages?maxResults=500", gmail_api_base());
506 78 : if (label_id)
507 7 : n += snprintf(url_buf + n, sizeof(url_buf) - (size_t)n, "&labelIds=%s", label_id);
508 78 : if (query)
509 0 : n += snprintf(url_buf + n, sizeof(url_buf) - (size_t)n, "&q=%s", query);
510 78 : if (page_token)
511 9 : n += snprintf(url_buf + n, sizeof(url_buf) - (size_t)n, "&pageToken=%s", page_token);
512 78 : free(page_token);
513 78 : page_token = NULL;
514 : (void)n;
515 :
516 78 : long code = 0;
517 78 : char *resp = api_get_retry(c, url_buf, &code);
518 78 : if (!resp || code != 200) {
519 0 : free(resp);
520 0 : break;
521 : }
522 :
523 78 : json_foreach_object(resp, "messages", collect_msg_id, &mc);
524 :
525 78 : if (history_id_out) {
526 78 : char *hid = json_get_string(resp, "historyId");
527 78 : if (hid) {
528 78 : free(*history_id_out);
529 78 : *history_id_out = hid;
530 : }
531 : }
532 :
533 78 : page_token = json_get_string(resp, "nextPageToken");
534 78 : free(resp);
535 :
536 78 : if (c->progress_fn)
537 71 : c->progress_fn((size_t)mc.count, 0, c->progress_ctx);
538 :
539 78 : if (!page_token) break;
540 : }
541 69 : free(page_token);
542 :
543 69 : *uids_out = mc.uids;
544 69 : *count_out = mc.count;
545 69 : return mc.uids ? 0 : (mc.count == 0 ? 0 : -1);
546 : }
547 :
548 : /* ── Fetch message (raw + labels) ─────────────────────────────────── */
549 :
550 822 : char *gmail_fetch_message(GmailClient *c, const char *uid,
551 : char ***labels_out, int *label_count_out) {
552 822 : if (labels_out) *labels_out = NULL;
553 822 : if (label_count_out) *label_count_out = 0;
554 :
555 822 : RAII_STRING char *url = NULL;
556 822 : if (asprintf(&url, "%s/messages/%s?format=raw", gmail_api_base(), uid) == -1)
557 0 : return NULL;
558 :
559 822 : long code = 0;
560 1644 : RAII_STRING char *resp = api_get_retry(c, url, &code);
561 822 : 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 1644 : RAII_STRING char *raw_b64 = json_get_string(resp, "raw");
572 822 : if (!raw_b64) {
573 0 : logger_log(LOG_ERROR, "gmail_fetch_message %s: no 'raw' field", uid);
574 0 : return NULL;
575 : }
576 :
577 822 : size_t decoded_len = 0;
578 822 : char *decoded = gmail_base64url_decode(raw_b64, strlen(raw_b64), &decoded_len);
579 822 : if (!decoded) return NULL;
580 :
581 : /* Extract labels if requested */
582 822 : if (labels_out && label_count_out) {
583 816 : json_get_string_array(resp, "labelIds", labels_out, label_count_out);
584 : }
585 :
586 822 : 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 0 : int gmail_delete_permanently(GmailClient *c, const char *uid) {
671 0 : RAII_STRING char *url = NULL;
672 0 : if (asprintf(&url, "%s/messages/%s", gmail_api_base(), uid) == -1)
673 0 : return -1;
674 :
675 0 : long code = 0;
676 0 : RAII_STRING char *resp = api_delete(c, url, &code);
677 0 : if (code != 204) {
678 0 : logger_log(LOG_ERROR, "gmail_delete_permanently %s: HTTP %ld", uid, code);
679 0 : return -1;
680 : }
681 0 : return 0;
682 : }
683 :
684 : /* ── Send ─────────────────────────────────────────────────────────── */
685 :
686 0 : int gmail_send(GmailClient *c, const char *raw_msg, size_t len) {
687 0 : RAII_STRING char *url = NULL;
688 0 : if (asprintf(&url, "%s/messages/send", gmail_api_base()) == -1)
689 0 : return -1;
690 :
691 : /* Base64url encode the raw RFC 2822 message */
692 0 : char *encoded = gmail_base64url_encode((const unsigned char *)raw_msg, len);
693 0 : if (!encoded) return -1;
694 :
695 : /* Build JSON body: {"raw": "<base64url>"} */
696 0 : size_t body_len = strlen(encoded) + 32;
697 0 : char *body = malloc(body_len);
698 0 : if (!body) { free(encoded); return -1; }
699 0 : snprintf(body, body_len, "{\"raw\":\"%s\"}", encoded);
700 0 : free(encoded);
701 :
702 0 : long code = 0;
703 0 : char *resp = api_post_retry(c, url, body, &code);
704 0 : free(body);
705 0 : free(resp);
706 :
707 0 : if (code != 200) {
708 0 : logger_log(LOG_ERROR, "gmail_send: HTTP %ld", code);
709 0 : return -1;
710 : }
711 0 : logger_log(LOG_INFO, "gmail_send: message sent successfully");
712 0 : return 0;
713 : }
714 :
715 : /* ── Profile (historyId) ──────────────────────────────────────────── */
716 :
717 0 : char *gmail_get_history_id(GmailClient *c) {
718 0 : RAII_STRING char *url = NULL;
719 0 : if (asprintf(&url, "%s/profile", gmail_api_base()) == -1) return NULL;
720 :
721 0 : long code = 0;
722 0 : RAII_STRING char *resp = api_get_retry(c, url, &code);
723 0 : if (!resp || code != 200) return NULL;
724 :
725 0 : return json_get_string(resp, "historyId");
726 : }
727 :
728 : /* ── History (incremental sync) ───────────────────────────────────── */
729 :
730 9 : char *gmail_get_history(GmailClient *c, const char *history_id) {
731 9 : RAII_STRING char *url = NULL;
732 9 : if (asprintf(&url, "%s/history?startHistoryId=%s"
733 : "&historyTypes=messageAdded"
734 : "&historyTypes=messageDeleted"
735 : "&historyTypes=labelAdded"
736 : "&historyTypes=labelRemoved",
737 : gmail_api_base(), history_id) == -1)
738 0 : return NULL;
739 :
740 9 : long code = 0;
741 9 : char *resp = api_get_retry(c, url, &code);
742 9 : if (!resp) return NULL;
743 :
744 9 : if (code == 404) {
745 2 : fprintf(stderr, " History API: 404 — historyId %s expired.\n", history_id);
746 2 : logger_log(LOG_WARN, "gmail: history %s expired — full resync needed", history_id);
747 2 : free(resp);
748 2 : return NULL;
749 : }
750 7 : if (code != 200) {
751 0 : fprintf(stderr, " History API: HTTP %ld for historyId %s.\n", code, history_id);
752 0 : logger_log(LOG_WARN, "gmail_get_history: HTTP %ld (will retry with full sync)", code);
753 0 : free(resp);
754 0 : return NULL;
755 : }
756 7 : fprintf(stderr, " History API: OK (historyId %s accepted).\n", history_id);
757 :
758 7 : return resp;
759 : }
|