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 493 : MailClient *mail_client_connect(Config *cfg) {
25 493 : if (!cfg) return NULL;
26 :
27 492 : MailClient *mc = calloc(1, sizeof(*mc));
28 492 : if (!mc) return NULL;
29 492 : mc->cfg = cfg;
30 :
31 492 : if (cfg->gmail_mode) {
32 75 : mc->is_gmail = 1;
33 75 : mc->gmail = gmail_connect(cfg);
34 75 : if (!mc->gmail) { free(mc); return NULL; }
35 : } else {
36 417 : if (!cfg->host) { free(mc); return NULL; }
37 820 : mc->imap = imap_connect(cfg->host, cfg->user, cfg->pass,
38 410 : cfg->ssl_no_verify ? 0 : 1);
39 410 : if (!mc->imap) { free(mc); return NULL; }
40 : }
41 :
42 453 : return mc;
43 : }
44 :
45 391 : void mail_client_free(MailClient *c) {
46 391 : if (!c) return;
47 390 : if (c->imap) imap_disconnect(c->imap);
48 390 : if (c->gmail) gmail_disconnect(c->gmail);
49 390 : free(c->selected);
50 390 : free(c);
51 : }
52 :
53 4 : int mail_client_uses_labels(const MailClient *c) {
54 4 : return c ? c->is_gmail : 0;
55 : }
56 :
57 : /* ── List ─────────────────────────────────────────────────────────── */
58 :
59 34 : int mail_client_list(MailClient *c, char ***names_out, int *count_out, char *sep_out) {
60 34 : if (c->is_gmail) {
61 2 : char **ids = NULL;
62 2 : 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 2 : if (ids) {
66 8 : for (int i = 0; i < *count_out; i++) free(ids[i]);
67 2 : free(ids);
68 : }
69 2 : if (sep_out) *sep_out = '/';
70 2 : return rc;
71 : }
72 32 : return imap_list(c->imap, names_out, count_out, sep_out);
73 : }
74 :
75 : /* ── Select ───────────────────────────────────────────────────────── */
76 :
77 344 : int mail_client_select(MailClient *c, const char *name) {
78 344 : free(c->selected);
79 344 : c->selected = name ? strdup(name) : NULL;
80 :
81 344 : if (c->is_gmail) {
82 : /* Gmail: no server-side SELECT needed; just remember the label. */
83 26 : return 0;
84 : }
85 318 : return imap_select(c->imap, name);
86 : }
87 :
88 : /* ── Search ───────────────────────────────────────────────────────── */
89 :
90 1746 : int mail_client_search(MailClient *c, MailSearchCriteria criteria,
91 : char (**uids_out)[17], int *count_out) {
92 1746 : if (c->is_gmail) {
93 : /* For Gmail: filter by selected label + additional criteria via query */
94 4 : const char *query = NULL;
95 4 : switch (criteria) {
96 1 : case MAIL_SEARCH_UNREAD: query = "is:unread"; break;
97 1 : case MAIL_SEARCH_FLAGGED: query = "is:starred"; break;
98 1 : case MAIL_SEARCH_DONE: query = NULL; break; /* Not yet supported */
99 1 : case MAIL_SEARCH_ALL: break;
100 : }
101 4 : 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 518 : char *mail_client_fetch_headers(MailClient *c, const char *uid) {
118 518 : if (c->is_gmail) {
119 : /* Fetch full message and return only the header portion */
120 3 : char *raw = gmail_fetch_message(c->gmail, uid, NULL, NULL);
121 3 : if (!raw) return NULL;
122 : /* Split at \r\n\r\n or \n\n boundary */
123 3 : const char *sep = strstr(raw, "\r\n\r\n");
124 : size_t hdr_len;
125 3 : if (sep) {
126 2 : hdr_len = (size_t)(sep - raw) + 4;
127 : } else {
128 1 : sep = strstr(raw, "\n\n");
129 1 : hdr_len = sep ? (size_t)(sep - raw) + 2 : strlen(raw);
130 : }
131 3 : char *headers = malloc(hdr_len + 1);
132 3 : if (!headers) { free(raw); return NULL; }
133 3 : memcpy(headers, raw, hdr_len);
134 3 : headers[hdr_len] = '\0';
135 3 : free(raw);
136 3 : return headers;
137 : }
138 515 : return imap_uid_fetch_headers(c->imap, uid);
139 : }
140 :
141 219 : char *mail_client_fetch_body(MailClient *c, const char *uid) {
142 219 : if (c->is_gmail)
143 6 : return gmail_fetch_message(c->gmail, uid, NULL, NULL);
144 213 : return imap_uid_fetch_body(c->imap, uid);
145 : }
146 :
147 2 : int mail_client_fetch_flags(MailClient *c, const char *uid) {
148 2 : if (c->is_gmail) {
149 : /* Fetch message to get labels, convert to flags bitmask */
150 1 : char **labels = NULL;
151 1 : int label_count = 0;
152 1 : char *raw = gmail_fetch_message(c->gmail, uid, &labels, &label_count);
153 1 : free(raw);
154 :
155 1 : int flags = 0;
156 4 : for (int i = 0; i < label_count; i++) {
157 3 : if (strcmp(labels[i], "UNREAD") == 0) flags |= MSG_FLAG_UNSEEN;
158 3 : if (strcmp(labels[i], "STARRED") == 0) flags |= MSG_FLAG_FLAGGED;
159 3 : free(labels[i]);
160 : }
161 1 : free(labels);
162 1 : return flags;
163 : }
164 1 : return imap_uid_fetch_flags(c->imap, uid);
165 : }
166 :
167 : /* ── Flags / Labels ───────────────────────────────────────────────── */
168 :
169 37 : int mail_client_set_flag(MailClient *c, const char *uid,
170 : const char *flag, int add) {
171 37 : if (c->is_gmail) {
172 : /* Translate IMAP flag names to Gmail label operations */
173 21 : if (strcmp(flag, "\\Seen") == 0) {
174 : /* \Seen add → remove UNREAD; \Seen remove → add UNREAD */
175 10 : const char *label = "UNREAD";
176 10 : if (add) {
177 5 : const char *rm[] = { label };
178 5 : return gmail_modify_labels(c->gmail, uid, NULL, 0, rm, 1);
179 : } else {
180 5 : const char *ad[] = { label };
181 5 : return gmail_modify_labels(c->gmail, uid, ad, 1, NULL, 0);
182 : }
183 : }
184 11 : if (strcmp(flag, "\\Flagged") == 0) {
185 10 : const char *label = "STARRED";
186 10 : if (add) {
187 5 : const char *ad[] = { label };
188 5 : return gmail_modify_labels(c->gmail, uid, ad, 1, NULL, 0);
189 : } else {
190 5 : const char *rm[] = { label };
191 5 : return gmail_modify_labels(c->gmail, uid, NULL, 0, rm, 1);
192 : }
193 : }
194 : /* Unknown flag — ignore for Gmail */
195 1 : logger_log(LOG_DEBUG, "mail_client: Gmail ignoring flag '%s'", flag);
196 1 : return 0;
197 : }
198 16 : return imap_uid_set_flag(c->imap, uid, flag, add);
199 : }
200 :
201 2 : int mail_client_trash(MailClient *c, const char *uid) {
202 2 : if (c->is_gmail)
203 1 : return gmail_trash(c->gmail, uid);
204 :
205 : /* IMAP: set \Deleted flag */
206 1 : return imap_uid_set_flag(c->imap, uid, "\\Deleted", 1);
207 : }
208 :
209 2 : int mail_client_delete(MailClient *c, const char *uid) {
210 2 : if (c->is_gmail)
211 1 : return gmail_delete_permanently(c->gmail, uid);
212 1 : return imap_uid_delete(c->imap, uid);
213 : }
214 :
215 2 : int mail_client_move_to_folder(MailClient *c, const char *uid, const char *target_folder) {
216 2 : if (c->is_gmail) {
217 1 : logger_log(LOG_DEBUG, "mail_client: Gmail ignoring move to '%s'", target_folder);
218 1 : return 0;
219 : }
220 1 : return imap_uid_move(c->imap, uid, target_folder);
221 : }
222 :
223 3 : int mail_client_mark_junk(MailClient *c, const char *uid) {
224 3 : if (c->is_gmail) {
225 2 : const char *add[] = { "SPAM" };
226 2 : const char *rm[] = { "INBOX" };
227 2 : 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 3 : int mail_client_mark_notjunk(MailClient *c, const char *uid) {
235 3 : if (c->is_gmail) {
236 2 : const char *add[] = { "INBOX" };
237 2 : const char *rm[] = { "SPAM" };
238 2 : 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 6 : int mail_client_list_with_ids(MailClient *c, char ***names_out,
248 : char ***ids_out, int *count_out) {
249 6 : *names_out = NULL;
250 6 : *ids_out = NULL;
251 6 : *count_out = 0;
252 :
253 6 : if (c->is_gmail) {
254 5 : return gmail_list_labels(c->gmail, names_out, ids_out, count_out);
255 : }
256 :
257 : /* IMAP: list folders, then duplicate names as IDs */
258 1 : int rc = imap_list(c->imap, names_out, count_out, NULL);
259 1 : if (rc != 0 || *count_out == 0) return rc;
260 :
261 1 : char **ids = calloc((size_t)*count_out, sizeof(char *));
262 1 : 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 3 : for (int i = 0; i < *count_out; i++) {
270 2 : ids[i] = strdup((*names_out)[i]);
271 2 : 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 1 : *ids_out = ids;
283 1 : return 0;
284 : }
285 :
286 : /* ── Create / delete label or folder ─────────────────────────────── */
287 :
288 3 : int mail_client_create_label(MailClient *c, const char *name, char **id_out) {
289 3 : if (id_out) *id_out = NULL;
290 3 : if (!c->is_gmail) {
291 1 : fprintf(stderr, "Error: 'create-label' is Gmail-only. Use 'create-folder' for IMAP.\n");
292 1 : return -1;
293 : }
294 2 : return gmail_create_label(c->gmail, name, id_out);
295 : }
296 :
297 3 : int mail_client_delete_label(MailClient *c, const char *label_id) {
298 3 : if (!c->is_gmail) {
299 1 : fprintf(stderr, "Error: 'delete-label' is Gmail-only. Use 'delete-folder' for IMAP.\n");
300 1 : return -1;
301 : }
302 2 : return gmail_delete_label(c->gmail, label_id);
303 : }
304 :
305 2 : int mail_client_create_folder(MailClient *c, const char *name) {
306 2 : if (c->is_gmail) {
307 1 : fprintf(stderr, "Error: 'create-folder' is IMAP-only. Use 'create-label' for Gmail.\n");
308 1 : return -1;
309 : }
310 1 : return imap_create_folder(c->imap, name);
311 : }
312 :
313 2 : int mail_client_delete_folder(MailClient *c, const char *name) {
314 2 : if (c->is_gmail) {
315 1 : fprintf(stderr, "Error: 'delete-folder' is IMAP-only. Use 'delete-label' for Gmail.\n");
316 1 : return -1;
317 : }
318 1 : return imap_delete_folder(c->imap, name);
319 : }
320 :
321 : /* ── Label modify (Gmail only) ────────────────────────────────────── */
322 :
323 8 : int mail_client_modify_label(MailClient *c, const char *uid,
324 : const char *label_id, int add) {
325 8 : if (!c->is_gmail) return 0; /* no-op for IMAP */
326 :
327 8 : if (add) {
328 4 : const char *add_arr[] = {label_id};
329 4 : return gmail_modify_labels(c->gmail, uid, add_arr, 1, NULL, 0);
330 : } else {
331 4 : const char *rm_arr[] = {label_id};
332 4 : return gmail_modify_labels(c->gmail, uid, NULL, 0, rm_arr, 1);
333 : }
334 : }
335 :
336 : /* ── Append / Send ────────────────────────────────────────────────── */
337 :
338 2 : int mail_client_append(MailClient *c, const char *folder,
339 : const char *msg, size_t msg_len) {
340 2 : if (c->is_gmail) {
341 : /* Gmail: send via REST API (folder is ignored) */
342 : (void)folder;
343 1 : 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 209 : int mail_client_select_ext(MailClient *c, const char *folder,
351 : uint32_t known_uidval, uint64_t known_modseq,
352 : ImapSelectResult *res_out) {
353 209 : memset(res_out, 0, sizeof(*res_out));
354 209 : free(c->selected);
355 209 : c->selected = folder ? strdup(folder) : NULL;
356 :
357 209 : 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 8 : int mail_client_fetch_flags_changedsince(MailClient *c, uint64_t modseq,
372 : ImapFlagUpdate **out, int *count_out) {
373 8 : *out = NULL;
374 8 : *count_out = 0;
375 8 : 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 392 : void mail_client_set_progress(MailClient *c, ImapProgressFn fn, void *ctx) {
382 392 : if (!c) return;
383 391 : 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 1 : int mail_client_sync(MailClient *c) {
391 1 : if (c->is_gmail)
392 1 : return gmail_sync(c->gmail);
393 : /* IMAP sync is handled by email_service_sync, not here */
394 0 : return 0;
395 : }
|