LCOV - code coverage report
Current view: top level - libemail/src/infrastructure - mail_client.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 58.7 % 213 125
Test Date: 2026-05-07 15:53:08 Functions: 80.0 % 25 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          275 : MailClient *mail_client_connect(Config *cfg) {
      25          275 :     if (!cfg) return NULL;
      26              : 
      27          275 :     MailClient *mc = calloc(1, sizeof(*mc));
      28          275 :     if (!mc) return NULL;
      29          275 :     mc->cfg = cfg;
      30              : 
      31          275 :     if (cfg->gmail_mode) {
      32           35 :         mc->is_gmail = 1;
      33           35 :         mc->gmail = gmail_connect(cfg);
      34           35 :         if (!mc->gmail) { free(mc); return NULL; }
      35              :     } else {
      36          240 :         if (!cfg->host) { free(mc); return NULL; }
      37          480 :         mc->imap = imap_connect(cfg->host, cfg->user, cfg->pass,
      38          240 :                                 cfg->ssl_no_verify ? 0 : 1);
      39          240 :         if (!mc->imap) { free(mc); return NULL; }
      40              :     }
      41              : 
      42          274 :     return mc;
      43              : }
      44              : 
      45          222 : void mail_client_free(MailClient *c) {
      46          222 :     if (!c) return;
      47          222 :     if (c->imap)    imap_disconnect(c->imap);
      48          222 :     if (c->gmail)   gmail_disconnect(c->gmail);
      49          222 :     free(c->selected);
      50          222 :     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           22 : int mail_client_list(MailClient *c, char ***names_out, int *count_out, char *sep_out) {
      60           22 :     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           22 :     return imap_list(c->imap, names_out, count_out, sep_out);
      73              : }
      74              : 
      75              : /* ── Select ───────────────────────────────────────────────────────── */
      76              : 
      77          214 : int mail_client_select(MailClient *c, const char *name) {
      78          214 :     free(c->selected);
      79          214 :     c->selected = name ? strdup(name) : NULL;
      80              : 
      81          214 :     if (c->is_gmail) {
      82              :         /* Gmail: no server-side SELECT needed; just remember the label. */
      83           21 :         return 0;
      84              :     }
      85          193 :     return imap_select(c->imap, name);
      86              : }
      87              : 
      88              : /* ── Search ───────────────────────────────────────────────────────── */
      89              : 
      90         1123 : int mail_client_search(MailClient *c, MailSearchCriteria criteria,
      91              :                        char (**uids_out)[17], int *count_out) {
      92         1123 :     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         1123 :     const char *imap_criteria = "ALL";
     106         1123 :     switch (criteria) {
     107          275 :         case MAIL_SEARCH_UNREAD:  imap_criteria = "UNSEEN"; break;
     108          275 :         case MAIL_SEARCH_FLAGGED: imap_criteria = "FLAGGED"; break;
     109          275 :         case MAIL_SEARCH_DONE:    imap_criteria = "KEYWORD $Done"; break;
     110          298 :         case MAIL_SEARCH_ALL:     break;
     111              :     }
     112         1123 :     return imap_uid_search(c->imap, imap_criteria, uids_out, count_out);
     113              : }
     114              : 
     115              : /* ── Fetch ────────────────────────────────────────────────────────── */
     116              : 
     117          486 : char *mail_client_fetch_headers(MailClient *c, const char *uid) {
     118          486 :     if (c->is_gmail) {
     119              :         /* Fetch full message and return only the header portion */
     120            0 :         char *raw = gmail_fetch_message(c->gmail, uid, NULL, NULL);
     121            0 :         if (!raw) return NULL;
     122              :         /* Split at \r\n\r\n or \n\n boundary */
     123            0 :         const char *sep = strstr(raw, "\r\n\r\n");
     124              :         size_t hdr_len;
     125            0 :         if (sep) {
     126            0 :             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            0 :         char *headers = malloc(hdr_len + 1);
     132            0 :         if (!headers) { free(raw); return NULL; }
     133            0 :         memcpy(headers, raw, hdr_len);
     134            0 :         headers[hdr_len] = '\0';
     135            0 :         free(raw);
     136            0 :         return headers;
     137              :     }
     138          486 :     return imap_uid_fetch_headers(c->imap, uid);
     139              : }
     140              : 
     141          173 : char *mail_client_fetch_body(MailClient *c, const char *uid) {
     142          173 :     if (c->is_gmail)
     143            5 :         return gmail_fetch_message(c->gmail, uid, NULL, NULL);
     144          168 :     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           31 : int mail_client_set_flag(MailClient *c, const char *uid,
     170              :                          const char *flag, int add) {
     171           31 :     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           15 :     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_move_to_folder(MailClient *c, const char *uid, const char *target_folder) {
     210            0 :     if (c->is_gmail) {
     211            0 :         logger_log(LOG_DEBUG, "mail_client: Gmail ignoring move to '%s'", target_folder);
     212            0 :         return 0;
     213              :     }
     214            0 :     return imap_uid_move(c->imap, uid, target_folder);
     215              : }
     216              : 
     217            2 : int mail_client_mark_junk(MailClient *c, const char *uid) {
     218            2 :     if (c->is_gmail) {
     219            1 :         const char *add[] = { "SPAM" };
     220            1 :         const char *rm[]  = { "INBOX" };
     221            1 :         return gmail_modify_labels(c->gmail, uid, add, 1, rm, 1);
     222              :     }
     223              :     /* IMAP: set $Junk, clear $NotJunk */
     224            1 :     imap_uid_set_flag(c->imap, uid, "$NotJunk", 0);
     225            1 :     return imap_uid_set_flag(c->imap, uid, "$Junk", 1);
     226              : }
     227              : 
     228            2 : int mail_client_mark_notjunk(MailClient *c, const char *uid) {
     229            2 :     if (c->is_gmail) {
     230            1 :         const char *add[] = { "INBOX" };
     231            1 :         const char *rm[]  = { "SPAM" };
     232            1 :         return gmail_modify_labels(c->gmail, uid, add, 1, rm, 1);
     233              :     }
     234              :     /* IMAP: set $NotJunk, clear $Junk */
     235            1 :     imap_uid_set_flag(c->imap, uid, "$Junk", 0);
     236            1 :     return imap_uid_set_flag(c->imap, uid, "$NotJunk", 1);
     237              : }
     238              : 
     239              : /* ── List with IDs ────────────────────────────────────────────────── */
     240              : 
     241            4 : int mail_client_list_with_ids(MailClient *c, char ***names_out,
     242              :                               char ***ids_out, int *count_out) {
     243            4 :     *names_out = NULL;
     244            4 :     *ids_out   = NULL;
     245            4 :     *count_out = 0;
     246              : 
     247            4 :     if (c->is_gmail) {
     248            4 :         return gmail_list_labels(c->gmail, names_out, ids_out, count_out);
     249              :     }
     250              : 
     251              :     /* IMAP: list folders, then duplicate names as IDs */
     252            0 :     int rc = imap_list(c->imap, names_out, count_out, NULL);
     253            0 :     if (rc != 0 || *count_out == 0) return rc;
     254              : 
     255            0 :     char **ids = calloc((size_t)*count_out, sizeof(char *));
     256            0 :     if (!ids) {
     257            0 :         for (int i = 0; i < *count_out; i++) free((*names_out)[i]);
     258            0 :         free(*names_out);
     259            0 :         *names_out = NULL;
     260            0 :         *count_out = 0;
     261            0 :         return -1;
     262              :     }
     263            0 :     for (int i = 0; i < *count_out; i++) {
     264            0 :         ids[i] = strdup((*names_out)[i]);
     265            0 :         if (!ids[i]) {
     266              :             /* clean up on alloc failure */
     267            0 :             for (int j = 0; j < i; j++) free(ids[j]);
     268            0 :             free(ids);
     269            0 :             for (int j = 0; j < *count_out; j++) free((*names_out)[j]);
     270            0 :             free(*names_out);
     271            0 :             *names_out = NULL;
     272            0 :             *count_out = 0;
     273            0 :             return -1;
     274              :         }
     275              :     }
     276            0 :     *ids_out = ids;
     277            0 :     return 0;
     278              : }
     279              : 
     280              : /* ── Create / delete label or folder ─────────────────────────────── */
     281              : 
     282            1 : int mail_client_create_label(MailClient *c, const char *name, char **id_out) {
     283            1 :     if (id_out) *id_out = NULL;
     284            1 :     if (!c->is_gmail) {
     285            0 :         fprintf(stderr, "Error: 'create-label' is Gmail-only. Use 'create-folder' for IMAP.\n");
     286            0 :         return -1;
     287              :     }
     288            1 :     return gmail_create_label(c->gmail, name, id_out);
     289              : }
     290              : 
     291            1 : int mail_client_delete_label(MailClient *c, const char *label_id) {
     292            1 :     if (!c->is_gmail) {
     293            0 :         fprintf(stderr, "Error: 'delete-label' is Gmail-only. Use 'delete-folder' for IMAP.\n");
     294            0 :         return -1;
     295              :     }
     296            1 :     return gmail_delete_label(c->gmail, label_id);
     297              : }
     298              : 
     299            1 : int mail_client_create_folder(MailClient *c, const char *name) {
     300            1 :     if (c->is_gmail) {
     301            0 :         fprintf(stderr, "Error: 'create-folder' is IMAP-only. Use 'create-label' for Gmail.\n");
     302            0 :         return -1;
     303              :     }
     304            1 :     return imap_create_folder(c->imap, name);
     305              : }
     306              : 
     307            1 : int mail_client_delete_folder(MailClient *c, const char *name) {
     308            1 :     if (c->is_gmail) {
     309            0 :         fprintf(stderr, "Error: 'delete-folder' is IMAP-only. Use 'delete-label' for Gmail.\n");
     310            0 :         return -1;
     311              :     }
     312            1 :     return imap_delete_folder(c->imap, name);
     313              : }
     314              : 
     315              : /* ── Label modify (Gmail only) ────────────────────────────────────── */
     316              : 
     317            6 : int mail_client_modify_label(MailClient *c, const char *uid,
     318              :                              const char *label_id, int add) {
     319            6 :     if (!c->is_gmail) return 0; /* no-op for IMAP */
     320              : 
     321            6 :     if (add) {
     322            3 :         const char *add_arr[] = {label_id};
     323            3 :         return gmail_modify_labels(c->gmail, uid, add_arr, 1, NULL, 0);
     324              :     } else {
     325            3 :         const char *rm_arr[] = {label_id};
     326            3 :         return gmail_modify_labels(c->gmail, uid, NULL, 0, rm_arr, 1);
     327              :     }
     328              : }
     329              : 
     330              : /* ── Append / Send ────────────────────────────────────────────────── */
     331              : 
     332            1 : int mail_client_append(MailClient *c, const char *folder,
     333              :                        const char *msg, size_t msg_len) {
     334            1 :     if (c->is_gmail) {
     335              :         /* Gmail: send via REST API (folder is ignored) */
     336              :         (void)folder;
     337            0 :         return gmail_send(c->gmail, msg, msg_len);
     338              :     }
     339            1 :     return imap_append(c->imap, folder, msg, msg_len);
     340              : }
     341              : 
     342              : /* ── Incremental sync (CONDSTORE / QRESYNC) ─────────────────────────────── */
     343              : 
     344          152 : int mail_client_select_ext(MailClient *c, const char *folder,
     345              :                             uint32_t known_uidval, uint64_t known_modseq,
     346              :                             ImapSelectResult *res_out) {
     347          152 :     memset(res_out, 0, sizeof(*res_out));
     348          152 :     free(c->selected);
     349          152 :     c->selected = folder ? strdup(folder) : NULL;
     350              : 
     351          152 :     if (c->is_gmail) return 0;  /* Gmail: no server-side SELECT */
     352              : 
     353          152 :     int caps = imap_get_caps(c->imap);
     354              : 
     355          152 :     if ((caps & IMAP_CAP_QRESYNC) && known_uidval && known_modseq)
     356           16 :         return imap_select_qresync(c->imap, folder,
     357              :                                    known_uidval, known_modseq, res_out);
     358          136 :     if (caps & IMAP_CAP_CONDSTORE)
     359           48 :         return imap_select_condstore(c->imap, folder, res_out);
     360              : 
     361              :     /* Fallback: plain SELECT (res_out stays zeroed) */
     362           88 :     return imap_select(c->imap, folder);
     363              : }
     364              : 
     365            7 : int mail_client_fetch_flags_changedsince(MailClient *c, uint64_t modseq,
     366              :                                           ImapFlagUpdate **out, int *count_out) {
     367            7 :     *out       = NULL;
     368            7 :     *count_out = 0;
     369            7 :     if (c->is_gmail) return 0;  /* not supported for Gmail */
     370            7 :     return imap_uid_fetch_flags_changedsince(c->imap, modseq, out, count_out);
     371              : }
     372              : 
     373              : /* ── Progress ─────────────────────────────────────────────────────── */
     374              : 
     375          306 : void mail_client_set_progress(MailClient *c, ImapProgressFn fn, void *ctx) {
     376          306 :     if (!c) return;
     377          306 :     if (c->imap)
     378          306 :         imap_set_progress(c->imap, fn, ctx);
     379              :     /* Gmail progress is handled separately via gmail_set_progress */
     380              : }
     381              : 
     382              : /* ── Sync ─────────────────────────────────────────────────────────── */
     383              : 
     384            0 : int mail_client_sync(MailClient *c) {
     385            0 :     if (c->is_gmail)
     386            0 :         return gmail_sync(c->gmail);
     387              :     /* IMAP sync is handled by email_service_sync, not here */
     388            0 :     return 0;
     389              : }
        

Generated by: LCOV version 2.0-1