LCOV - code coverage report
Current view: top level - libemail/src/infrastructure - local_store.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 84.9 % 1448 1230
Test Date: 2026-05-07 15:53:08 Functions: 90.5 % 95 86

            Line data    Source code
       1              : #include "local_store.h"
       2              : #include "fs_util.h"
       3              : #include "mime_util.h"
       4              : #include "platform/path.h"
       5              : #include "raii.h"
       6              : #include "logger.h"
       7              : #include <ctype.h>
       8              : #include <inttypes.h>
       9              : #include <stdio.h>
      10              : #include <stdlib.h>
      11              : #include <string.h>
      12              : #include <dirent.h>
      13              : #include <unistd.h>
      14              : #include <time.h>
      15              : 
      16              : /* ── Account base path (set by local_store_init) ─────────────────────── */
      17              : 
      18              : static char g_account_base[8192];
      19              : static char g_account_name[520];
      20              : 
      21          948 : int local_store_init(const char *host_url, const char *username) {
      22          948 :     const char *data_base = platform_data_dir();
      23          948 :     if (!data_base) return -1;
      24          948 :     if (!host_url && (!username || !username[0])) return -1;
      25              : 
      26              :     /* The email address (username) uniquely identifies an account.
      27              :      * Use it directly as the directory key so two accounts on the same
      28              :      * server get separate local stores without a double-@ suffix.
      29              :      * Falls back to hostname-only for legacy single-account setups. */
      30          948 :     if (username && username[0]) {
      31          948 :         snprintf(g_account_base, sizeof(g_account_base),
      32              :                  "%s/email-cli/accounts/%s", data_base, username);
      33          948 :         snprintf(g_account_name, sizeof(g_account_name), "%s", username);
      34              :     } else {
      35              :         /* Extract hostname from URL: imaps://host:port → host */
      36            0 :         const char *p = strstr(host_url, "://");
      37            0 :         p = p ? p + 3 : host_url;
      38              :         char hostname[512];
      39            0 :         int i = 0;
      40            0 :         while (*p && *p != ':' && *p != '/' && i < (int)sizeof(hostname) - 1)
      41            0 :             hostname[i++] = *p++;
      42            0 :         hostname[i] = '\0';
      43            0 :         for (char *c = hostname; *c; c++) *c = (char)tolower((unsigned char)*c);
      44            0 :         snprintf(g_account_base, sizeof(g_account_base),
      45              :                  "%s/email-cli/accounts/imap.%s", data_base, hostname);
      46            0 :         snprintf(g_account_name, sizeof(g_account_name), "imap.%s", hostname);
      47              :     }
      48              : 
      49          948 :     logger_log(LOG_DEBUG, "local_store: account base = %s", g_account_base);
      50              :     /* Ensure the account base directory exists so callers can write files
      51              :      * (e.g. pending_fetch.tsv) before any message has been downloaded. */
      52          948 :     fs_mkdir_p(g_account_base, 0700);
      53          948 :     return 0;
      54              : }
      55              : 
      56           33 : const char *local_store_account_name(void) { return g_account_name; }
      57              : 
      58              : /* ── Reverse digit bucketing helpers ─────────────────────────────────── */
      59              : 
      60        13549 : static char digit1(const char *uid) {
      61        13549 :     size_t len = strlen(uid);
      62        13549 :     return len > 0 ? uid[len - 1] : '0';
      63              : }
      64        13549 : static char digit2(const char *uid) {
      65        13549 :     size_t len = strlen(uid);
      66        13549 :     return len > 1 ? uid[len - 2] : '0';
      67              : }
      68              : 
      69              : /* ── Shared file I/O ─────────────────────────────────────────────────── */
      70              : 
      71         8635 : static char *load_file(const char *path) {
      72        17270 :     RAII_FILE FILE *fp = fopen(path, "r");
      73         8635 :     if (!fp) return NULL;
      74         8013 :     if (fseek(fp, 0, SEEK_END) != 0) return NULL;
      75         8013 :     long size = ftell(fp);
      76         8013 :     if (size <= 0) return NULL;
      77         8013 :     rewind(fp);
      78         8013 :     char *buf = malloc((size_t)size + 1);
      79         8013 :     if (!buf) return NULL;
      80         8013 :     if ((long)fread(buf, 1, (size_t)size, fp) != size) { free(buf); return NULL; }
      81         8013 :     buf[size] = '\0';
      82         8013 :     return buf;
      83              : }
      84              : 
      85         2103 : static int write_file(const char *path, const char *content, size_t len) {
      86         4206 :     RAII_FILE FILE *fp = fopen(path, "w");
      87         2103 :     if (!fp) return -1;
      88         2103 :     if (fwrite(content, 1, len, fp) != len) return -1;
      89         2103 :     return 0;
      90              : }
      91              : 
      92              : /** @brief Ensures the parent directory of a bucketed path exists. */
      93         2042 : static int ensure_bucket_dir(const char *area, const char *folder, const char *uid) {
      94         2041 :     RAII_STRING char *dir = NULL;
      95         2042 :     if (asprintf(&dir, "%s/%s/%s/%c/%c",
      96         2042 :                  g_account_base, area, folder, digit1(uid), digit2(uid)) == -1)
      97            0 :         return -1;
      98         2042 :     return fs_mkdir_p(dir, 0700);
      99              : }
     100              : 
     101              : /* ── Message store ───────────────────────────────────────────────────── */
     102              : 
     103         3493 : static char *msg_path(const char *folder, const char *uid) {
     104         3493 :     if (!g_account_base[0]) return NULL;
     105         3493 :     char *path = NULL;
     106         3493 :     if (asprintf(&path, "%s/store/%s/%c/%c/%s.eml",
     107         3493 :                  g_account_base, folder, digit1(uid), digit2(uid), uid) == -1)
     108            0 :         return NULL;
     109         3493 :     return path;
     110              : }
     111              : 
     112         2664 : int local_msg_exists(const char *folder, const char *uid) {
     113         5328 :     RAII_STRING char *path = msg_path(folder, uid);
     114         2664 :     if (!path) return 0;
     115         2664 :     RAII_FILE FILE *fp = fopen(path, "r");
     116         2664 :     return fp != NULL;
     117              : }
     118              : 
     119          778 : int local_msg_save(const char *folder, const char *uid, const char *content, size_t len) {
     120          778 :     if (!g_account_base[0]) return -1;
     121          778 :     if (ensure_bucket_dir("store", folder, uid) != 0) {
     122            0 :         logger_log(LOG_ERROR, "Failed to create store bucket for %s/%s", folder, uid);
     123            0 :         return -1;
     124              :     }
     125         1554 :     RAII_STRING char *path = msg_path(folder, uid);
     126          777 :     if (!path) return -1;
     127          777 :     if (write_file(path, content, len) != 0) {
     128            0 :         logger_log(LOG_ERROR, "Failed to write store file: %s", path);
     129            0 :         return -1;
     130              :     }
     131          777 :     logger_log(LOG_DEBUG, "Stored %s/%s at %s", folder, uid, path);
     132          777 :     return 0;
     133              : }
     134              : 
     135           51 : char *local_msg_load(const char *folder, const char *uid) {
     136          102 :     RAII_STRING char *path = msg_path(folder, uid);
     137           51 :     if (!path) return NULL;
     138           51 :     return load_file(path);
     139              : }
     140              : 
     141              : /* ── Header store ────────────────────────────────────────────────────── */
     142              : 
     143         8014 : static char *hdr_path(const char *folder, const char *uid) {
     144         8014 :     if (!g_account_base[0]) return NULL;
     145         8014 :     char *path = NULL;
     146         8014 :     if (asprintf(&path, "%s/headers/%s/%c/%c/%s.hdr",
     147         8014 :                  g_account_base, folder, digit1(uid), digit2(uid), uid) == -1)
     148            0 :         return NULL;
     149         8014 :     return path;
     150              : }
     151              : 
     152         2012 : int local_hdr_exists(const char *folder, const char *uid) {
     153         4024 :     RAII_STRING char *path = hdr_path(folder, uid);
     154         2012 :     if (!path) return 0;
     155         2012 :     RAII_FILE FILE *fp = fopen(path, "r");
     156         2012 :     return fp != NULL;
     157              : }
     158              : 
     159         1264 : int local_hdr_save(const char *folder, const char *uid, const char *content, size_t len) {
     160         1264 :     if (!g_account_base[0]) return -1;
     161         1264 :     if (ensure_bucket_dir("headers", folder, uid) != 0) {
     162            0 :         logger_log(LOG_ERROR, "Failed to create header bucket for %s/%s", folder, uid);
     163            0 :         return -1;
     164              :     }
     165         2528 :     RAII_STRING char *path = hdr_path(folder, uid);
     166         1264 :     if (!path) return -1;
     167         1264 :     if (write_file(path, content, len) != 0) return -1;
     168         1264 :     logger_log(LOG_DEBUG, "Stored header %s/%s", folder, uid);
     169         1264 :     return 0;
     170              : }
     171              : 
     172         4737 : char *local_hdr_load(const char *folder, const char *uid) {
     173         9474 :     RAII_STRING char *path = hdr_path(folder, uid);
     174         4737 :     if (!path) return NULL;
     175         4737 :     return load_file(path);
     176              : }
     177              : 
     178            0 : int local_hdr_update_flags(const char *folder, const char *uid, int new_flags) {
     179            0 :     char *hdr = local_hdr_load(folder, uid);
     180            0 :     if (!hdr) return -1;
     181              : 
     182              :     /* Find the last tab → flags field starts after it */
     183            0 :     char *last_tab = strrchr(hdr, '\t');
     184            0 :     if (!last_tab) { free(hdr); return -1; }
     185              : 
     186              :     /* Rebuild: keep everything up to and including last tab, replace flags */
     187            0 :     *last_tab = '\0';
     188            0 :     char *updated = NULL;
     189            0 :     if (asprintf(&updated, "%s\t%d", hdr, new_flags) == -1) {
     190            0 :         free(hdr);
     191            0 :         return -1;
     192              :     }
     193            0 :     free(hdr);
     194              : 
     195            0 :     int rc = local_hdr_save(folder, uid, updated, strlen(updated));
     196            0 :     free(updated);
     197            0 :     return rc;
     198              : }
     199              : 
     200           22 : int local_hdr_update_labels(const char *folder, const char *uid,
     201              :                              const char **add_ids, int add_count,
     202              :                              const char **rm_ids,  int rm_count) {
     203           22 :     char *hdr = local_hdr_load(folder, uid);
     204           22 :     if (!hdr) return -1;
     205              : 
     206              :     /* .hdr format: from\tsubject\tdate\tlabels\tflags
     207              :      * Locate the labels field (4th tab-separated token). */
     208           22 :     char *t1 = strchr(hdr, '\t');
     209           22 :     if (!t1) { free(hdr); return -1; }
     210           22 :     char *t2 = strchr(t1 + 1, '\t');
     211           22 :     if (!t2) { free(hdr); return -1; }
     212           22 :     char *t3 = strchr(t2 + 1, '\t');
     213           22 :     if (!t3) { free(hdr); return -1; }
     214           22 :     char *t4 = strchr(t3 + 1, '\t');   /* may be NULL if flags field absent */
     215              : 
     216              :     /* labels field: [t3+1 .. t4)  (or end of string if no t4) */
     217           22 :     *t3 = '\0';                /* NUL-terminate prefix (from\tsubject\tdate) */
     218           22 :     const char *prefix  = hdr;
     219           22 :     const char *lbl_str = t3 + 1;
     220           22 :     const char *suffix  = t4 ? t4 + 1 : "";  /* flags value */
     221           22 :     if (t4) *t4 = '\0';
     222              : 
     223              :     /* Build new label set: start from existing labels */
     224           22 :     int cap = 64, cnt = 0;
     225           22 :     char **labels = malloc((size_t)cap * sizeof(char *));
     226           22 :     if (!labels) { free(hdr); return -1; }
     227              : 
     228           22 :     char *lbl_copy = strdup(lbl_str);
     229           22 :     if (!lbl_copy) { free(labels); free(hdr); return -1; }
     230           22 :     char *saveptr = NULL;
     231           22 :     for (char *tok = strtok_r(lbl_copy, ",", &saveptr);
     232           56 :          tok; tok = strtok_r(NULL, ",", &saveptr)) {
     233           34 :         if (!tok[0]) continue;
     234              :         /* skip labels in rm_ids */
     235           34 :         int rm = 0;
     236           45 :         for (int i = 0; i < rm_count; i++)
     237           21 :             if (rm_ids && rm_ids[i] && strcmp(tok, rm_ids[i]) == 0) { rm = 1; break; }
     238           34 :         if (rm) continue;
     239           24 :         if (cnt == cap) {
     240            0 :             cap *= 2;
     241            0 :             char **tmp = realloc(labels, (size_t)cap * sizeof(char *));
     242            0 :             if (!tmp) { free(lbl_copy); free(labels); free(hdr); return -1; }
     243            0 :             labels = tmp;
     244              :         }
     245           24 :         labels[cnt++] = tok;   /* points into lbl_copy */
     246              :     }
     247              : 
     248              :     /* append add_ids (skip duplicates) */
     249           33 :     for (int i = 0; i < add_count; i++) {
     250           11 :         if (!add_ids || !add_ids[i] || !add_ids[i][0]) continue;
     251           11 :         int dup = 0;
     252           21 :         for (int j = 0; j < cnt; j++)
     253           12 :             if (strcmp(labels[j], add_ids[i]) == 0) { dup = 1; break; }
     254           11 :         if (!dup) {
     255            9 :             if (cnt == cap) {
     256            0 :                 cap *= 2;
     257            0 :                 char **tmp = realloc(labels, (size_t)cap * sizeof(char *));
     258            0 :                 if (!tmp) { free(lbl_copy); free(labels); free(hdr); return -1; }
     259            0 :                 labels = tmp;
     260              :             }
     261            9 :             labels[cnt++] = (char *)add_ids[i];  /* borrows caller's pointer */
     262              :         }
     263              :     }
     264              : 
     265              :     /* Rebuild labels CSV */
     266           22 :     size_t lbl_len = 0;
     267           55 :     for (int i = 0; i < cnt; i++) lbl_len += strlen(labels[i]) + 1;
     268           22 :     char *new_lbl = malloc(lbl_len + 1);
     269           22 :     if (!new_lbl) { free(lbl_copy); free(labels); free(hdr); return -1; }
     270           22 :     new_lbl[0] = '\0';
     271           55 :     for (int i = 0; i < cnt; i++) {
     272           33 :         if (i) strcat(new_lbl, ",");
     273           33 :         strcat(new_lbl, labels[i]);
     274              :     }
     275              : 
     276              :     /* Recompute flags integer from the updated label set.
     277              :      * Label-derived bits: UNREAD→MSG_FLAG_UNSEEN(1), STARRED→MSG_FLAG_FLAGGED(2).
     278              :      * Non-label bits (MSG_FLAG_DONE=4, MSG_FLAG_ATTACH=8) are preserved. */
     279           22 :     int old_flags = (suffix && suffix[0]) ? atoi(suffix) : 0;
     280           22 :     int new_flags = old_flags & ~(MSG_FLAG_UNSEEN | MSG_FLAG_FLAGGED);
     281           55 :     for (int i = 0; i < cnt; i++) {
     282           33 :         if (strcmp(labels[i], "UNREAD")  == 0) new_flags |= MSG_FLAG_UNSEEN;
     283           33 :         if (strcmp(labels[i], "STARRED") == 0) new_flags |= MSG_FLAG_FLAGGED;
     284              :     }
     285              :     char flags_str[16];
     286           22 :     snprintf(flags_str, sizeof(flags_str), "%d", new_flags);
     287              : 
     288           22 :     free(lbl_copy);
     289           22 :     free(labels);
     290              : 
     291              :     /* Reassemble: prefix already NUL-terminated at t3 */
     292           22 :     char *updated = NULL;
     293           22 :     int rc = asprintf(&updated, "%s\t%s\t%s", prefix, new_lbl, flags_str);
     294           22 :     free(new_lbl);
     295           22 :     free(hdr);
     296           22 :     if (rc == -1) return -1;
     297              : 
     298           22 :     rc = local_hdr_save(folder, uid, updated, strlen(updated));
     299           22 :     free(updated);
     300           22 :     return rc;
     301              : }
     302              : 
     303         8776 : static int cmp_uid_evict(const void *a, const void *b) {
     304         8776 :     return memcmp(a, b, 16);
     305              : }
     306              : 
     307          164 : void local_hdr_evict_stale(const char *folder,
     308              :                              const char (*keep_uids)[17], int keep_count) {
     309          164 :     if (!g_account_base[0]) return;
     310              : 
     311          164 :     char (*sorted)[17] = malloc((size_t)keep_count * sizeof(char[17]));
     312          164 :     if (!sorted) return;
     313          164 :     memcpy(sorted, keep_uids, (size_t)keep_count * sizeof(char[17]));
     314          164 :     qsort(sorted, (size_t)keep_count, sizeof(char[17]), cmp_uid_evict);
     315              : 
     316              :     /* Walk all 100 buckets (10 × 10) */
     317         1804 :     for (int d1 = 0; d1 <= 9; d1++) {
     318        18040 :         for (int d2 = 0; d2 <= 9; d2++) {
     319        16400 :             RAII_STRING char *dir = NULL;
     320        16400 :             if (asprintf(&dir, "%s/headers/%s/%d/%d",
     321              :                          g_account_base, folder, d1, d2) == -1)
     322            0 :                 continue;
     323              : 
     324        32800 :             RAII_DIR DIR *d = opendir(dir);
     325        16400 :             if (!d) continue;
     326              : 
     327              :             struct dirent *ent;
     328         2070 :             while ((ent = readdir(d)) != NULL) {
     329         1640 :                 const char *name = ent->d_name;
     330         1640 :                 const char *dot  = strrchr(name, '.');
     331         1640 :                 if (!dot || strcmp(dot, ".hdr") != 0) continue;
     332          780 :                 size_t stem_len = (size_t)(dot - name);
     333          780 :                 if (stem_len == 0 || stem_len > 16) continue;
     334          780 :                 char key[17] = {0};
     335          780 :                 memcpy(key, name, stem_len);
     336          780 :                 if (!bsearch(key, sorted, (size_t)keep_count,
     337              :                              sizeof(char[17]), cmp_uid_evict)) {
     338            0 :                     RAII_STRING char *path = NULL;
     339            0 :                     if (asprintf(&path, "%s/%s", dir, name) != -1) {
     340            0 :                         remove(path);
     341            0 :                         logger_log(LOG_DEBUG,
     342              :                                    "Evicted stale header: UID %s in %s", key, folder);
     343              :                     }
     344              :                 }
     345              :             }
     346              :         }
     347              :     }
     348          164 :     free(sorted);
     349              : }
     350              : 
     351           35 : int local_hdr_list_all_uids(const char *folder,
     352              :                              char (**uids_out)[17], int *count_out) {
     353           35 :     *uids_out  = NULL;
     354           35 :     *count_out = 0;
     355              : 
     356           35 :     int cap = 256;
     357           35 :     char (*arr)[17] = malloc((size_t)cap * sizeof(char[17]));
     358           35 :     if (!arr) return -1;
     359           35 :     int count = 0;
     360              : 
     361              :     /* Walk all 256 buckets (d1, d2 ∈ 0-9 + a-f).
     362              :      * IMAP UIDs use decimal digits only; Gmail UIDs use full hex.
     363              :      * Hex directories a-f will simply not exist for IMAP accounts. */
     364              :     static const char hex[] = "0123456789abcdef";
     365          595 :     for (int i1 = 0; i1 < 16; i1++) {
     366         9520 :         for (int i2 = 0; i2 < 16; i2++) {
     367         8960 :             char d1 = hex[i1], d2 = hex[i2];
     368         8960 :             RAII_STRING char *dir = NULL;
     369         8960 :             if (asprintf(&dir, "%s/headers/%s/%c/%c",
     370              :                          g_account_base, folder, d1, d2) == -1)
     371            0 :                 continue;
     372        17920 :             RAII_DIR DIR *dp = opendir(dir);
     373         8960 :             if (!dp) continue;
     374              : 
     375              :             struct dirent *ent;
     376        14303 :             while ((ent = readdir(dp)) != NULL) {
     377        10727 :                 const char *name = ent->d_name;
     378        10727 :                 const char *dot  = strrchr(name, '.');
     379        10727 :                 if (!dot || strcmp(dot, ".hdr") != 0) continue;
     380         3575 :                 size_t stem_len = (size_t)(dot - name);
     381         3575 :                 if (stem_len == 0 || stem_len > 16) continue;
     382              : 
     383         3575 :                 if (count >= cap) {
     384            0 :                     cap *= 2;
     385            0 :                     char (*tmp)[17] = realloc(arr, (size_t)cap * sizeof(char[17]));
     386            0 :                     if (!tmp) { free(arr); return -1; }
     387            0 :                     arr = tmp;
     388              :                 }
     389         3575 :                 memset(arr[count], 0, 17);
     390         3575 :                 memcpy(arr[count], name, stem_len);
     391         3575 :                 count++;
     392              :             }
     393              :         }
     394              :     }
     395              : 
     396           35 :     *uids_out  = arr;
     397           35 :     *count_out = count;
     398           35 :     return 0;
     399              : }
     400              : 
     401              : /* ── Index helpers ───────────────────────────────────────────────────── */
     402              : 
     403              : /** @brief Checks if a reference line already exists in an index file. */
     404          346 : static int index_has_ref(const char *path, const char *ref) {
     405          346 :     char *content = load_file(path);
     406          346 :     if (!content) return 0;
     407          268 :     size_t ref_len = strlen(ref);
     408          268 :     const char *p = content;
     409         1208 :     while (*p) {
     410          940 :         if (strncmp(p, ref, ref_len) == 0 &&
     411            0 :             (p[ref_len] == '\n' || p[ref_len] == '\0')) {
     412            0 :             free(content);
     413            0 :             return 1;
     414              :         }
     415          940 :         const char *nl = strchr(p, '\n');
     416          940 :         if (!nl) break;
     417          940 :         p = nl + 1;
     418              :     }
     419          268 :     free(content);
     420          268 :     return 0;
     421              : }
     422              : 
     423              : /** @brief Appends a reference to an index file (skips duplicates). */
     424          346 : static int index_append(const char *dir_path, const char *file_name,
     425              :                         const char *ref) {
     426          346 :     if (fs_mkdir_p(dir_path, 0700) != 0) return -1;
     427              : 
     428          346 :     RAII_STRING char *path = NULL;
     429          346 :     if (asprintf(&path, "%s/%s", dir_path, file_name) == -1) return -1;
     430              : 
     431          346 :     if (index_has_ref(path, ref)) return 0; /* already indexed */
     432              : 
     433          692 :     RAII_FILE FILE *fp = fopen(path, "a");
     434          346 :     if (!fp) return -1;
     435          346 :     fprintf(fp, "%s\n", ref);
     436          346 :     return 0;
     437              : }
     438              : 
     439              : /** @brief Removes a reference from an index file. */
     440              : __attribute__((unused))
     441              : /** @brief Extracts email address parts from a From header value. */
     442          173 : static void extract_email_parts(const char *from,
     443              :                                 char *domain, size_t dlen,
     444              :                                 char *local_part, size_t llen) {
     445          173 :     domain[0] = '\0';
     446          173 :     local_part[0] = '\0';
     447              : 
     448              :     /* Try "Name <user@domain>" format first */
     449          173 :     const char *lt = strchr(from, '<');
     450          173 :     const char *gt = lt ? strchr(lt, '>') : NULL;
     451              :     const char *email;
     452              :     size_t elen;
     453          173 :     if (lt && gt && gt > lt + 1) {
     454          173 :         email = lt + 1;
     455          173 :         elen  = (size_t)(gt - email);
     456              :     } else {
     457              :         /* Bare address: skip leading whitespace */
     458            0 :         email = from;
     459            0 :         while (*email == ' ' || *email == '\t') email++;
     460            0 :         elen = strlen(email);
     461              :         /* Trim trailing whitespace */
     462            0 :         while (elen > 0 && (email[elen - 1] == ' ' || email[elen - 1] == '\n'
     463            0 :                             || email[elen - 1] == '\r'))
     464            0 :             elen--;
     465              :     }
     466              : 
     467          173 :     const char *at = memchr(email, '@', elen);
     468          173 :     if (!at) return;
     469              : 
     470          173 :     size_t ll = (size_t)(at - email);
     471          173 :     size_t dl = elen - ll - 1;
     472          173 :     if (ll >= llen) ll = llen - 1;
     473          173 :     if (dl >= dlen) dl = dlen - 1;
     474          173 :     memcpy(local_part, email, ll);
     475          173 :     local_part[ll] = '\0';
     476          173 :     memcpy(domain, at + 1, dl);
     477          173 :     domain[dl] = '\0';
     478              : 
     479              :     /* Lowercase domain */
     480         2091 :     for (char *c = domain; *c; c++)
     481         1918 :         *c = (char)tolower((unsigned char)*c);
     482              :     /* Lowercase local part */
     483         1160 :     for (char *c = local_part; *c; c++)
     484          987 :         *c = (char)tolower((unsigned char)*c);
     485              : }
     486              : 
     487          173 : int local_index_update(const char *folder, const char *uid, const char *raw_msg) {
     488          173 :     if (!g_account_base[0] || !raw_msg) return -1;
     489              : 
     490              :     char ref[512];
     491          173 :     snprintf(ref, sizeof(ref), "%s/%s", folder, uid);
     492              : 
     493              :     /* 1. From index: index/from/<domain>/<localpart> */
     494          346 :     RAII_STRING char *from_raw = mime_get_header(raw_msg, "From");
     495          173 :     if (from_raw) {
     496              :         char domain[256], local_part[256];
     497          173 :         extract_email_parts(from_raw, domain, sizeof(domain),
     498              :                             local_part, sizeof(local_part));
     499          173 :         if (domain[0] && local_part[0]) {
     500          173 :             RAII_STRING char *idx_dir = NULL;
     501          173 :             if (asprintf(&idx_dir, "%s/index/from/%s",
     502              :                          g_account_base, domain) != -1)
     503          173 :                 index_append(idx_dir, local_part, ref);
     504              :         }
     505              :     }
     506              : 
     507              :     /* 2. Date index: index/date/<year>/<month>/<day> */
     508          173 :     RAII_STRING char *date_raw = mime_get_header(raw_msg, "Date");
     509          173 :     if (date_raw) {
     510          346 :         RAII_STRING char *formatted = mime_format_date(date_raw);
     511          173 :         if (formatted && strlen(formatted) >= 10) {
     512              :             int year, month, day;
     513          173 :             if (sscanf(formatted, "%d-%d-%d", &year, &month, &day) == 3) {
     514          173 :                 RAII_STRING char *idx_dir = NULL;
     515              :                 char day_str[4];
     516          173 :                 snprintf(day_str, sizeof(day_str), "%02d", day);
     517          173 :                 if (asprintf(&idx_dir, "%s/index/date/%04d/%02d",
     518              :                              g_account_base, year, month) != -1)
     519          173 :                     index_append(idx_dir, day_str, ref);
     520              :             }
     521              :         }
     522              :     }
     523              : 
     524          173 :     return 0;
     525              : }
     526              : 
     527            1 : int local_msg_delete(const char *folder, const char *uid) {
     528            1 :     if (!g_account_base[0]) return -1;
     529              : 
     530              :     char ref[512];
     531            1 :     snprintf(ref, sizeof(ref), "%s/%s", folder, uid);
     532              : 
     533              :     /* 1. Remove .eml file */
     534            2 :     RAII_STRING char *mpath = msg_path(folder, uid);
     535            1 :     if (mpath) remove(mpath);
     536              : 
     537              :     /* 2. Remove .hdr file */
     538            1 :     RAII_STRING char *hpath = hdr_path(folder, uid);
     539            1 :     if (hpath) remove(hpath);
     540              : 
     541              :     /* 3. Remove from indexes — best effort scan of from/ and date/ */
     542              :     /* For from/: we'd need to know which file has this ref.
     543              :      * Since we don't track that, just load the message (if still cached)
     544              :      * or accept the stale entry.  A full re-index can clean up. */
     545            1 :     logger_log(LOG_DEBUG, "Deleted %s/%s", folder, uid);
     546            1 :     return 0;
     547              : }
     548              : 
     549              : /* ── UI preferences ──────────────────────────────────────────────────── */
     550              : 
     551          466 : static char *ui_pref_path(void) {
     552          466 :     const char *data_base = platform_data_dir();
     553          466 :     if (!data_base) return NULL;
     554          466 :     char *path = NULL;
     555          466 :     if (asprintf(&path, "%s/email-cli/ui.ini", data_base) == -1)
     556            0 :         return NULL;
     557          466 :     return path;
     558              : }
     559              : 
     560          103 : int ui_pref_get_int(const char *key, int default_val) {
     561          206 :     RAII_STRING char *path = ui_pref_path();
     562          103 :     if (!path) return default_val;
     563          206 :     RAII_FILE FILE *fp = fopen(path, "r");
     564          103 :     if (!fp) return default_val;
     565              :     char line[256];
     566          103 :     size_t klen = strlen(key);
     567          265 :     while (fgets(line, sizeof(line), fp))
     568          184 :         if (strncmp(line, key, klen) == 0 && line[klen] == '=')
     569           22 :             return atoi(line + klen + 1);
     570           81 :     return default_val;
     571              : }
     572              : 
     573            4 : int ui_pref_set_int(const char *key, int value) {
     574            4 :     const char *data_base = platform_data_dir();
     575            4 :     if (!data_base) return -1;
     576            4 :     RAII_STRING char *dir = NULL;
     577            4 :     if (asprintf(&dir, "%s/email-cli", data_base) == -1) return -1;
     578            4 :     if (fs_mkdir_p(dir, 0700) != 0) return -1;
     579            8 :     RAII_STRING char *path = ui_pref_path();
     580            4 :     if (!path) return -1;
     581              : 
     582            4 :     char *existing = load_file(path);
     583              : 
     584            8 :     RAII_FILE FILE *fp = fopen(path, "w");
     585            4 :     if (!fp) { free(existing); return -1; }
     586              : 
     587            4 :     size_t klen = strlen(key);
     588            4 :     if (existing) {
     589            4 :         char *line = existing;
     590           15 :         while (*line) {
     591           11 :             char *nl = strchr(line, '\n');
     592           11 :             size_t llen = nl ? (size_t)(nl - line + 1) : strlen(line);
     593           11 :             if (!(strncmp(line, key, klen) == 0 && line[klen] == '='))
     594            8 :                 fwrite(line, 1, llen, fp);
     595           11 :             line += llen;
     596              :         }
     597            4 :         free(existing);
     598              :     }
     599            4 :     fprintf(fp, "%s=%d\n", key, value);
     600            4 :     logger_log(LOG_DEBUG, "UI pref %s=%d saved", key, value);
     601            4 :     return 0;
     602              : }
     603              : 
     604          185 : char *ui_pref_get_str(const char *key) {
     605          370 :     RAII_STRING char *path = ui_pref_path();
     606          185 :     if (!path) return NULL;
     607          370 :     RAII_FILE FILE *fp = fopen(path, "r");
     608          185 :     if (!fp) return NULL;
     609              :     char line[1024];
     610          183 :     size_t klen = strlen(key);
     611          220 :     while (fgets(line, sizeof(line), fp)) {
     612          217 :         if (strncmp(line, key, klen) == 0 && line[klen] == '=') {
     613          180 :             char *val = line + klen + 1;
     614          180 :             size_t vlen = strlen(val);
     615          360 :             while (vlen > 0 && (val[vlen-1] == '\n' || val[vlen-1] == '\r'))
     616          180 :                 val[--vlen] = '\0';
     617          180 :             return strdup(val);
     618              :         }
     619              :     }
     620            3 :     return NULL;
     621              : }
     622              : 
     623          174 : int ui_pref_set_str(const char *key, const char *value) {
     624          174 :     const char *data_base = platform_data_dir();
     625          174 :     if (!data_base) return -1;
     626          174 :     RAII_STRING char *dir = NULL;
     627          174 :     if (asprintf(&dir, "%s/email-cli", data_base) == -1) return -1;
     628          174 :     if (fs_mkdir_p(dir, 0700) != 0) return -1;
     629          348 :     RAII_STRING char *path = ui_pref_path();
     630          174 :     if (!path) return -1;
     631              : 
     632          174 :     char *existing = load_file(path);
     633              : 
     634          348 :     RAII_FILE FILE *fp = fopen(path, "w");
     635          174 :     if (!fp) { free(existing); return -1; }
     636              : 
     637          174 :     size_t klen = strlen(key);
     638          174 :     if (existing) {
     639          172 :         char *line = existing;
     640          544 :         while (*line) {
     641          372 :             char *nl = strchr(line, '\n');
     642          372 :             size_t llen = nl ? (size_t)(nl - line + 1) : strlen(line);
     643          372 :             if (!(strncmp(line, key, klen) == 0 && line[klen] == '='))
     644          202 :                 fwrite(line, 1, llen, fp);
     645          372 :             line += llen;
     646              :         }
     647          172 :         free(existing);
     648              :     }
     649          174 :     fprintf(fp, "%s=%s\n", key, value);
     650          174 :     logger_log(LOG_DEBUG, "UI pref %s=%s saved", key, value);
     651          174 :     return 0;
     652              : }
     653              : 
     654              : /* ── Folder manifest ─────────────────────────────────────────────────── */
     655              : 
     656         3314 : static char *manifest_path(const char *folder) {
     657         3314 :     if (!g_account_base[0]) return NULL;
     658         3314 :     char *path = NULL;
     659         3314 :     if (asprintf(&path, "%s/manifests/%s.tsv", g_account_base, folder) == -1)
     660            0 :         return NULL;
     661         3314 :     return path;
     662              : }
     663              : 
     664              : /** @brief Duplicates a string, replacing tabs with spaces. */
     665         2091 : static char *sanitise(const char *s) {
     666         2091 :     if (!s) return strdup("");
     667         2091 :     char *d = strdup(s);
     668        49240 :     if (d) for (char *p = d; *p; p++) if (*p == '\t') *p = ' ';
     669         2091 :     return d;
     670              : }
     671              : 
     672         3144 : Manifest *manifest_load(const char *folder) {
     673         6288 :     RAII_STRING char *path = manifest_path(folder);
     674         3144 :     logger_log(LOG_DEBUG, "manifest_load: folder=%s account_base=%s path=%s",
     675         3144 :                folder, g_account_base, path ? path : "(null)");
     676         3144 :     if (!path) return NULL;
     677              : 
     678         3144 :     char *data = load_file(path);
     679         3144 :     if (!data) return NULL;
     680              : 
     681         2730 :     Manifest *m = calloc(1, sizeof(*m));
     682         2730 :     if (!m) { free(data); return NULL; }
     683         2730 :     m->capacity = 64;
     684         2730 :     m->entries = malloc((size_t)m->capacity * sizeof(ManifestEntry));
     685         2730 :     if (!m->entries) { free(m); free(data); return NULL; }
     686              : 
     687         2730 :     char *line = data;
     688         6346 :     while (*line) {
     689         3616 :         char *nl = strchr(line, '\n');
     690         3616 :         if (nl) *nl = '\0';
     691              : 
     692              :         /* Parse: uid\tfrom\tsubject\tdate */
     693         3616 :         char *t1 = strchr(line, '\t');
     694         3616 :         if (!t1 || t1 == line) {
     695            0 :             line = nl ? nl + 1 : line + strlen(line);
     696            0 :             continue;
     697              :         }
     698         3616 :         *t1 = '\0';
     699         3616 :         char *uid_field = line;
     700         3616 :         char *from_start = t1 + 1;
     701         3616 :         char *t2 = strchr(from_start, '\t');
     702         3616 :         if (!t2) { line = nl ? nl + 1 : line + strlen(line); continue; }
     703         3616 :         *t2 = '\0';
     704         3616 :         char *subj_start = t2 + 1;
     705         3616 :         char *t3 = strchr(subj_start, '\t');
     706         3616 :         if (!t3) { line = nl ? nl + 1 : line + strlen(line); continue; }
     707         3616 :         *t3 = '\0';
     708         3616 :         char *date_start = t3 + 1;
     709              :         /* Optional 5th field: unseen flag */
     710         3616 :         int unseen_val = 0;
     711         3616 :         char *t4 = strchr(date_start, '\t');
     712         3616 :         if (t4) {
     713         3616 :             *t4 = '\0';
     714         3616 :             unseen_val = atoi(t4 + 1);
     715              :         }
     716              : 
     717         3616 :         if (m->count == m->capacity) {
     718            8 :             m->capacity *= 2;
     719            8 :             ManifestEntry *tmp = realloc(m->entries,
     720            8 :                                          (size_t)m->capacity * sizeof(ManifestEntry));
     721            8 :             if (!tmp) break;
     722            8 :             m->entries = tmp;
     723              :         }
     724         3616 :         ManifestEntry *e = &m->entries[m->count++];
     725         3616 :         snprintf(e->uid, sizeof(e->uid), "%s", uid_field);
     726         3616 :         e->from    = strdup(from_start);
     727         3616 :         e->subject = strdup(subj_start);
     728         3616 :         e->date    = strdup(date_start);
     729         3616 :         e->flags   = unseen_val;
     730              : 
     731         3616 :         line = nl ? nl + 1 : line + strlen(line);
     732              :     }
     733         2730 :     free(data);
     734         2730 :     return m;
     735              : }
     736              : 
     737          170 : int manifest_save(const char *folder, const Manifest *m) {
     738          170 :     if (!g_account_base[0] || !m) return -1;
     739              : 
     740          170 :     RAII_STRING char *dir = NULL;
     741          170 :     if (asprintf(&dir, "%s/manifests", g_account_base) == -1) return -1;
     742          170 :     if (fs_mkdir_p(dir, 0700) != 0) return -1;
     743              : 
     744              :     /* For nested folders like "munka/ai" we need the parent dir */
     745          340 :     RAII_STRING char *path = manifest_path(folder);
     746          170 :     if (!path) return -1;
     747              : 
     748              :     /* Ensure parent directory exists (folder path may have slashes) */
     749          170 :     char *last_slash = strrchr(path, '/');
     750          170 :     if (last_slash) {
     751          170 :         char saved = *last_slash;
     752          170 :         *last_slash = '\0';
     753          170 :         fs_mkdir_p(path, 0700);
     754          170 :         *last_slash = saved;
     755              :     }
     756              : 
     757          340 :     RAII_FILE FILE *fp = fopen(path, "w");
     758          170 :     if (!fp) return -1;
     759              : 
     760          867 :     for (int i = 0; i < m->count; i++) {
     761          697 :         ManifestEntry *e = &m->entries[i];
     762         1394 :         RAII_STRING char *f = sanitise(e->from);
     763         1394 :         RAII_STRING char *s = sanitise(e->subject);
     764         1394 :         RAII_STRING char *d = sanitise(e->date);
     765          697 :         fprintf(fp, "%s\t%s\t%s\t%s\t%d\n", e->uid, f ? f : "", s ? s : "", d ? d : "", e->flags);
     766              :     }
     767          170 :     logger_log(LOG_DEBUG, "Manifest saved: %s (%d entries)", folder, m->count);
     768          170 :     return 0;
     769              : }
     770              : 
     771         2813 : void manifest_free(Manifest *m) {
     772         2813 :     if (!m) return;
     773         7991 :     for (int i = 0; i < m->count; i++) {
     774         5178 :         free(m->entries[i].from);
     775         5178 :         free(m->entries[i].subject);
     776         5178 :         free(m->entries[i].date);
     777              :     }
     778         2813 :     free(m->entries);
     779         2813 :     free(m);
     780              : }
     781              : 
     782         9802 : ManifestEntry *manifest_find(const Manifest *m, const char *uid) {
     783         9802 :     if (!m) return NULL;
     784       803394 :     for (int i = 0; i < m->count; i++)
     785       800635 :         if (strcmp(m->entries[i].uid, uid) == 0) return &m->entries[i];
     786         2759 :     return NULL;
     787              : }
     788              : 
     789         1730 : void manifest_upsert(Manifest *m, const char *uid,
     790              :                      char *from, char *subject, char *date, int flags) {
     791         1730 :     if (!m) return;
     792         1730 :     ManifestEntry *existing = manifest_find(m, uid);
     793         1730 :     if (existing) {
     794           97 :         free(existing->from);    existing->from    = from;
     795           97 :         free(existing->subject); existing->subject = subject;
     796           97 :         free(existing->date);    existing->date    = date;
     797           97 :         existing->flags = flags;
     798           97 :         return;
     799              :     }
     800         1633 :     if (m->count == m->capacity) {
     801          147 :         int new_cap = m->capacity ? m->capacity * 2 : 64;
     802          147 :         ManifestEntry *tmp = realloc(m->entries,
     803          147 :                                      (size_t)new_cap * sizeof(ManifestEntry));
     804          147 :         if (!tmp) { free(from); free(subject); free(date); return; }
     805          147 :         m->entries = tmp;
     806          147 :         m->capacity = new_cap;
     807              :     }
     808         1633 :     ManifestEntry *e = &m->entries[m->count++];
     809         1633 :     snprintf(e->uid, sizeof(e->uid), "%s", uid);
     810         1633 :     e->from = from; e->subject = subject; e->date = date;
     811         1633 :     e->flags = flags;
     812              : }
     813              : 
     814          276 : void manifest_retain(Manifest *m, const char (*keep_uids)[17], int keep_count) {
     815          276 :     if (!m) return;
     816          276 :     int dst = 0;
     817         1093 :     for (int i = 0; i < m->count; i++) {
     818          817 :         int found = 0;
     819        71771 :         for (int j = 0; j < keep_count; j++) {
     820        71770 :             if (strcmp(keep_uids[j], m->entries[i].uid) == 0) { found = 1; break; }
     821              :         }
     822          817 :         if (found) {
     823          816 :             if (dst != i) m->entries[dst] = m->entries[i];
     824          816 :             dst++;
     825              :         } else {
     826            1 :             free(m->entries[i].from);
     827            1 :             free(m->entries[i].subject);
     828            1 :             free(m->entries[i].date);
     829              :         }
     830              :     }
     831          276 :     m->count = dst;
     832              : }
     833              : 
     834            1 : void manifest_remove(Manifest *m, const char *uid) {
     835            1 :     if (!m || !uid) return;
     836            2 :     for (int i = 0; i < m->count; i++) {
     837            2 :         if (strcmp(m->entries[i].uid, uid) == 0) {
     838            1 :             free(m->entries[i].from);
     839            1 :             free(m->entries[i].subject);
     840            1 :             free(m->entries[i].date);
     841              :             /* Shift remaining entries down */
     842            1 :             for (int j = i + 1; j < m->count; j++)
     843            0 :                 m->entries[j - 1] = m->entries[j];
     844            1 :             m->count--;
     845            1 :             return;
     846              :         }
     847              :     }
     848              : }
     849              : 
     850              : /* ── Folder list cache ───────────────────────────────────────────────── */
     851              : 
     852           22 : int local_folder_list_save(const char **folders, int count, char sep) {
     853           22 :     if (!g_account_base[0]) return -1;
     854           22 :     RAII_STRING char *path = NULL;
     855           22 :     if (asprintf(&path, "%s/folders.cache", g_account_base) == -1) return -1;
     856           44 :     RAII_FILE FILE *fp = fopen(path, "w");
     857           22 :     if (!fp) return -1;
     858           22 :     fprintf(fp, "sep=%c\n", sep);
     859          198 :     for (int i = 0; i < count; i++)
     860          176 :         fprintf(fp, "%s\n", folders[i] ? folders[i] : "");
     861           22 :     logger_log(LOG_DEBUG, "Folder list cache saved: %d folders", count);
     862           22 :     return 0;
     863              : }
     864              : 
     865          438 : char **local_folder_list_load(int *count_out, char *sep_out) {
     866          438 :     *count_out = 0;
     867          438 :     if (!g_account_base[0]) return NULL;
     868          438 :     RAII_STRING char *path = NULL;
     869          438 :     if (asprintf(&path, "%s/folders.cache", g_account_base) == -1) return NULL;
     870          876 :     RAII_FILE FILE *fp = fopen(path, "r");
     871          438 :     if (!fp) return NULL;
     872              : 
     873              :     char line[1024];
     874          359 :     char sep = '.';
     875              :     /* First line: sep=<char> */
     876          359 :     if (!fgets(line, sizeof(line), fp)) return NULL;
     877          359 :     if (strncmp(line, "sep=", 4) == 0 && line[4] != '\n')
     878          359 :         sep = line[4];
     879              : 
     880          359 :     int cap = 32, cnt = 0;
     881          359 :     char **folders = malloc((size_t)cap * sizeof(char *));
     882          359 :     if (!folders) return NULL;
     883         3231 :     while (fgets(line, sizeof(line), fp)) {
     884              :         /* strip trailing newline */
     885         2872 :         size_t len = strlen(line);
     886         5744 :         while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r'))
     887         2872 :             line[--len] = '\0';
     888         2872 :         if (len == 0) continue;
     889         2872 :         if (cnt == cap) {
     890            0 :             cap *= 2;
     891            0 :             char **tmp = realloc(folders, (size_t)cap * sizeof(char *));
     892            0 :             if (!tmp) { for (int i = 0; i < cnt; i++) free(folders[i]); free(folders); return NULL; }
     893            0 :             folders = tmp;
     894              :         }
     895         2872 :         folders[cnt] = strdup(line);
     896         2872 :         if (!folders[cnt]) { for (int i = 0; i < cnt; i++) free(folders[i]); free(folders); return NULL; }
     897         2872 :         cnt++;
     898              :     }
     899          359 :     *count_out = cnt;
     900          359 :     if (sep_out) *sep_out = sep;
     901          359 :     logger_log(LOG_DEBUG, "Folder list cache loaded: %d folders", cnt);
     902          359 :     return folders;
     903              : }
     904              : 
     905         1792 : void manifest_count_folder(const char *folder, int *total_out,
     906              :                             int *unseen_out, int *flagged_out) {
     907         1792 :     *total_out = 0; *unseen_out = 0; *flagged_out = 0;
     908         1792 :     Manifest *m = manifest_load(folder);
     909         1792 :     if (!m) return;
     910         1540 :     *total_out = m->count;
     911         3294 :     for (int i = 0; i < m->count; i++) {
     912         1754 :         if (m->entries[i].flags & MSG_FLAG_UNSEEN)  (*unseen_out)++;
     913         1754 :         if (m->entries[i].flags & MSG_FLAG_FLAGGED) (*flagged_out)++;
     914              :     }
     915         1540 :     manifest_free(m);
     916              : }
     917              : 
     918           13 : Manifest *manifest_load_all_with_flag(int flag_mask) {
     919           13 :     Manifest *result = calloc(1, sizeof(Manifest));
     920           13 :     if (!result) return NULL;
     921           13 :     if (!g_account_base[0]) return result;
     922              :     char dir_path[8300];
     923           13 :     snprintf(dir_path, sizeof(dir_path), "%s/manifests", g_account_base);
     924           26 :     RAII_DIR DIR *dp = opendir(dir_path);
     925           13 :     if (!dp) return result;
     926              :     struct dirent *ent;
     927          143 :     while ((ent = readdir(dp)) != NULL) {
     928          130 :         const char *name = ent->d_name;
     929          130 :         size_t nlen = strlen(name);
     930          130 :         if (nlen <= 4 || strcmp(name + nlen - 4, ".tsv") != 0) continue;
     931          104 :         RAII_STRING char *folder = strndup(name, nlen - 4);
     932          104 :         if (!folder) continue;
     933          104 :         Manifest *m = manifest_load(folder);
     934          104 :         if (!m) continue;
     935          213 :         for (int i = 0; i < m->count; i++) {
     936          109 :             if (m->entries[i].flags & flag_mask)
     937           50 :                 manifest_upsert(result, m->entries[i].uid,
     938           50 :                                 strdup(m->entries[i].from    ? m->entries[i].from    : ""),
     939           50 :                                 strdup(m->entries[i].subject ? m->entries[i].subject : ""),
     940           50 :                                 strdup(m->entries[i].date    ? m->entries[i].date    : ""),
     941           50 :                                 m->entries[i].flags);
     942              :         }
     943          104 :         manifest_free(m);
     944              :     }
     945           13 :     return result;
     946              : }
     947              : 
     948          103 : void manifest_count_all_flags(int *unread_out, int *flagged_out,
     949              :                                int *junk_out, int *phishing_out,
     950              :                                int *answered_out, int *forwarded_out) {
     951          103 :     if (unread_out)   *unread_out   = 0;
     952          103 :     if (flagged_out)  *flagged_out  = 0;
     953          103 :     if (junk_out)     *junk_out     = 0;
     954          103 :     if (phishing_out) *phishing_out = 0;
     955          103 :     if (answered_out) *answered_out = 0;
     956          103 :     if (forwarded_out)*forwarded_out= 0;
     957          103 :     if (!g_account_base[0]) return;
     958              :     char dir_path[8300];
     959          103 :     snprintf(dir_path, sizeof(dir_path), "%s/manifests", g_account_base);
     960          206 :     RAII_DIR DIR *dp = opendir(dir_path);
     961          103 :     if (!dp) return;
     962              :     struct dirent *ent;
     963         1060 :     while ((ent = readdir(dp)) != NULL) {
     964          957 :         const char *name = ent->d_name;
     965          957 :         size_t nlen = strlen(name);
     966          957 :         if (nlen <= 4 || strcmp(name + nlen - 4, ".tsv") != 0) continue;
     967          751 :         RAII_STRING char *folder = strndup(name, nlen - 4);
     968          751 :         if (!folder) continue;
     969          751 :         Manifest *m = manifest_load(folder);
     970          751 :         if (!m) continue;
     971         1509 :         for (int i = 0; i < m->count; i++) {
     972          758 :             int f = m->entries[i].flags;
     973          758 :             if (unread_out   && (f & MSG_FLAG_UNSEEN))    (*unread_out)++;
     974          758 :             if (flagged_out  && (f & MSG_FLAG_FLAGGED))   (*flagged_out)++;
     975          758 :             if (junk_out     && (f & MSG_FLAG_JUNK))      (*junk_out)++;
     976          758 :             if (phishing_out && (f & MSG_FLAG_PHISHING))  (*phishing_out)++;
     977          758 :             if (answered_out && (f & MSG_FLAG_ANSWERED))  (*answered_out)++;
     978          758 :             if (forwarded_out&& (f & MSG_FLAG_FORWARDED)) (*forwarded_out)++;
     979              :         }
     980          751 :         manifest_free(m);
     981              :     }
     982              : }
     983              : 
     984              : /* ── Cross-folder flag search ────────────────────────────────────────── */
     985              : 
     986           13 : int local_flag_search(int flag_mask,
     987              :                       SearchResult **results_out, int *count_out)
     988              : {
     989           13 :     *results_out = NULL;
     990           13 :     *count_out   = 0;
     991           13 :     if (!g_account_base[0]) return 0;
     992              : 
     993           13 :     int cap = 64, cnt = 0;
     994           13 :     SearchResult *res = malloc((size_t)cap * sizeof(SearchResult));
     995           13 :     if (!res) return -1;
     996              : 
     997              :     char dir_path[8300];
     998           13 :     snprintf(dir_path, sizeof(dir_path), "%s/manifests", g_account_base);
     999           26 :     RAII_DIR DIR *dp = opendir(dir_path);
    1000           13 :     if (!dp) { free(res); return 0; }
    1001              : 
    1002              :     struct dirent *ent;
    1003          143 :     while ((ent = readdir(dp)) != NULL) {
    1004          130 :         const char *name = ent->d_name;
    1005          130 :         size_t nlen = strlen(name);
    1006          130 :         if (nlen <= 4 || strcmp(name + nlen - 4, ".tsv") != 0) continue;
    1007          104 :         RAII_STRING char *folder = strndup(name, nlen - 4);
    1008          104 :         if (!folder) continue;
    1009          104 :         Manifest *m = manifest_load(folder);
    1010          104 :         if (!m) continue;
    1011          213 :         for (int i = 0; i < m->count; i++) {
    1012          109 :             if (!(m->entries[i].flags & flag_mask)) continue;
    1013           50 :             if (cnt == cap) {
    1014            0 :                 int nc = cap * 2;
    1015            0 :                 SearchResult *tmp = realloc(res, (size_t)nc * sizeof(SearchResult));
    1016            0 :                 if (!tmp) { manifest_free(m); free(res); return -1; }
    1017            0 :                 res = tmp; cap = nc;
    1018              :             }
    1019           50 :             SearchResult *r = &res[cnt++];
    1020           50 :             snprintf(r->uid,    sizeof(r->uid),    "%s", m->entries[i].uid);
    1021           50 :             snprintf(r->folder, sizeof(r->folder), "%s", folder);
    1022           50 :             r->flags   = m->entries[i].flags;
    1023           50 :             r->from    = strdup(m->entries[i].from    ? m->entries[i].from    : "");
    1024           50 :             r->subject = strdup(m->entries[i].subject ? m->entries[i].subject : "");
    1025           50 :             r->date    = strdup(m->entries[i].date    ? m->entries[i].date    : "");
    1026              :         }
    1027          104 :         manifest_free(m);
    1028              :     }
    1029           13 :     *results_out = res;
    1030           13 :     *count_out   = cnt;
    1031           13 :     return 0;
    1032              : }
    1033              : 
    1034              : /* ── Cross-folder text search ─────────────────────────────────────────── */
    1035              : 
    1036            5 : int local_search(const char *query, int scope,
    1037              :                  SearchResult **results_out, int *count_out)
    1038              : {
    1039            5 :     *results_out = NULL;
    1040            5 :     *count_out   = 0;
    1041            5 :     if (!query || !query[0] || !g_account_base[0]) return 0;
    1042              : 
    1043              :     char dir_path[8300];
    1044            5 :     snprintf(dir_path, sizeof(dir_path), "%s/manifests", g_account_base);
    1045           10 :     RAII_DIR DIR *dp = opendir(dir_path);
    1046            5 :     if (!dp) return 0;   /* no manifests — not an error */
    1047              : 
    1048            5 :     int cap = 64;
    1049            5 :     SearchResult *results = malloc((size_t)cap * sizeof(SearchResult));
    1050            5 :     if (!results) return -1;
    1051            5 :     int count = 0;
    1052              : 
    1053              :     struct dirent *ent;
    1054           55 :     while ((ent = readdir(dp)) != NULL) {
    1055           50 :         const char *name = ent->d_name;
    1056           50 :         size_t nlen = strlen(name);
    1057           50 :         if (nlen <= 4 || strcmp(name + nlen - 4, ".tsv") != 0) continue;
    1058              : 
    1059           40 :         RAII_STRING char *fold = strndup(name, nlen - 4);
    1060           40 :         if (!fold) continue;
    1061              : 
    1062           40 :         Manifest *m = manifest_load(fold);
    1063           40 :         if (!m) continue;
    1064              : 
    1065           80 :         for (int i = 0; i < m->count; i++) {
    1066           40 :             ManifestEntry *me = &m->entries[i];
    1067           40 :             int match = 0;
    1068           40 :             if (scope == 0) {
    1069           16 :                 const char *s = (me->subject && me->subject[0]) ? me->subject : "";
    1070           16 :                 match = strcasestr(s, query) != NULL;
    1071           24 :             } else if (scope == 1) {
    1072            8 :                 const char *s = (me->from && me->from[0]) ? me->from : "";
    1073            8 :                 match = strcasestr(s, query) != NULL;
    1074           16 :             } else if (scope == 2) {
    1075            8 :                 char *hdr = local_hdr_load(fold, me->uid);
    1076            8 :                 if (hdr) {
    1077            8 :                     char *to_raw = mime_get_header(hdr, "To");
    1078            8 :                     if (to_raw) { match = strcasestr(to_raw, query) != NULL; free(to_raw); }
    1079            8 :                     free(hdr);
    1080              :                 }
    1081              :             } else {
    1082            8 :                 char *body = local_msg_load(fold, me->uid);
    1083            8 :                 if (body) { match = strcasestr(body, query) != NULL; free(body); }
    1084              :             }
    1085           40 :             if (!match) continue;
    1086              : 
    1087           15 :             if (count >= cap) {
    1088            0 :                 cap *= 2;
    1089            0 :                 SearchResult *tmp = realloc(results, (size_t)cap * sizeof(SearchResult));
    1090            0 :                 if (!tmp) { manifest_free(m); free(results); return -1; }
    1091            0 :                 results = tmp;
    1092              :             }
    1093           15 :             SearchResult *r = &results[count++];
    1094           15 :             memcpy(r->uid, me->uid, 17);
    1095           15 :             snprintf(r->folder, sizeof(r->folder), "%s", fold);
    1096           15 :             r->flags   = me->flags;
    1097           15 :             r->from    = me->from    ? strdup(me->from)    : strdup("");
    1098           15 :             r->subject = me->subject ? strdup(me->subject) : strdup("");
    1099           15 :             r->date    = me->date    ? strdup(me->date)    : strdup("");
    1100              :         }
    1101           40 :         manifest_free(m);
    1102              :     }
    1103              : 
    1104            5 :     *results_out = results;
    1105            5 :     *count_out   = count;
    1106            5 :     return 0;
    1107              : }
    1108              : 
    1109            5 : void local_search_free(SearchResult *results, int count)
    1110              : {
    1111            5 :     if (!results) return;
    1112           20 :     for (int i = 0; i < count; i++) {
    1113           15 :         free(results[i].from);
    1114           15 :         free(results[i].subject);
    1115           15 :         free(results[i].date);
    1116              :     }
    1117            5 :     free(results);
    1118              : }
    1119              : 
    1120              : /* ── Pending flag changes ─────────────────────────────────────────────── */
    1121              : 
    1122          143 : static char *pending_flag_path(const char *folder) {
    1123          143 :     if (!g_account_base[0]) return NULL;
    1124          143 :     char *path = NULL;
    1125          143 :     if (asprintf(&path, "%s/pending_flags/%s.tsv", g_account_base, folder) == -1)
    1126            0 :         return NULL;
    1127          143 :     return path;
    1128              : }
    1129              : 
    1130           30 : int local_pending_flag_add(const char *folder, const char *uid,
    1131              :                             const char *flag_name, int add) {
    1132           60 :     RAII_STRING char *path = pending_flag_path(folder);
    1133           30 :     if (!path) return -1;
    1134              : 
    1135              :     /* Ensure parent directory exists (folder path may have slashes) */
    1136           30 :     char *dir_end = strrchr(path, '/');
    1137           30 :     if (dir_end) {
    1138           30 :         char saved = *dir_end;
    1139           30 :         *dir_end = '\0';
    1140           30 :         fs_mkdir_p(path, 0700);
    1141           30 :         *dir_end = saved;
    1142              :     }
    1143              : 
    1144           60 :     RAII_FILE FILE *fp = fopen(path, "a");
    1145           30 :     if (!fp) return -1;
    1146           30 :     fprintf(fp, "%s\t%s\t%d\n", uid, flag_name, add);
    1147           30 :     return 0;
    1148              : }
    1149              : 
    1150          112 : PendingFlag *local_pending_flag_load(const char *folder, int *count_out) {
    1151          112 :     *count_out = 0;
    1152          224 :     RAII_STRING char *path = pending_flag_path(folder);
    1153          112 :     if (!path) return NULL;
    1154              : 
    1155          224 :     RAII_FILE FILE *fp = fopen(path, "r");
    1156          112 :     if (!fp) return NULL;
    1157              : 
    1158            1 :     int cap = 16, count = 0;
    1159            1 :     PendingFlag *arr = malloc((size_t)cap * sizeof(PendingFlag));
    1160            1 :     if (!arr) return NULL;
    1161              : 
    1162              :     char line[256];
    1163            5 :     while (fgets(line, sizeof(line), fp)) {
    1164              :         int add_val;
    1165              :         char uid_str[17], flag[64];
    1166            4 :         if (sscanf(line, "%16[^\t]\t%63[^\t]\t%d", uid_str, flag, &add_val) != 3)
    1167            0 :             continue;
    1168            4 :         if (count == cap) {
    1169            0 :             cap *= 2;
    1170            0 :             PendingFlag *tmp = realloc(arr, (size_t)cap * sizeof(PendingFlag));
    1171            0 :             if (!tmp) break;
    1172            0 :             arr = tmp;
    1173              :         }
    1174            4 :         snprintf(arr[count].uid, sizeof(arr[count].uid), "%s", uid_str);
    1175            4 :         arr[count].add = add_val;
    1176            4 :         strncpy(arr[count].flag_name, flag, sizeof(arr[count].flag_name) - 1);
    1177            4 :         arr[count].flag_name[sizeof(arr[count].flag_name) - 1] = '\0';
    1178            4 :         count++;
    1179              :     }
    1180            1 :     *count_out = count;
    1181            1 :     return arr;
    1182              : }
    1183              : 
    1184            1 : void local_pending_flag_clear(const char *folder) {
    1185            2 :     RAII_STRING char *path = pending_flag_path(folder);
    1186            1 :     if (path) remove(path);
    1187            1 : }
    1188              : 
    1189              : /* ── Pending folder moves ─────────────────────────────────────────────── */
    1190              : 
    1191          112 : static char *pending_move_path(const char *folder) {
    1192          112 :     if (!g_account_base[0]) return NULL;
    1193          112 :     char *path = NULL;
    1194          112 :     if (asprintf(&path, "%s/pending_moves/%s.tsv", g_account_base, folder) == -1)
    1195            0 :         return NULL;
    1196          112 :     return path;
    1197              : }
    1198              : 
    1199            0 : int local_pending_move_add(const char *folder, const char *uid,
    1200              :                             const char *target_folder) {
    1201            0 :     RAII_STRING char *path = pending_move_path(folder);
    1202            0 :     if (!path) return -1;
    1203            0 :     char *dir_end = strrchr(path, '/');
    1204            0 :     if (dir_end) {
    1205            0 :         char saved = *dir_end; *dir_end = '\0';
    1206            0 :         fs_mkdir_p(path, 0700);
    1207            0 :         *dir_end = saved;
    1208              :     }
    1209            0 :     RAII_FILE FILE *fp = fopen(path, "a");
    1210            0 :     if (!fp) return -1;
    1211            0 :     fprintf(fp, "%s\t%s\n", uid, target_folder);
    1212            0 :     return 0;
    1213              : }
    1214              : 
    1215          112 : PendingMove *local_pending_move_load(const char *folder, int *count_out) {
    1216          112 :     *count_out = 0;
    1217          224 :     RAII_STRING char *path = pending_move_path(folder);
    1218          112 :     if (!path) return NULL;
    1219          224 :     RAII_FILE FILE *fp = fopen(path, "r");
    1220          112 :     if (!fp) return NULL;
    1221            0 :     int cap = 16, count = 0;
    1222            0 :     PendingMove *arr = malloc((size_t)cap * sizeof(PendingMove));
    1223            0 :     if (!arr) return NULL;
    1224              :     char line[512];
    1225            0 :     while (fgets(line, sizeof(line), fp)) {
    1226              :         char uid_str[17], tgt[256];
    1227            0 :         if (sscanf(line, "%16[^\t]\t%255[^\n]", uid_str, tgt) != 2)
    1228            0 :             continue;
    1229            0 :         if (count == cap) {
    1230            0 :             cap *= 2;
    1231            0 :             PendingMove *tmp = realloc(arr, (size_t)cap * sizeof(PendingMove));
    1232            0 :             if (!tmp) break;
    1233            0 :             arr = tmp;
    1234              :         }
    1235            0 :         snprintf(arr[count].uid, sizeof(arr[count].uid), "%s", uid_str);
    1236            0 :         snprintf(arr[count].target_folder, sizeof(arr[count].target_folder), "%s", tgt);
    1237            0 :         count++;
    1238              :     }
    1239            0 :     *count_out = count;
    1240            0 :     return arr;
    1241              : }
    1242              : 
    1243            0 : void local_pending_move_clear(const char *folder) {
    1244            0 :     RAII_STRING char *path = pending_move_path(folder);
    1245            0 :     if (path) remove(path);
    1246            0 : }
    1247              : 
    1248              : /* ── Gmail label index files (.idx) ──────────────────────────────────── */
    1249              : 
    1250              : #define IDX_RECORD_SIZE 17  /* 16 char UID + '\n' */
    1251              : 
    1252              : /** @brief Returns heap-allocated path to labels/<label>.idx. */
    1253         3220 : static char *label_idx_path(const char *label) {
    1254         3220 :     if (!g_account_base[0] || !label) return NULL;
    1255         3220 :     char *path = NULL;
    1256         3220 :     if (asprintf(&path, "%s/labels/%s.idx", g_account_base, label) == -1)
    1257            0 :         return NULL;
    1258         3220 :     return path;
    1259              : }
    1260              : 
    1261              : /** @brief Ensures the labels/ directory (and any parent for nested labels) exists. */
    1262         1100 : static int ensure_label_dir(const char *label) {
    1263         2200 :     RAII_STRING char *path = label_idx_path(label);
    1264         1100 :     if (!path) return -1;
    1265              :     /* Find last slash and mkdir_p up to it */
    1266         1100 :     char *last_slash = strrchr(path, '/');
    1267         1100 :     if (!last_slash) return -1;
    1268         1100 :     *last_slash = '\0';
    1269         1100 :     int rc = fs_mkdir_p(path, 0700);
    1270         1100 :     return rc;
    1271              : }
    1272              : 
    1273            0 : int label_idx_contains(const char *label, const char *uid) {
    1274            0 :     char (*arr)[17] = NULL;
    1275            0 :     int n = 0;
    1276            0 :     if (label_idx_load(label, &arr, &n) != 0 || n == 0) {
    1277            0 :         free(arr);
    1278            0 :         return 0;
    1279              :     }
    1280              : 
    1281              :     /* In-memory binary search (file is kept sorted) */
    1282            0 :     int lo = 0, hi = n - 1, found = 0;
    1283            0 :     while (lo <= hi) {
    1284            0 :         int mid = lo + (hi - lo) / 2;
    1285            0 :         int cmp = strcmp(arr[mid], uid);
    1286            0 :         if (cmp == 0) { found = 1; break; }
    1287            0 :         if (cmp < 0) lo = mid + 1;
    1288            0 :         else          hi = mid - 1;
    1289              :     }
    1290            0 :     free(arr);
    1291            0 :     return found;
    1292              : }
    1293              : 
    1294           14 : int label_idx_count(const char *label) {
    1295           14 :     char (*arr)[17] = NULL;
    1296           14 :     int n = 0;
    1297           14 :     label_idx_load(label, &arr, &n);
    1298           14 :     free(arr);
    1299           14 :     return n;
    1300              : }
    1301              : 
    1302            0 : int label_idx_intersect_count(const char *label_a,
    1303              :                                const char (*b_uids)[17], int b_count) {
    1304            0 :     if (!label_a || b_count <= 0 || !b_uids) return 0;
    1305            0 :     char (*a_uids)[17] = NULL;
    1306            0 :     int a_count = 0;
    1307            0 :     if (label_idx_load(label_a, &a_uids, &a_count) != 0 || a_count == 0) {
    1308            0 :         free(a_uids);
    1309            0 :         return 0;
    1310              :     }
    1311              :     /* Merge-join on two sorted arrays — O(N+M). */
    1312            0 :     int i = 0, j = 0, matches = 0;
    1313            0 :     while (i < a_count && j < b_count) {
    1314            0 :         int cmp = strcmp(a_uids[i], b_uids[j]);
    1315            0 :         if      (cmp == 0) { matches++; i++; j++; }
    1316            0 :         else if (cmp  < 0) { i++; }
    1317            0 :         else               { j++; }
    1318              :     }
    1319            0 :     free(a_uids);
    1320            0 :     return matches;
    1321              : }
    1322              : 
    1323         1020 : int label_idx_load(const char *label, char (**uids_out)[17], int *count_out) {
    1324         1020 :     *uids_out  = NULL;
    1325         1020 :     *count_out = 0;
    1326              : 
    1327         2040 :     RAII_STRING char *path = label_idx_path(label);
    1328         1020 :     if (!path) return -1;
    1329              : 
    1330         2040 :     RAII_FILE FILE *fp = fopen(path, "r");
    1331         1020 :     if (!fp) return 0;  /* Empty / nonexistent label → 0 entries, not error */
    1332              : 
    1333              :     /* Use fgets-based reading to handle both old variable-length format
    1334              :      * (where short Gmail IDs < 16 chars were stored without NUL padding)
    1335              :      * and new fixed-width format (16 NUL-padded bytes + '\n'). */
    1336          975 :     int cap = 256;
    1337          975 :     char (*arr)[17] = malloc((size_t)cap * sizeof(char[17]));
    1338          975 :     if (!arr) return -1;
    1339              : 
    1340          975 :     int count = 0;
    1341              :     char line[64];
    1342        60552 :     while (fgets(line, sizeof(line), fp)) {
    1343              :         /* fgets stops at '\n'; strip trailing whitespace/newline */
    1344        59577 :         size_t len = strlen(line);
    1345       119154 :         while (len > 0 && ((unsigned char)line[len-1] <= ' '))
    1346        59577 :             line[--len] = '\0';
    1347        59577 :         if (len == 0 || len > 16) continue;
    1348              : 
    1349        59577 :         if (count >= cap) {
    1350            0 :             cap *= 2;
    1351            0 :             char (*tmp)[17] = realloc(arr, (size_t)cap * sizeof(char[17]));
    1352            0 :             if (!tmp) { free(arr); return -1; }
    1353            0 :             arr = tmp;
    1354              :         }
    1355        59577 :         memset(arr[count], 0, sizeof(arr[count]));
    1356        59577 :         memcpy(arr[count], line, len);
    1357        59577 :         count++;
    1358              :     }
    1359              : 
    1360          975 :     *uids_out  = arr;
    1361          975 :     *count_out = count;
    1362          975 :     return 0;
    1363              : }
    1364              : 
    1365         1100 : int label_idx_write(const char *label, const char (*uids)[17], int count) {
    1366         1100 :     if (ensure_label_dir(label) != 0) return -1;
    1367              : 
    1368         2200 :     RAII_STRING char *path = label_idx_path(label);
    1369         1100 :     if (!path) return -1;
    1370              : 
    1371         2200 :     RAII_FILE FILE *fp = fopen(path, "w");
    1372         1100 :     if (!fp) return -1;
    1373              : 
    1374              :     /* Write fixed-width records: exactly 16 NUL-padded bytes + '\n' = 17 bytes.
    1375              :      * Short Gmail IDs (< 16 chars) are padded with NUL so the record size is
    1376              :      * always 17 bytes, preventing embedded newlines on read-back. */
    1377        65983 :     for (int i = 0; i < count; i++) {
    1378              :         char padded[17];
    1379        64883 :         size_t uid_len = strlen(uids[i]);
    1380        64883 :         if (uid_len > 16) uid_len = 16;
    1381        64883 :         memset(padded, 0, 16);
    1382        64883 :         memcpy(padded, uids[i], uid_len);
    1383        64883 :         padded[16] = '\n';
    1384        64883 :         if (fwrite(padded, 1, 17, fp) != 17) return -1;
    1385              :     }
    1386              : 
    1387         1100 :     logger_log(LOG_DEBUG, "label_idx_write: %s → %d entries", label, count);
    1388         1100 :     return 0;
    1389              : }
    1390              : 
    1391            6 : char *local_hdr_get_labels(const char *folder, const char *uid) {
    1392            6 :     char *hdr = local_hdr_load(folder, uid);
    1393            6 :     if (!hdr) return NULL;
    1394              : 
    1395              :     /* Parse 4th tab-separated field: from\tsubject\tdate\tLABELS\tflags */
    1396            6 :     const char *p = hdr;
    1397           24 :     for (int t = 0; t < 3; t++) {
    1398           18 :         p = strchr(p, '\t');
    1399           18 :         if (!p) { free(hdr); return NULL; }
    1400           18 :         p++;
    1401              :     }
    1402              :     /* p now points to the start of the labels field */
    1403            6 :     const char *end = strchr(p, '\t');
    1404            6 :     size_t len = end ? (size_t)(end - p) : strlen(p);
    1405            6 :     char *result = strndup(p, len);
    1406            6 :     free(hdr);
    1407            6 :     return result;
    1408              : }
    1409              : 
    1410            1 : int label_idx_list(char ***labels_out, int *count_out) {
    1411            1 :     *labels_out = NULL;
    1412            1 :     *count_out  = 0;
    1413              : 
    1414              :     char dir_path[8300];
    1415            1 :     snprintf(dir_path, sizeof(dir_path), "%s/labels", g_account_base);
    1416              : 
    1417            2 :     RAII_DIR DIR *dp = opendir(dir_path);
    1418            1 :     if (!dp) return 0;  /* No labels directory → 0 labels */
    1419              : 
    1420            1 :     char **list = NULL;
    1421            1 :     int count = 0, cap = 0;
    1422              : 
    1423              :     struct dirent *ent;
    1424            4 :     while ((ent = readdir(dp)) != NULL) {
    1425            3 :         const char *name = ent->d_name;
    1426            3 :         size_t nlen = strlen(name);
    1427            3 :         if (nlen <= 4) continue;
    1428            1 :         if (strcmp(name + nlen - 4, ".idx") != 0) continue;
    1429              : 
    1430              :         /* Extract label name (strip .idx) */
    1431            1 :         char *label = strndup(name, nlen - 4);
    1432            1 :         if (!label) continue;
    1433              : 
    1434            1 :         if (count == cap) {
    1435            1 :             int newcap = cap ? cap * 2 : 16;
    1436            1 :             char **tmp = realloc(list, (size_t)newcap * sizeof(char *));
    1437            1 :             if (!tmp) { free(label); break; }
    1438            1 :             list = tmp;
    1439            1 :             cap = newcap;
    1440              :         }
    1441            1 :         list[count++] = label;
    1442              :     }
    1443              : 
    1444            1 :     *labels_out = list;
    1445            1 :     *count_out  = count;
    1446            1 :     return 0;
    1447              : }
    1448              : 
    1449          983 : int label_idx_add(const char *label, const char *uid) {
    1450          983 :     if (!uid || strlen(uid) < 1) return -1;
    1451              : 
    1452              :     /* Load existing entries */
    1453          983 :     char (*existing)[17] = NULL;
    1454          983 :     int ecount = 0;
    1455          983 :     label_idx_load(label, &existing, &ecount);
    1456              : 
    1457              :     /* Check if already present (binary search) */
    1458          983 :     int lo = 0, hi = ecount - 1, insert_pos = ecount;
    1459         6283 :     while (lo <= hi) {
    1460         5302 :         int mid = lo + (hi - lo) / 2;
    1461         5302 :         int cmp = strcmp(existing[mid], uid);
    1462         5302 :         if (cmp == 0) { free(existing); return 0; }  /* Already present */
    1463         5300 :         if (cmp < 0) lo = mid + 1;
    1464           38 :         else { insert_pos = mid; hi = mid - 1; }
    1465              :     }
    1466          981 :     if (lo < ecount && insert_pos == ecount) insert_pos = lo;
    1467              : 
    1468              :     /* Build new array with uid inserted at insert_pos */
    1469          981 :     int newcount = ecount + 1;
    1470          981 :     char (*arr)[17] = malloc((size_t)newcount * sizeof(char[17]));
    1471          981 :     if (!arr) { free(existing); return -1; }
    1472              : 
    1473          981 :     if (insert_pos > 0 && existing)
    1474          946 :         memcpy(arr, existing, (size_t)insert_pos * sizeof(char[17]));
    1475          981 :     snprintf(arr[insert_pos], 17, "%.16s", uid);
    1476          981 :     if (insert_pos < ecount && existing)
    1477            7 :         memcpy(arr + insert_pos + 1, existing + insert_pos,
    1478            7 :                (size_t)(ecount - insert_pos) * sizeof(char[17]));
    1479          981 :     free(existing);
    1480              : 
    1481          981 :     int rc = label_idx_write(label, (const char (*)[17])arr, newcount);
    1482          981 :     free(arr);
    1483          981 :     return rc;
    1484              : }
    1485              : 
    1486           11 : int label_idx_remove(const char *label, const char *uid) {
    1487           11 :     if (!uid) return -1;
    1488              : 
    1489           11 :     char (*existing)[17] = NULL;
    1490           11 :     int ecount = 0;
    1491           11 :     label_idx_load(label, &existing, &ecount);
    1492           11 :     if (!existing || ecount == 0) { free(existing); return 0; }
    1493              : 
    1494              :     /* Find uid with binary search */
    1495           11 :     int lo = 0, hi = ecount - 1, found = -1;
    1496           43 :     while (lo <= hi) {
    1497           42 :         int mid = lo + (hi - lo) / 2;
    1498           42 :         int cmp = strcmp(existing[mid], uid);
    1499           42 :         if (cmp == 0) { found = mid; break; }
    1500           32 :         if (cmp < 0) lo = mid + 1;
    1501           32 :         else          hi = mid - 1;
    1502              :     }
    1503              : 
    1504           11 :     if (found < 0) { free(existing); return 0; }  /* Not present */
    1505              : 
    1506              :     /* Shift down */
    1507           10 :     if (found < ecount - 1)
    1508            8 :         memmove(existing + found, existing + found + 1,
    1509            8 :                 (size_t)(ecount - found - 1) * sizeof(char[17]));
    1510              : 
    1511           10 :     int rc = label_idx_write(label, (const char (*)[17])existing, ecount - 1);
    1512           10 :     free(existing);
    1513           10 :     return rc;
    1514              : }
    1515              : 
    1516              : /* ── Gmail history ID ─────────────────────────────────────────────── */
    1517              : 
    1518              : /* ── Trash label backup (for untrash restore) ────────────────────── */
    1519              : 
    1520            0 : static char *trash_labels_path(const char *uid) {
    1521            0 :     if (!g_account_base[0] || !uid) return NULL;
    1522            0 :     char *path = NULL;
    1523            0 :     if (asprintf(&path, "%s/trash_labels/%s.lbl", g_account_base, uid) == -1)
    1524            0 :         return NULL;
    1525            0 :     return path;
    1526              : }
    1527              : 
    1528            0 : int local_trash_labels_save(const char *uid, const char *labels) {
    1529            0 :     if (!uid || !labels) return -1;
    1530              :     /* Ensure directory exists */
    1531              :     char dir[8300];
    1532            0 :     snprintf(dir, sizeof(dir), "%s/trash_labels", g_account_base);
    1533            0 :     fs_mkdir_p(dir, 0700);
    1534              : 
    1535            0 :     RAII_STRING char *path = trash_labels_path(uid);
    1536            0 :     if (!path) return -1;
    1537            0 :     RAII_FILE FILE *fp = fopen(path, "w");
    1538            0 :     if (!fp) return -1;
    1539            0 :     fprintf(fp, "%s\n", labels);
    1540            0 :     return 0;
    1541              : }
    1542              : 
    1543            0 : char *local_trash_labels_load(const char *uid) {
    1544            0 :     RAII_STRING char *path = trash_labels_path(uid);
    1545            0 :     if (!path) return NULL;
    1546            0 :     RAII_FILE FILE *fp = fopen(path, "r");
    1547            0 :     if (!fp) return NULL;
    1548              :     char buf[4096];
    1549            0 :     if (!fgets(buf, (int)sizeof(buf), fp)) return NULL;
    1550            0 :     buf[strcspn(buf, "\r\n")] = '\0';
    1551            0 :     return strdup(buf);
    1552              : }
    1553              : 
    1554            0 : void local_trash_labels_remove(const char *uid) {
    1555            0 :     RAII_STRING char *path = trash_labels_path(uid);
    1556            0 :     if (path) unlink(path);
    1557            0 : }
    1558              : 
    1559           18 : int local_gmail_label_names_save(char **ids, char **names, int count) {
    1560           18 :     if (!g_account_base[0]) return -1;
    1561           18 :     if (fs_mkdir_p(g_account_base, 0700) != 0) return -1;
    1562           18 :     RAII_STRING char *path = NULL;
    1563           18 :     if (asprintf(&path, "%s/gmail_label_names", g_account_base) == -1) return -1;
    1564           36 :     RAII_FILE FILE *fp = fopen(path, "w");
    1565           18 :     if (!fp) return -1;
    1566          180 :     for (int i = 0; i < count; i++)
    1567          162 :         fprintf(fp, "%s\t%s\n", ids[i], names[i]);
    1568           18 :     return 0;
    1569              : }
    1570              : 
    1571            8 : char *local_gmail_label_name_lookup(const char *id) {
    1572            8 :     if (!g_account_base[0] || !id) return NULL;
    1573            8 :     RAII_STRING char *path = NULL;
    1574            8 :     if (asprintf(&path, "%s/gmail_label_names", g_account_base) == -1) return NULL;
    1575           16 :     RAII_FILE FILE *fp = fopen(path, "r");
    1576            8 :     if (!fp) return NULL;
    1577              :     char buf[1024];
    1578            8 :     while (fgets(buf, (int)sizeof(buf), fp)) {
    1579            8 :         buf[strcspn(buf, "\r\n")] = '\0';
    1580            8 :         char *tab = strchr(buf, '\t');
    1581            8 :         if (!tab) continue;
    1582            8 :         *tab = '\0';
    1583            8 :         if (strcmp(buf, id) == 0)
    1584            8 :             return strdup(tab + 1);
    1585              :     }
    1586            0 :     return NULL;
    1587              : }
    1588              : 
    1589            8 : char *local_gmail_label_id_lookup(const char *name) {
    1590            8 :     if (!g_account_base[0] || !name) return NULL;
    1591            8 :     RAII_STRING char *path = NULL;
    1592            8 :     if (asprintf(&path, "%s/gmail_label_names", g_account_base) == -1) return NULL;
    1593           16 :     RAII_FILE FILE *fp = fopen(path, "r");
    1594            8 :     if (!fp) return NULL;
    1595              :     char buf[1024];
    1596            8 :     while (fgets(buf, (int)sizeof(buf), fp)) {
    1597            8 :         buf[strcspn(buf, "\r\n")] = '\0';
    1598            8 :         char *tab = strchr(buf, '\t');
    1599            8 :         if (!tab) continue;
    1600            8 :         *tab = '\0';
    1601            8 :         if (strcasecmp(tab + 1, name) == 0)
    1602            8 :             return strdup(buf); /* return the ID */
    1603              :     }
    1604            0 :     return NULL;
    1605              : }
    1606              : 
    1607           22 : int local_gmail_history_save(const char *history_id) {
    1608           22 :     if (!g_account_base[0] || !history_id) return -1;
    1609           22 :     if (fs_mkdir_p(g_account_base, 0700) != 0) return -1;
    1610           22 :     RAII_STRING char *path = NULL;
    1611           22 :     if (asprintf(&path, "%s/gmail_history_id", g_account_base) == -1) return -1;
    1612           22 :     return write_file(path, history_id, strlen(history_id));
    1613              : }
    1614              : 
    1615           27 : char *local_gmail_history_load(void) {
    1616           27 :     if (!g_account_base[0]) return NULL;
    1617           27 :     RAII_STRING char *path = NULL;
    1618           27 :     if (asprintf(&path, "%s/gmail_history_id", g_account_base) == -1) return NULL;
    1619           27 :     char *data = load_file(path);
    1620           27 :     if (!data) return NULL;
    1621              :     /* Trim trailing whitespace */
    1622           12 :     size_t len = strlen(data);
    1623           12 :     while (len > 0 && (data[len-1] == '\n' || data[len-1] == '\r' || data[len-1] == ' '))
    1624            0 :         data[--len] = '\0';
    1625           12 :     return data;
    1626              : }
    1627              : 
    1628              : /* ── Contact suggestion cache ────────────────────────────────────────── */
    1629              : 
    1630              : /** Extract all "addr" tokens from a comma/semicolon-separated RFC 2822
    1631              :  *  address list like  "Alice B <alice@x.com>, bob@y.com" .
    1632              :  *  Calls cb(addr, display_name, userdata) for each address found.
    1633              :  *  Addresses longer than 255 bytes are silently skipped. */
    1634          480 : static void parse_addr_list(const char *hdr,
    1635              :                              void (*cb)(const char *, const char *, void *),
    1636              :                              void *ud) {
    1637          480 :     if (!hdr || !hdr[0]) return;
    1638              :     /* Walk comma-separated tokens */
    1639              :     char buf[512];
    1640          160 :     const char *p = hdr;
    1641          320 :     while (*p) {
    1642              :         /* skip leading whitespace / commas / semicolons */
    1643          320 :         while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' ||
    1644          320 :                *p == ',' || *p == ';') p++;
    1645          160 :         if (!*p) break;
    1646              : 
    1647              :         /* Copy until the next top-level comma (respecting quoted strings
    1648              :          * and angle-bracket groups). */
    1649          160 :         int depth = 0; int in_q = 0; const char *start = p;
    1650          160 :         size_t i = 0;
    1651         5790 :         while (*p) {
    1652         5630 :             if (*p == '"') { in_q = !in_q; }
    1653         5630 :             else if (!in_q && *p == '<') { depth++; }
    1654         5470 :             else if (!in_q && *p == '>') { depth--; }
    1655         5310 :             else if (!in_q && depth == 0 && (*p == ',' || *p == ';')) break;
    1656         5630 :             if (i < sizeof(buf) - 1) buf[i++] = *p;
    1657         5630 :             p++;
    1658              :         }
    1659          160 :         buf[i] = '\0';
    1660          160 :         if (buf[0] == '\0') continue;
    1661              : 
    1662              :         /* Extract: "Display Name <addr>" or bare "addr" */
    1663          160 :         char addr[256] = ""; char name[256] = "";
    1664          160 :         char *lt = strchr(buf, '<');
    1665          160 :         char *gt = lt ? strchr(lt, '>') : NULL;
    1666          320 :         if (lt && gt) {
    1667          160 :             size_t alen = (size_t)(gt - lt - 1);
    1668          160 :             if (alen < sizeof(addr)) {
    1669          160 :                 memcpy(addr, lt + 1, alen); addr[alen] = '\0';
    1670              :             }
    1671              :             /* display name: everything before '<', trimmed, dequoted */
    1672          160 :             size_t nlen = (size_t)(lt - buf);
    1673          160 :             if (nlen > 0 && nlen < sizeof(name)) {
    1674          160 :                 memcpy(name, buf, nlen); name[nlen] = '\0';
    1675              :                 /* trim whitespace */
    1676          160 :                 char *ns = name;
    1677          160 :                 while (*ns == ' ' || *ns == '\t') ns++;
    1678          160 :                 char *ne = ns + strlen(ns);
    1679          320 :                 while (ne > ns && (*(ne-1) == ' ' || *(ne-1) == '\t' ||
    1680          320 :                                    *(ne-1) == '"')) ne--;
    1681          160 :                 if (*ns == '"') ns++;
    1682          160 :                 *ne = '\0';
    1683          160 :                 memmove(name, ns, strlen(ns) + 1);
    1684              :             }
    1685              :         } else {
    1686              :             /* bare address */
    1687            0 :             char *ns = buf;
    1688            0 :             while (*ns == ' ' || *ns == '\t') ns++;
    1689            0 :             char *ne = ns + strlen(ns);
    1690            0 :             while (ne > ns && (*(ne-1) == ' ' || *(ne-1) == '\t')) ne--;
    1691            0 :             size_t alen = (size_t)(ne - ns);
    1692            0 :             if (alen < sizeof(addr)) { memcpy(addr, ns, alen); addr[alen] = '\0'; }
    1693              :         }
    1694          160 :         if (addr[0]) cb(addr, name, ud);
    1695              :         (void)start;
    1696              :     }
    1697              : }
    1698              : 
    1699              : /* ---- contacts.tsv upsert ---- */
    1700              : 
    1701              : #define CONTACTS_MAX 4096
    1702              : 
    1703              : typedef struct {
    1704              :     char addr[256];
    1705              :     char name[128];
    1706              :     int  freq;
    1707              : } ContactEntry;
    1708              : 
    1709          778 : static int contact_cmp_freq(const void *a, const void *b) {
    1710          778 :     return ((const ContactEntry *)b)->freq - ((const ContactEntry *)a)->freq;
    1711              : }
    1712              : 
    1713              : typedef struct { ContactEntry *arr; int count; int cap; } ContactBuf;
    1714              : 
    1715          160 : static void contact_add_cb(const char *addr, const char *name, void *ud) {
    1716          160 :     ContactBuf *cb = (ContactBuf *)ud;
    1717          160 :     if (!addr || !addr[0]) return;
    1718              :     /* case-insensitive dedup on address */
    1719          426 :     for (int i = 0; i < cb->count; i++) {
    1720          403 :         if (strcasecmp(cb->arr[i].addr, addr) == 0) {
    1721          137 :             cb->arr[i].freq++;
    1722              :             /* update name if we now have one and didn't before */
    1723          137 :             if (name && name[0] && !cb->arr[i].name[0]) {
    1724            0 :                 size_t _n = strlen(name);
    1725            0 :                 if (_n >= sizeof(cb->arr[i].name)) _n = sizeof(cb->arr[i].name) - 1;
    1726            0 :                 memcpy(cb->arr[i].name, name, _n); cb->arr[i].name[_n] = '\0';
    1727              :             }
    1728          137 :             return;
    1729              :         }
    1730              :     }
    1731           23 :     if (cb->count >= cb->cap) return; /* full */
    1732           23 :     { size_t _a = strlen(addr); if (_a >= sizeof(cb->arr[cb->count].addr)) _a = sizeof(cb->arr[cb->count].addr) - 1;
    1733           23 :       memcpy(cb->arr[cb->count].addr, addr, _a); cb->arr[cb->count].addr[_a] = '\0'; }
    1734           23 :     { const char *_nm = name ? name : "";
    1735           23 :       size_t _n = strlen(_nm); if (_n >= sizeof(cb->arr[cb->count].name)) _n = sizeof(cb->arr[cb->count].name) - 1;
    1736           23 :       memcpy(cb->arr[cb->count].name, _nm, _n); cb->arr[cb->count].name[_n] = '\0'; }
    1737           23 :     cb->arr[cb->count].freq = 1;
    1738           23 :     cb->count++;
    1739              : }
    1740              : 
    1741            1 : void local_contacts_rebuild(void) {
    1742            1 :     const char *data_base = platform_data_dir();
    1743            1 :     if (!data_base || !g_account_name[0]) return;
    1744              : 
    1745            1 :     ContactEntry *arr = calloc(CONTACTS_MAX, sizeof(ContactEntry));
    1746            1 :     if (!arr) return;
    1747            1 :     ContactBuf cb = { arr, 0, CONTACTS_MAX };
    1748              : 
    1749            1 :     int fcount = 0;
    1750            1 :     char **folders = local_folder_list_load(&fcount, NULL);
    1751              : 
    1752            1 :     if (fcount > 0 && folders) {
    1753              :         /* IMAP account: .hdr files contain raw RFC 2822 headers */
    1754            9 :         for (int fi = 0; fi < fcount && cb.count < CONTACTS_MAX; fi++) {
    1755            8 :             char (*uids)[17] = NULL;
    1756            8 :             int uid_count = 0;
    1757            8 :             local_hdr_list_all_uids(folders[fi], &uids, &uid_count);
    1758           15 :             for (int u = 0; u < uid_count && cb.count < CONTACTS_MAX; u++) {
    1759            7 :                 char *raw = local_hdr_load(folders[fi], uids[u]);
    1760            7 :                 if (!raw) continue;
    1761            7 :                 char *from_h = mime_get_header(raw, "From");
    1762            7 :                 char *to_h   = mime_get_header(raw, "To");
    1763            7 :                 char *cc_h   = mime_get_header(raw, "Cc");
    1764            7 :                 parse_addr_list(from_h, contact_add_cb, &cb);
    1765            7 :                 parse_addr_list(to_h,   contact_add_cb, &cb);
    1766            7 :                 parse_addr_list(cc_h,   contact_add_cb, &cb);
    1767            7 :                 free(from_h); free(to_h); free(cc_h);
    1768            7 :                 free(raw);
    1769              :             }
    1770            8 :             free(uids);
    1771              :         }
    1772            9 :         for (int i = 0; i < fcount; i++) free(folders[i]);
    1773            1 :         free(folders);
    1774              :     } else {
    1775              :         /* Gmail account (or no folder cache): .hdr files are tab-separated;
    1776              :          * load full .eml files to extract From/To/Cc. */
    1777            0 :         if (folders) {
    1778            0 :             for (int i = 0; i < fcount; i++) free(folders[i]);
    1779            0 :             free(folders);
    1780              :         }
    1781            0 :         char (*uids)[17] = NULL;
    1782            0 :         int uid_count = 0;
    1783            0 :         local_hdr_list_all_uids("", &uids, &uid_count);
    1784            0 :         for (int u = 0; u < uid_count && cb.count < CONTACTS_MAX; u++) {
    1785            0 :             char *raw = local_msg_load("", uids[u]);
    1786            0 :             if (!raw) continue;
    1787            0 :             char *from_h = mime_get_header(raw, "From");
    1788            0 :             char *to_h   = mime_get_header(raw, "To");
    1789            0 :             char *cc_h   = mime_get_header(raw, "Cc");
    1790            0 :             parse_addr_list(from_h, contact_add_cb, &cb);
    1791            0 :             parse_addr_list(to_h,   contact_add_cb, &cb);
    1792            0 :             parse_addr_list(cc_h,   contact_add_cb, &cb);
    1793            0 :             free(from_h); free(to_h); free(cc_h);
    1794            0 :             free(raw);
    1795              :         }
    1796            0 :         free(uids);
    1797              :     }
    1798              : 
    1799            1 :     qsort(arr, (size_t)cb.count, sizeof(ContactEntry), contact_cmp_freq);
    1800              : 
    1801              :     char path[8192];
    1802            1 :     snprintf(path, sizeof(path), "%s/email-cli/accounts/%s/contacts.tsv",
    1803              :              data_base, g_account_name);
    1804            1 :     FILE *f = fopen(path, "w");
    1805            1 :     if (f) {
    1806            2 :         for (int i = 0; i < cb.count; i++)
    1807            1 :             fprintf(f, "%s\t%s\t%d\n", arr[i].addr, arr[i].name, arr[i].freq);
    1808            1 :         fclose(f);
    1809              :     }
    1810            1 :     printf("Contacts rebuilt: %d entries written to %s\n", cb.count, path);
    1811            1 :     free(arr);
    1812              : }
    1813              : 
    1814          153 : void local_contacts_update(const char *from_hdr,
    1815              :                             const char *to_hdr,
    1816              :                             const char *cc_hdr) {
    1817          153 :     const char *data_base = platform_data_dir();
    1818          153 :     if (!data_base || !g_account_name[0]) return;
    1819              : 
    1820              :     char path[8192];
    1821          153 :     snprintf(path, sizeof(path), "%s/email-cli/accounts/%s/contacts.tsv",
    1822              :              data_base, g_account_name);
    1823              : 
    1824              :     /* Load existing entries */
    1825          153 :     ContactEntry *arr = calloc(CONTACTS_MAX, sizeof(ContactEntry));
    1826          153 :     if (!arr) return;
    1827          153 :     ContactBuf cb = { arr, 0, CONTACTS_MAX };
    1828              : 
    1829          153 :     FILE *f = fopen(path, "r");
    1830          153 :     if (f) {
    1831              :         char line[512];
    1832          767 :         while (cb.count < CONTACTS_MAX && fgets(line, sizeof(line), f)) {
    1833              :             /* format: addr\tname\tfreq\n */
    1834          625 :             char *t1 = strchr(line, '\t');
    1835          625 :             if (!t1) continue;
    1836          625 :             *t1 = '\0';
    1837          625 :             char *t2 = strchr(t1 + 1, '\t');
    1838          625 :             char *name = t1 + 1;
    1839          625 :             int freq = 1;
    1840          625 :             if (t2) { *t2 = '\0'; freq = atoi(t2 + 1); if (freq < 1) freq = 1; }
    1841          625 :             char *nl = strchr(name, '\n'); if (nl) *nl = '\0';
    1842          625 :             size_t _al = strlen(line); if (_al >= sizeof(arr[cb.count].addr)) _al = sizeof(arr[cb.count].addr) - 1;
    1843          625 :             memcpy(arr[cb.count].addr, line, _al); arr[cb.count].addr[_al] = '\0';
    1844          625 :             size_t _nl = strlen(name); if (_nl >= sizeof(arr[cb.count].name)) _nl = sizeof(arr[cb.count].name) - 1;
    1845          625 :             memcpy(arr[cb.count].name, name, _nl); arr[cb.count].name[_nl] = '\0';
    1846          625 :             arr[cb.count].freq = freq;
    1847          625 :             cb.count++;
    1848              :         }
    1849          142 :         fclose(f);
    1850              :     }
    1851              : 
    1852              :     /* Add new addresses from headers */
    1853          153 :     parse_addr_list(from_hdr, contact_add_cb, &cb);
    1854          153 :     parse_addr_list(to_hdr,   contact_add_cb, &cb);
    1855          153 :     parse_addr_list(cc_hdr,   contact_add_cb, &cb);
    1856              : 
    1857              :     /* Sort by frequency descending */
    1858          153 :     qsort(arr, (size_t)cb.count, sizeof(ContactEntry), contact_cmp_freq);
    1859              : 
    1860              :     /* Write back */
    1861          153 :     f = fopen(path, "w");
    1862          153 :     if (f) {
    1863          800 :         for (int i = 0; i < cb.count; i++)
    1864          647 :             fprintf(f, "%s\t%s\t%d\n", arr[i].addr, arr[i].name, arr[i].freq);
    1865          153 :         fclose(f);
    1866              :     }
    1867          153 :     free(arr);
    1868              : }
    1869              : 
    1870              : /* ── Pending APPEND queue ────────────────────────────────────────────── */
    1871              : 
    1872           23 : static char *pending_append_path(void) {
    1873           23 :     if (!g_account_base[0]) return NULL;
    1874           23 :     char *path = NULL;
    1875           23 :     if (asprintf(&path, "%s/pending_appends.tsv", g_account_base) == -1)
    1876            0 :         return NULL;
    1877           23 :     return path;
    1878              : }
    1879              : 
    1880            3 : int local_pending_append_add(const char *folder, const char *uid) {
    1881            6 :     RAII_STRING char *path = pending_append_path();
    1882            3 :     if (!path) return -1;
    1883            6 :     RAII_FILE FILE *fp = fopen(path, "a");
    1884            3 :     if (!fp) return -1;
    1885            3 :     fprintf(fp, "%s\t%s\n", folder, uid);
    1886            3 :     return 0;
    1887              : }
    1888              : 
    1889           19 : PendingAppend *local_pending_append_load(int *count_out) {
    1890           19 :     *count_out = 0;
    1891           38 :     RAII_STRING char *path = pending_append_path();
    1892           19 :     if (!path) return NULL;
    1893           38 :     RAII_FILE FILE *fp = fopen(path, "r");
    1894           19 :     if (!fp) return NULL;
    1895              : 
    1896            1 :     int cap = 8, count = 0;
    1897            1 :     PendingAppend *arr = malloc((size_t)cap * sizeof(PendingAppend));
    1898            1 :     if (!arr) return NULL;
    1899              : 
    1900              :     char line[512];
    1901            2 :     while (fgets(line, sizeof(line), fp)) {
    1902            1 :         char *tab = strchr(line, '\t');
    1903            1 :         if (!tab) continue;
    1904            1 :         *tab = '\0';
    1905            1 :         char *nl = strchr(tab + 1, '\n'); if (nl) *nl = '\0';
    1906            1 :         if (count == cap) {
    1907            0 :             cap *= 2;
    1908            0 :             PendingAppend *tmp = realloc(arr, (size_t)cap * sizeof(PendingAppend));
    1909            0 :             if (!tmp) break;
    1910            0 :             arr = tmp;
    1911              :         }
    1912            1 :         strncpy(arr[count].folder, line,    sizeof(arr[count].folder) - 1);
    1913            1 :         arr[count].folder[sizeof(arr[count].folder) - 1] = '\0';
    1914            1 :         strncpy(arr[count].uid,    tab + 1, sizeof(arr[count].uid) - 1);
    1915            1 :         arr[count].uid[sizeof(arr[count].uid) - 1] = '\0';
    1916            1 :         count++;
    1917              :     }
    1918            1 :     *count_out = count;
    1919            1 :     return arr;
    1920              : }
    1921              : 
    1922            1 : void local_pending_append_remove(const char *folder, const char *uid) {
    1923            2 :     RAII_STRING char *path = pending_append_path();
    1924            1 :     if (!path) return;
    1925              : 
    1926              :     /* Read all lines except the matching one */
    1927            1 :     FILE *rfp = fopen(path, "r");
    1928            1 :     if (!rfp) return;
    1929              : 
    1930              :     char lines[4096][512];
    1931            1 :     int lcount = 0;
    1932              :     char line[512];
    1933            2 :     while (lcount < 4096 && fgets(line, sizeof(line), rfp)) {
    1934              :         char tmp[512];
    1935            1 :         strncpy(tmp, line, sizeof(tmp) - 1); tmp[sizeof(tmp) - 1] = '\0';
    1936            1 :         char *tab = strchr(tmp, '\t');
    1937            2 :         if (!tab) { snprintf(lines[lcount++], 512, "%s", line); continue; }
    1938            1 :         *tab = '\0';
    1939            1 :         char *nl = strchr(tab + 1, '\n'); if (nl) *nl = '\0';
    1940            1 :         if (strcmp(tmp, folder) == 0 && strcmp(tab + 1, uid) == 0)
    1941            1 :             continue; /* skip this entry */
    1942            0 :         snprintf(lines[lcount++], 512, "%s", line);
    1943              :     }
    1944            1 :     fclose(rfp);
    1945              : 
    1946            1 :     FILE *wfp = fopen(path, "w");
    1947            1 :     if (!wfp) return;
    1948            1 :     for (int i = 0; i < lcount; i++)
    1949            0 :         fputs(lines[i], wfp);
    1950            1 :     fclose(wfp);
    1951              : }
    1952              : 
    1953              : /* ── Pending Gmail fetch queue ───────────────────────────────────────── */
    1954              : 
    1955         1248 : static char *pending_fetch_path(void) {
    1956         1248 :     if (!g_account_base[0]) return NULL;
    1957         1248 :     char *path = NULL;
    1958         1248 :     if (asprintf(&path, "%s/pending_fetch.tsv", g_account_base) == -1)
    1959            0 :         return NULL;
    1960         1248 :     return path;
    1961              : }
    1962              : 
    1963          599 : int local_pending_fetch_add(const char *uid) {
    1964         1198 :     RAII_STRING char *path = pending_fetch_path();
    1965          599 :     if (!path || !uid) return -1;
    1966         1198 :     RAII_FILE FILE *fp = fopen(path, "a");
    1967          599 :     if (!fp) return -1;
    1968          599 :     fprintf(fp, "%s\n", uid);
    1969          599 :     return 0;
    1970              : }
    1971              : 
    1972           10 : char (*local_pending_fetch_load(int *count_out))[17] {
    1973           10 :     *count_out = 0;
    1974           20 :     RAII_STRING char *path = pending_fetch_path();
    1975           10 :     if (!path) return NULL;
    1976           20 :     RAII_FILE FILE *fp = fopen(path, "r");
    1977           10 :     if (!fp) return NULL;
    1978              : 
    1979           10 :     int cap = 64, count = 0;
    1980           10 :     char (*arr)[17] = malloc((size_t)cap * sizeof(char[17]));
    1981           10 :     if (!arr) return NULL;
    1982              : 
    1983              :     char line[32];
    1984          610 :     while (fgets(line, sizeof(line), fp)) {
    1985          600 :         char *nl = strchr(line, '\n'); if (nl) *nl = '\0';
    1986          600 :         char *cr = strchr(line, '\r'); if (cr) *cr = '\0';
    1987          600 :         if (line[0] == '\0') continue;
    1988          600 :         if (count == cap) {
    1989            6 :             cap *= 2;
    1990            6 :             char (*tmp)[17] = realloc(arr, (size_t)cap * sizeof(char[17]));
    1991            6 :             if (!tmp) break;
    1992            6 :             arr = tmp;
    1993              :         }
    1994          600 :         memcpy(arr[count], line, 16);
    1995          600 :         arr[count][16] = '\0';
    1996          600 :         count++;
    1997              :     }
    1998           10 :     *count_out = count;
    1999           10 :     return arr;
    2000              : }
    2001              : 
    2002          600 : void local_pending_fetch_remove(const char *uid) {
    2003         1200 :     RAII_STRING char *path = pending_fetch_path();
    2004          600 :     if (!path || !uid) return;
    2005              : 
    2006          600 :     FILE *rfp = fopen(path, "r");
    2007          600 :     if (!rfp) return;
    2008              : 
    2009              :     /* Read all lines, skip the matching UID */
    2010          600 :     int cap = 64, count = 0;
    2011          600 :     char (*lines)[32] = malloc((size_t)cap * sizeof(char[32]));
    2012          600 :     if (!lines) { fclose(rfp); return; }
    2013              : 
    2014              :     char line[32];
    2015        52448 :     while (fgets(line, sizeof(line), rfp)) {
    2016              :         char tmp[32];
    2017        51848 :         strncpy(tmp, line, 31); tmp[31] = '\0';
    2018        51848 :         char *nl = strchr(tmp, '\n'); if (nl) *nl = '\0';
    2019        51848 :         char *cr = strchr(tmp, '\r'); if (cr) *cr = '\0';
    2020        51848 :         if (strcmp(tmp, uid) == 0) continue;
    2021        51248 :         if (count == cap) {
    2022          518 :             cap *= 2;
    2023          518 :             char (*newlines)[32] = realloc(lines, (size_t)cap * sizeof(char[32]));
    2024          518 :             if (!newlines) break;
    2025          518 :             lines = newlines;
    2026              :         }
    2027        51248 :         memcpy(lines[count++], line, 31);
    2028        51248 :         lines[count - 1][31] = '\0';
    2029              :     }
    2030          600 :     fclose(rfp);
    2031              : 
    2032          600 :     FILE *wfp = fopen(path, "w");
    2033          600 :     if (wfp) {
    2034        51848 :         for (int i = 0; i < count; i++)
    2035        51248 :             fputs(lines[i], wfp);
    2036          600 :         fclose(wfp);
    2037              :     }
    2038          600 :     free(lines);
    2039              : }
    2040              : 
    2041           21 : int local_pending_fetch_count(void) {
    2042           42 :     RAII_STRING char *path = pending_fetch_path();
    2043           21 :     if (!path) return 0;
    2044           42 :     RAII_FILE FILE *fp = fopen(path, "r");
    2045           21 :     if (!fp) return 0;
    2046            8 :     int count = 0;
    2047              :     char line[32];
    2048            9 :     while (fgets(line, sizeof(line), fp)) {
    2049            1 :         if (line[0] != '\n' && line[0] != '\r' && line[0] != '\0')
    2050            1 :             count++;
    2051              :     }
    2052            8 :     return count;
    2053              : }
    2054              : 
    2055           18 : void local_pending_fetch_clear(void) {
    2056           36 :     RAII_STRING char *path = pending_fetch_path();
    2057           18 :     if (path) remove(path);
    2058           18 : }
    2059              : 
    2060              : /* ── Local outgoing message save ─────────────────────────────────────── */
    2061              : 
    2062            4 : int local_save_outgoing(const char *folder, const char *msg, size_t msg_len) {
    2063            4 :     if (!g_account_base[0] || !folder || !msg) return -1;
    2064              : 
    2065              :     /* Generate temporary UID: t<milliseconds_since_epoch> */
    2066              :     char uid[17];
    2067              :     {
    2068              :         struct timespec ts;
    2069            4 :         clock_gettime(CLOCK_REALTIME, &ts);
    2070            4 :         long long ms = (long long)ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL;
    2071            4 :         snprintf(uid, sizeof(uid), "t%lld", ms);
    2072              :     }
    2073              : 
    2074              :     /* Save full message */
    2075            4 :     if (local_msg_save(folder, uid, msg, msg_len) != 0) return -1;
    2076              : 
    2077              :     /* Extract raw header block (everything up to the first blank line) */
    2078            3 :     const char *blank = strstr(msg, "\r\n\r\n");
    2079            3 :     if (!blank) blank = strstr(msg, "\n\n");
    2080            3 :     size_t hdr_len = blank ? (size_t)(blank - msg) : msg_len;
    2081            3 :     local_hdr_save(folder, uid, msg, hdr_len);
    2082              : 
    2083              :     /* Decode fields for the manifest */
    2084            3 :     char *from_raw = mime_get_header(msg, "From");
    2085            3 :     char *subj_raw = mime_get_header(msg, "Subject");
    2086            3 :     char *date_raw = mime_get_header(msg, "Date");
    2087            3 :     char *from_dec = from_raw ? mime_decode_words(from_raw) : strdup("");
    2088            3 :     char *subj_dec = subj_raw ? mime_decode_words(subj_raw) : strdup("");
    2089            3 :     char *date_dec = date_raw ? mime_format_date(date_raw)  : strdup("");
    2090            3 :     free(from_raw); free(subj_raw); free(date_raw);
    2091              : 
    2092              :     /* Update manifest (MSG_FLAG_SEEN: sent messages are already read) */
    2093            3 :     Manifest *mf = manifest_load(folder);
    2094            3 :     if (!mf) mf = calloc(1, sizeof(Manifest));
    2095            3 :     if (mf) {
    2096              :         /* flags=0: no UNSEEN bit → sent message is already read */
    2097            3 :         manifest_upsert(mf, uid, from_dec, subj_dec, date_dec, 0);
    2098            3 :         manifest_save(folder, mf);
    2099            3 :         manifest_free(mf);
    2100              :     } else {
    2101            0 :         free(from_dec); free(subj_dec); free(date_dec);
    2102              :     }
    2103              : 
    2104              :     /* Queue for IMAP APPEND on next sync */
    2105            3 :     local_pending_append_add(folder, uid);
    2106              : 
    2107            3 :     logger_log(LOG_INFO, "local_save_outgoing: saved %s/%s, queued for APPEND",
    2108              :                folder, uid);
    2109            3 :     return 0;
    2110              : }
    2111              : 
    2112              : /* ── CONDSTORE folder sync state ─────────────────────────────────────────── */
    2113              : 
    2114          200 : static char *sync_state_path(const char *folder) {
    2115          200 :     if (!g_account_base[0]) return NULL;
    2116          200 :     char *path = NULL;
    2117          200 :     if (asprintf(&path, "%s/sync_state/%s.tsv", g_account_base, folder) == -1)
    2118            0 :         return NULL;
    2119          200 :     return path;
    2120              : }
    2121              : 
    2122           40 : int local_sync_state_save(const char *folder, const FolderSyncState *state) {
    2123           40 :     if (!folder || !state) return -1;
    2124           80 :     RAII_STRING char *path = sync_state_path(folder);
    2125           40 :     if (!path) return -1;
    2126           40 :     char *last_slash = strrchr(path, '/');
    2127           40 :     if (last_slash) {
    2128           40 :         char saved = *last_slash; *last_slash = '\0';
    2129           40 :         fs_mkdir_p(path, 0700);
    2130           40 :         *last_slash = saved;
    2131              :     }
    2132              :     char buf[64];
    2133           40 :     int n = snprintf(buf, sizeof(buf), "%" PRIu32 "\t%" PRIu64 "\n",
    2134           40 :                      state->uidvalidity, state->highestmodseq);
    2135           40 :     return write_file(path, buf, (size_t)n);
    2136              : }
    2137              : 
    2138          152 : int local_sync_state_load(const char *folder, FolderSyncState *state) {
    2139          152 :     state->uidvalidity    = 0;
    2140          152 :     state->highestmodseq  = 0;
    2141          304 :     RAII_STRING char *path = sync_state_path(folder);
    2142          152 :     if (!path) return -1;
    2143          152 :     char *data = load_file(path);
    2144          152 :     if (!data) return -1;
    2145           40 :     int rc = sscanf(data, "%" SCNu32 "\t%" SCNu64,
    2146              :                     &state->uidvalidity, &state->highestmodseq);
    2147           40 :     free(data);
    2148           40 :     return (rc == 2) ? 0 : -1;
    2149              : }
    2150              : 
    2151            8 : void local_sync_state_clear(const char *folder) {
    2152           16 :     RAII_STRING char *path = sync_state_path(folder);
    2153            8 :     if (path) unlink(path);
    2154            8 : }
        

Generated by: LCOV version 2.0-1