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 333 : MailClient *mail_client_connect(Config *cfg) {
25 333 : if (!cfg) return NULL;
26 :
27 332 : MailClient *mc = calloc(1, sizeof(*mc));
28 332 : if (!mc) return NULL;
29 332 : mc->cfg = cfg;
30 :
31 332 : if (cfg->gmail_mode) {
32 73 : mc->is_gmail = 1;
33 73 : mc->gmail = gmail_connect(cfg);
34 73 : if (!mc->gmail) { free(mc); return NULL; }
35 : } else {
36 259 : if (!cfg->host) { free(mc); return NULL; }
37 504 : mc->imap = imap_connect(cfg->host, cfg->user, cfg->pass,
38 252 : cfg->ssl_no_verify ? 0 : 1);
39 252 : if (!mc->imap) { free(mc); return NULL; }
40 : }
41 :
42 312 : return mc;
43 : }
44 :
45 261 : void mail_client_free(MailClient *c) {
46 261 : if (!c) return;
47 260 : if (c->imap) imap_disconnect(c->imap);
48 260 : if (c->gmail) gmail_disconnect(c->gmail);
49 260 : free(c->selected);
50 260 : 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 24 : int mail_client_list(MailClient *c, char ***names_out, int *count_out, char *sep_out) {
60 24 : 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 22 : return imap_list(c->imap, names_out, count_out, sep_out);
73 : }
74 :
75 : /* ── Select ───────────────────────────────────────────────────────── */
76 :
77 221 : int mail_client_select(MailClient *c, const char *name) {
78 221 : free(c->selected);
79 221 : c->selected = name ? strdup(name) : NULL;
80 :
81 221 : if (c->is_gmail) {
82 : /* Gmail: no server-side SELECT needed; just remember the label. */
83 25 : return 0;
84 : }
85 196 : return imap_select(c->imap, name);
86 : }
87 :
88 : /* ── Search ───────────────────────────────────────────────────────── */
89 :
90 1127 : int mail_client_search(MailClient *c, MailSearchCriteria criteria,
91 : char (**uids_out)[17], int *count_out) {
92 1127 : 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 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 488 : char *mail_client_fetch_headers(MailClient *c, const char *uid) {
118 488 : if (c->is_gmail) {
119 : /* Fetch full message and return only the header portion */
120 2 : char *raw = gmail_fetch_message(c->gmail, uid, NULL, NULL);
121 2 : if (!raw) return NULL;
122 : /* Split at \r\n\r\n or \n\n boundary */
123 2 : const char *sep = strstr(raw, "\r\n\r\n");
124 : size_t hdr_len;
125 2 : if (sep) {
126 1 : 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 2 : char *headers = malloc(hdr_len + 1);
132 2 : if (!headers) { free(raw); return NULL; }
133 2 : memcpy(headers, raw, hdr_len);
134 2 : headers[hdr_len] = '\0';
135 2 : free(raw);
136 2 : return headers;
137 : }
138 486 : return imap_uid_fetch_headers(c->imap, uid);
139 : }
140 :
141 174 : char *mail_client_fetch_body(MailClient *c, const char *uid) {
142 174 : if (c->is_gmail)
143 6 : return gmail_fetch_message(c->gmail, uid, NULL, NULL);
144 168 : 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 36 : int mail_client_set_flag(MailClient *c, const char *uid,
170 : const char *flag, int add) {
171 36 : 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 15 : 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_move_to_folder(MailClient *c, const char *uid, const char *target_folder) {
210 2 : if (c->is_gmail) {
211 1 : logger_log(LOG_DEBUG, "mail_client: Gmail ignoring move to '%s'", target_folder);
212 1 : return 0;
213 : }
214 1 : return imap_uid_move(c->imap, uid, target_folder);
215 : }
216 :
217 3 : int mail_client_mark_junk(MailClient *c, const char *uid) {
218 3 : if (c->is_gmail) {
219 2 : const char *add[] = { "SPAM" };
220 2 : const char *rm[] = { "INBOX" };
221 2 : 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 3 : int mail_client_mark_notjunk(MailClient *c, const char *uid) {
229 3 : if (c->is_gmail) {
230 2 : const char *add[] = { "INBOX" };
231 2 : const char *rm[] = { "SPAM" };
232 2 : 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 6 : int mail_client_list_with_ids(MailClient *c, char ***names_out,
242 : char ***ids_out, int *count_out) {
243 6 : *names_out = NULL;
244 6 : *ids_out = NULL;
245 6 : *count_out = 0;
246 :
247 6 : if (c->is_gmail) {
248 5 : return gmail_list_labels(c->gmail, names_out, ids_out, count_out);
249 : }
250 :
251 : /* IMAP: list folders, then duplicate names as IDs */
252 1 : int rc = imap_list(c->imap, names_out, count_out, NULL);
253 1 : if (rc != 0 || *count_out == 0) return rc;
254 :
255 1 : char **ids = calloc((size_t)*count_out, sizeof(char *));
256 1 : 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 3 : for (int i = 0; i < *count_out; i++) {
264 2 : ids[i] = strdup((*names_out)[i]);
265 2 : 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 1 : *ids_out = ids;
277 1 : return 0;
278 : }
279 :
280 : /* ── Create / delete label or folder ─────────────────────────────── */
281 :
282 3 : int mail_client_create_label(MailClient *c, const char *name, char **id_out) {
283 3 : if (id_out) *id_out = NULL;
284 3 : if (!c->is_gmail) {
285 1 : fprintf(stderr, "Error: 'create-label' is Gmail-only. Use 'create-folder' for IMAP.\n");
286 1 : return -1;
287 : }
288 2 : return gmail_create_label(c->gmail, name, id_out);
289 : }
290 :
291 3 : int mail_client_delete_label(MailClient *c, const char *label_id) {
292 3 : if (!c->is_gmail) {
293 1 : fprintf(stderr, "Error: 'delete-label' is Gmail-only. Use 'delete-folder' for IMAP.\n");
294 1 : return -1;
295 : }
296 2 : return gmail_delete_label(c->gmail, label_id);
297 : }
298 :
299 2 : int mail_client_create_folder(MailClient *c, const char *name) {
300 2 : if (c->is_gmail) {
301 1 : fprintf(stderr, "Error: 'create-folder' is IMAP-only. Use 'create-label' for Gmail.\n");
302 1 : return -1;
303 : }
304 1 : return imap_create_folder(c->imap, name);
305 : }
306 :
307 2 : int mail_client_delete_folder(MailClient *c, const char *name) {
308 2 : if (c->is_gmail) {
309 1 : fprintf(stderr, "Error: 'delete-folder' is IMAP-only. Use 'delete-label' for Gmail.\n");
310 1 : return -1;
311 : }
312 1 : return imap_delete_folder(c->imap, name);
313 : }
314 :
315 : /* ── Label modify (Gmail only) ────────────────────────────────────── */
316 :
317 8 : int mail_client_modify_label(MailClient *c, const char *uid,
318 : const char *label_id, int add) {
319 8 : if (!c->is_gmail) return 0; /* no-op for IMAP */
320 :
321 8 : if (add) {
322 4 : const char *add_arr[] = {label_id};
323 4 : return gmail_modify_labels(c->gmail, uid, add_arr, 1, NULL, 0);
324 : } else {
325 4 : const char *rm_arr[] = {label_id};
326 4 : return gmail_modify_labels(c->gmail, uid, NULL, 0, rm_arr, 1);
327 : }
328 : }
329 :
330 : /* ── Append / Send ────────────────────────────────────────────────── */
331 :
332 2 : int mail_client_append(MailClient *c, const char *folder,
333 : const char *msg, size_t msg_len) {
334 2 : if (c->is_gmail) {
335 : /* Gmail: send via REST API (folder is ignored) */
336 : (void)folder;
337 1 : 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 153 : int mail_client_select_ext(MailClient *c, const char *folder,
345 : uint32_t known_uidval, uint64_t known_modseq,
346 : ImapSelectResult *res_out) {
347 153 : memset(res_out, 0, sizeof(*res_out));
348 153 : free(c->selected);
349 153 : c->selected = folder ? strdup(folder) : NULL;
350 :
351 153 : 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 8 : int mail_client_fetch_flags_changedsince(MailClient *c, uint64_t modseq,
366 : ImapFlagUpdate **out, int *count_out) {
367 8 : *out = NULL;
368 8 : *count_out = 0;
369 8 : 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 308 : void mail_client_set_progress(MailClient *c, ImapProgressFn fn, void *ctx) {
376 308 : if (!c) return;
377 307 : 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 1 : int mail_client_sync(MailClient *c) {
385 1 : if (c->is_gmail)
386 1 : return gmail_sync(c->gmail);
387 : /* IMAP sync is handled by email_service_sync, not here */
388 0 : return 0;
389 : }
|