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