LCOV - code coverage report
Current view: top level - libemail/src/infrastructure - mail_client.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 62.7 % 217 136
Test Date: 2026-08-21 10:11:30 Functions: 76.9 % 26 20

            Line data    Source code
       1              : #include "mail_client.h"
       2              : #include "imap_client.h"
       3              : #include "gmail_client.h"
       4              : #include "gmail_sync.h"
       5              : #include "local_store.h"
       6              : #include "logger.h"
       7              : #include "raii.h"
       8              : #include <stdio.h>
       9              : #include <stdlib.h>
      10              : #include <string.h>
      11              : 
      12              : /* ── Client struct ────────────────────────────────────────────────── */
      13              : 
      14              : struct MailClient {
      15              :     int is_gmail;
      16              :     Config *cfg;              /* borrowed */
      17              :     ImapClient  *imap;        /* non-NULL if IMAP */
      18              :     GmailClient *gmail;       /* non-NULL if Gmail */
      19              :     char *selected;           /* currently selected folder/label */
      20              : };
      21              : 
      22              : /* ── Connect / Free ───────────────────────────────────────────────── */
      23              : 
      24          430 : MailClient *mail_client_connect(Config *cfg) {
      25          430 :     if (!cfg) return NULL;
      26              : 
      27          430 :     MailClient *mc = calloc(1, sizeof(*mc));
      28          430 :     if (!mc) return NULL;
      29          430 :     mc->cfg = cfg;
      30              : 
      31          430 :     if (cfg->gmail_mode) {
      32           36 :         mc->is_gmail = 1;
      33           36 :         mc->gmail = gmail_connect(cfg);
      34           36 :         if (!mc->gmail) { free(mc); return NULL; }
      35              :     } else {
      36          394 :         if (!cfg->host) { free(mc); return NULL; }
      37          788 :         mc->imap = imap_connect(cfg->host, cfg->user, cfg->pass,
      38          394 :                                 cfg->ssl_no_verify ? 0 : 1);
      39          394 :         if (!mc->imap) { free(mc); return NULL; }
      40              :     }
      41              : 
      42          413 :     return mc;
      43              : }
      44              : 
      45          350 : void mail_client_free(MailClient *c) {
      46          350 :     if (!c) return;
      47          350 :     if (c->imap)    imap_disconnect(c->imap);
      48          350 :     if (c->gmail)   gmail_disconnect(c->gmail);
      49          350 :     free(c->selected);
      50          350 :     free(c);
      51              : }
      52              : 
      53            0 : int mail_client_uses_labels(const MailClient *c) {
      54            0 :     return c ? c->is_gmail : 0;
      55              : }
      56              : 
      57              : /* ── List ─────────────────────────────────────────────────────────── */
      58              : 
      59           32 : int mail_client_list(MailClient *c, char ***names_out, int *count_out, char *sep_out) {
      60           32 :     if (c->is_gmail) {
      61            0 :         char **ids = NULL;
      62            0 :         int rc = gmail_list_labels(c->gmail, names_out, &ids, count_out);
      63              :         /* Free the IDs array — the caller only needs display names.
      64              :          * TODO: in future, store ids for label_id lookups. */
      65            0 :         if (ids) {
      66            0 :             for (int i = 0; i < *count_out; i++) free(ids[i]);
      67            0 :             free(ids);
      68              :         }
      69            0 :         if (sep_out) *sep_out = '/';
      70            0 :         return rc;
      71              :     }
      72           32 :     return imap_list(c->imap, names_out, count_out, sep_out);
      73              : }
      74              : 
      75              : /* ── Select ───────────────────────────────────────────────────────── */
      76              : 
      77          336 : int mail_client_select(MailClient *c, const char *name) {
      78          336 :     free(c->selected);
      79          336 :     c->selected = name ? strdup(name) : NULL;
      80              : 
      81          336 :     if (c->is_gmail) {
      82              :         /* Gmail: no server-side SELECT needed; just remember the label. */
      83           22 :         return 0;
      84              :     }
      85          314 :     return imap_select(c->imap, name);
      86              : }
      87              : 
      88              : /* ── Search ───────────────────────────────────────────────────────── */
      89              : 
      90         1742 : int mail_client_search(MailClient *c, MailSearchCriteria criteria,
      91              :                        char (**uids_out)[17], int *count_out) {
      92         1742 :     if (c->is_gmail) {
      93              :         /* For Gmail: filter by selected label + additional criteria via query */
      94            0 :         const char *query = NULL;
      95            0 :         switch (criteria) {
      96            0 :             case MAIL_SEARCH_UNREAD:  query = "is:unread"; break;
      97            0 :             case MAIL_SEARCH_FLAGGED: query = "is:starred"; break;
      98            0 :             case MAIL_SEARCH_DONE:    query = NULL; break;  /* Not yet supported */
      99            0 :             case MAIL_SEARCH_ALL:     break;
     100              :         }
     101            0 :         return gmail_list_messages(c->gmail, c->selected, query, uids_out, count_out, NULL);
     102              :     }
     103              : 
     104              :     /* IMAP: map criteria to IMAP search string */
     105         1742 :     const char *imap_criteria = "ALL";
     106         1742 :     switch (criteria) {
     107          428 :         case MAIL_SEARCH_UNREAD:  imap_criteria = "UNSEEN"; break;
     108          428 :         case MAIL_SEARCH_FLAGGED: imap_criteria = "FLAGGED"; break;
     109          428 :         case MAIL_SEARCH_DONE:    imap_criteria = "KEYWORD $Done"; break;
     110          458 :         case MAIL_SEARCH_ALL:     break;
     111              :     }
     112         1742 :     return imap_uid_search(c->imap, imap_criteria, uids_out, count_out);
     113              : }
     114              : 
     115              : /* ── Fetch ────────────────────────────────────────────────────────── */
     116              : 
     117          516 : char *mail_client_fetch_headers(MailClient *c, const char *uid) {
     118          516 :     if (c->is_gmail) {
     119              :         /* Fetch full message and return only the header portion */
     120            1 :         char *raw = gmail_fetch_message(c->gmail, uid, NULL, NULL);
     121            1 :         if (!raw) return NULL;
     122              :         /* Split at \r\n\r\n or \n\n boundary */
     123            1 :         const char *sep = strstr(raw, "\r\n\r\n");
     124              :         size_t hdr_len;
     125            1 :         if (sep) {
     126            1 :             hdr_len = (size_t)(sep - raw) + 4;
     127              :         } else {
     128            0 :             sep = strstr(raw, "\n\n");
     129            0 :             hdr_len = sep ? (size_t)(sep - raw) + 2 : strlen(raw);
     130              :         }
     131            1 :         char *headers = malloc(hdr_len + 1);
     132            1 :         if (!headers) { free(raw); return NULL; }
     133            1 :         memcpy(headers, raw, hdr_len);
     134            1 :         headers[hdr_len] = '\0';
     135            1 :         free(raw);
     136            1 :         return headers;
     137              :     }
     138          515 :     return imap_uid_fetch_headers(c->imap, uid);
     139              : }
     140              : 
     141          218 : char *mail_client_fetch_body(MailClient *c, const char *uid) {
     142          218 :     if (c->is_gmail)
     143            5 :         return gmail_fetch_message(c->gmail, uid, NULL, NULL);
     144          213 :     return imap_uid_fetch_body(c->imap, uid);
     145              : }
     146              : 
     147            0 : int mail_client_fetch_flags(MailClient *c, const char *uid) {
     148            0 :     if (c->is_gmail) {
     149              :         /* Fetch message to get labels, convert to flags bitmask */
     150            0 :         char **labels = NULL;
     151            0 :         int label_count = 0;
     152            0 :         char *raw = gmail_fetch_message(c->gmail, uid, &labels, &label_count);
     153            0 :         free(raw);
     154              : 
     155            0 :         int flags = 0;
     156            0 :         for (int i = 0; i < label_count; i++) {
     157            0 :             if (strcmp(labels[i], "UNREAD") == 0)  flags |= MSG_FLAG_UNSEEN;
     158            0 :             if (strcmp(labels[i], "STARRED") == 0) flags |= MSG_FLAG_FLAGGED;
     159            0 :             free(labels[i]);
     160              :         }
     161            0 :         free(labels);
     162            0 :         return flags;
     163              :     }
     164            0 :     return imap_uid_fetch_flags(c->imap, uid);
     165              : }
     166              : 
     167              : /* ── Flags / Labels ───────────────────────────────────────────────── */
     168              : 
     169           32 : int mail_client_set_flag(MailClient *c, const char *uid,
     170              :                          const char *flag, int add) {
     171           32 :     if (c->is_gmail) {
     172              :         /* Translate IMAP flag names to Gmail label operations */
     173           16 :         if (strcmp(flag, "\\Seen") == 0) {
     174              :             /* \Seen add → remove UNREAD; \Seen remove → add UNREAD */
     175            8 :             const char *label = "UNREAD";
     176            8 :             if (add) {
     177            4 :                 const char *rm[] = { label };
     178            4 :                 return gmail_modify_labels(c->gmail, uid, NULL, 0, rm, 1);
     179              :             } else {
     180            4 :                 const char *ad[] = { label };
     181            4 :                 return gmail_modify_labels(c->gmail, uid, ad, 1, NULL, 0);
     182              :             }
     183              :         }
     184            8 :         if (strcmp(flag, "\\Flagged") == 0) {
     185            8 :             const char *label = "STARRED";
     186            8 :             if (add) {
     187            4 :                 const char *ad[] = { label };
     188            4 :                 return gmail_modify_labels(c->gmail, uid, ad, 1, NULL, 0);
     189              :             } else {
     190            4 :                 const char *rm[] = { label };
     191            4 :                 return gmail_modify_labels(c->gmail, uid, NULL, 0, rm, 1);
     192              :             }
     193              :         }
     194              :         /* Unknown flag — ignore for Gmail */
     195            0 :         logger_log(LOG_DEBUG, "mail_client: Gmail ignoring flag '%s'", flag);
     196            0 :         return 0;
     197              :     }
     198           16 :     return imap_uid_set_flag(c->imap, uid, flag, add);
     199              : }
     200              : 
     201            0 : int mail_client_trash(MailClient *c, const char *uid) {
     202            0 :     if (c->is_gmail)
     203            0 :         return gmail_trash(c->gmail, uid);
     204              : 
     205              :     /* IMAP: set \Deleted flag */
     206            0 :     return imap_uid_set_flag(c->imap, uid, "\\Deleted", 1);
     207              : }
     208              : 
     209            0 : int mail_client_delete(MailClient *c, const char *uid) {
     210            0 :     if (c->is_gmail)
     211            0 :         return gmail_delete_permanently(c->gmail, uid);
     212            0 :     return imap_uid_delete(c->imap, uid);
     213              : }
     214              : 
     215            0 : int mail_client_move_to_folder(MailClient *c, const char *uid, const char *target_folder) {
     216            0 :     if (c->is_gmail) {
     217            0 :         logger_log(LOG_DEBUG, "mail_client: Gmail ignoring move to '%s'", target_folder);
     218            0 :         return 0;
     219              :     }
     220            0 :     return imap_uid_move(c->imap, uid, target_folder);
     221              : }
     222              : 
     223            2 : int mail_client_mark_junk(MailClient *c, const char *uid) {
     224            2 :     if (c->is_gmail) {
     225            1 :         const char *add[] = { "SPAM" };
     226            1 :         const char *rm[]  = { "INBOX" };
     227            1 :         return gmail_modify_labels(c->gmail, uid, add, 1, rm, 1);
     228              :     }
     229              :     /* IMAP: set $Junk, clear $NotJunk */
     230            1 :     imap_uid_set_flag(c->imap, uid, "$NotJunk", 0);
     231            1 :     return imap_uid_set_flag(c->imap, uid, "$Junk", 1);
     232              : }
     233              : 
     234            2 : int mail_client_mark_notjunk(MailClient *c, const char *uid) {
     235            2 :     if (c->is_gmail) {
     236            1 :         const char *add[] = { "INBOX" };
     237            1 :         const char *rm[]  = { "SPAM" };
     238            1 :         return gmail_modify_labels(c->gmail, uid, add, 1, rm, 1);
     239              :     }
     240              :     /* IMAP: set $NotJunk, clear $Junk */
     241            1 :     imap_uid_set_flag(c->imap, uid, "$Junk", 0);
     242            1 :     return imap_uid_set_flag(c->imap, uid, "$NotJunk", 1);
     243              : }
     244              : 
     245              : /* ── List with IDs ────────────────────────────────────────────────── */
     246              : 
     247            4 : int mail_client_list_with_ids(MailClient *c, char ***names_out,
     248              :                               char ***ids_out, int *count_out) {
     249            4 :     *names_out = NULL;
     250            4 :     *ids_out   = NULL;
     251            4 :     *count_out = 0;
     252              : 
     253            4 :     if (c->is_gmail) {
     254            4 :         return gmail_list_labels(c->gmail, names_out, ids_out, count_out);
     255              :     }
     256              : 
     257              :     /* IMAP: list folders, then duplicate names as IDs */
     258            0 :     int rc = imap_list(c->imap, names_out, count_out, NULL);
     259            0 :     if (rc != 0 || *count_out == 0) return rc;
     260              : 
     261            0 :     char **ids = calloc((size_t)*count_out, sizeof(char *));
     262            0 :     if (!ids) {
     263            0 :         for (int i = 0; i < *count_out; i++) free((*names_out)[i]);
     264            0 :         free(*names_out);
     265            0 :         *names_out = NULL;
     266            0 :         *count_out = 0;
     267            0 :         return -1;
     268              :     }
     269            0 :     for (int i = 0; i < *count_out; i++) {
     270            0 :         ids[i] = strdup((*names_out)[i]);
     271            0 :         if (!ids[i]) {
     272              :             /* clean up on alloc failure */
     273            0 :             for (int j = 0; j < i; j++) free(ids[j]);
     274            0 :             free(ids);
     275            0 :             for (int j = 0; j < *count_out; j++) free((*names_out)[j]);
     276            0 :             free(*names_out);
     277            0 :             *names_out = NULL;
     278            0 :             *count_out = 0;
     279            0 :             return -1;
     280              :         }
     281              :     }
     282            0 :     *ids_out = ids;
     283            0 :     return 0;
     284              : }
     285              : 
     286              : /* ── Create / delete label or folder ─────────────────────────────── */
     287              : 
     288            1 : int mail_client_create_label(MailClient *c, const char *name, char **id_out) {
     289            1 :     if (id_out) *id_out = NULL;
     290            1 :     if (!c->is_gmail) {
     291            0 :         fprintf(stderr, "Error: 'create-label' is Gmail-only. Use 'create-folder' for IMAP.\n");
     292            0 :         return -1;
     293              :     }
     294            1 :     return gmail_create_label(c->gmail, name, id_out);
     295              : }
     296              : 
     297            1 : int mail_client_delete_label(MailClient *c, const char *label_id) {
     298            1 :     if (!c->is_gmail) {
     299            0 :         fprintf(stderr, "Error: 'delete-label' is Gmail-only. Use 'delete-folder' for IMAP.\n");
     300            0 :         return -1;
     301              :     }
     302            1 :     return gmail_delete_label(c->gmail, label_id);
     303              : }
     304              : 
     305            1 : int mail_client_create_folder(MailClient *c, const char *name) {
     306            1 :     if (c->is_gmail) {
     307            0 :         fprintf(stderr, "Error: 'create-folder' is IMAP-only. Use 'create-label' for Gmail.\n");
     308            0 :         return -1;
     309              :     }
     310            1 :     return imap_create_folder(c->imap, name);
     311              : }
     312              : 
     313            1 : int mail_client_delete_folder(MailClient *c, const char *name) {
     314            1 :     if (c->is_gmail) {
     315            0 :         fprintf(stderr, "Error: 'delete-folder' is IMAP-only. Use 'delete-label' for Gmail.\n");
     316            0 :         return -1;
     317              :     }
     318            1 :     return imap_delete_folder(c->imap, name);
     319              : }
     320              : 
     321              : /* ── Label modify (Gmail only) ────────────────────────────────────── */
     322              : 
     323            6 : int mail_client_modify_label(MailClient *c, const char *uid,
     324              :                              const char *label_id, int add) {
     325            6 :     if (!c->is_gmail) return 0; /* no-op for IMAP */
     326              : 
     327            6 :     if (add) {
     328            3 :         const char *add_arr[] = {label_id};
     329            3 :         return gmail_modify_labels(c->gmail, uid, add_arr, 1, NULL, 0);
     330              :     } else {
     331            3 :         const char *rm_arr[] = {label_id};
     332            3 :         return gmail_modify_labels(c->gmail, uid, NULL, 0, rm_arr, 1);
     333              :     }
     334              : }
     335              : 
     336              : /* ── Append / Send ────────────────────────────────────────────────── */
     337              : 
     338            1 : int mail_client_append(MailClient *c, const char *folder,
     339              :                        const char *msg, size_t msg_len) {
     340            1 :     if (c->is_gmail) {
     341              :         /* Gmail: send via REST API (folder is ignored) */
     342              :         (void)folder;
     343            0 :         return gmail_send(c->gmail, msg, msg_len);
     344              :     }
     345            1 :     return imap_append(c->imap, folder, msg, msg_len);
     346              : }
     347              : 
     348              : /* ── Incremental sync (CONDSTORE / QRESYNC) ─────────────────────────────── */
     349              : 
     350          208 : int mail_client_select_ext(MailClient *c, const char *folder,
     351              :                             uint32_t known_uidval, uint64_t known_modseq,
     352              :                             ImapSelectResult *res_out) {
     353          208 :     memset(res_out, 0, sizeof(*res_out));
     354          208 :     free(c->selected);
     355          208 :     c->selected = folder ? strdup(folder) : NULL;
     356              : 
     357          208 :     if (c->is_gmail) return 0;  /* Gmail: no server-side SELECT */
     358              : 
     359          208 :     int caps = imap_get_caps(c->imap);
     360              : 
     361          208 :     if ((caps & IMAP_CAP_QRESYNC) && known_uidval && known_modseq)
     362           16 :         return imap_select_qresync(c->imap, folder,
     363              :                                    known_uidval, known_modseq, res_out);
     364          192 :     if (caps & IMAP_CAP_CONDSTORE)
     365           48 :         return imap_select_condstore(c->imap, folder, res_out);
     366              : 
     367              :     /* Fallback: plain SELECT (res_out stays zeroed) */
     368          144 :     return imap_select(c->imap, folder);
     369              : }
     370              : 
     371            7 : int mail_client_fetch_flags_changedsince(MailClient *c, uint64_t modseq,
     372              :                                           ImapFlagUpdate **out, int *count_out) {
     373            7 :     *out       = NULL;
     374            7 :     *count_out = 0;
     375            7 :     if (c->is_gmail) return 0;  /* not supported for Gmail */
     376            7 :     return imap_uid_fetch_flags_changedsince(c->imap, modseq, out, count_out);
     377              : }
     378              : 
     379              : /* ── Progress ─────────────────────────────────────────────────────── */
     380              : 
     381          390 : void mail_client_set_progress(MailClient *c, ImapProgressFn fn, void *ctx) {
     382          390 :     if (!c) return;
     383          390 :     if (c->imap)
     384          390 :         imap_set_progress(c->imap, fn, ctx);
     385              :     /* Gmail progress is handled separately via gmail_set_progress */
     386              : }
     387              : 
     388              : /* ── Sync ─────────────────────────────────────────────────────────── */
     389              : 
     390            0 : int mail_client_sync(MailClient *c) {
     391            0 :     if (c->is_gmail)
     392            0 :         return gmail_sync(c->gmail);
     393              :     /* IMAP sync is handled by email_service_sync, not here */
     394            0 :     return 0;
     395              : }
        

Generated by: LCOV version 2.0-1