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