LCOV - code coverage report
Current view: top level - libemail/src/infrastructure - gmail_client.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 69.7 % 422 294
Test Date: 2026-05-07 15:53:08 Functions: 80.8 % 26 21

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

Generated by: LCOV version 2.0-1