Line data Source code
1 : #include <stdio.h>
2 : #include <stdlib.h>
3 : #include <string.h>
4 : #include <ctype.h>
5 : #include <locale.h>
6 : #include "config_store.h"
7 : #include "setup_wizard.h"
8 : #include "email_service.h"
9 : #include "platform/path.h"
10 : #include "raii.h"
11 : #include "logger.h"
12 : #include "local_store.h"
13 : #include "fs_util.h"
14 : #include "smtp_adapter.h"
15 : #include "compose_service.h"
16 : #include "help_gmail.h"
17 : #include "mail_rules.h"
18 : #include "when_expr.h"
19 :
20 : /* Default limit for batch output */
21 : #define BATCH_DEFAULT_LIMIT 100
22 :
23 : /* ── Help pages ──────────────────────────────────────────────────────── */
24 :
25 4 : static void help_general(void) {
26 4 : printf(
27 : "Usage: email-cli [<account>] <command> [options]\n"
28 : "\n"
29 : "Batch-mode email CLI with read and write operations.\n"
30 : "For the interactive TUI, use email-tui.\n"
31 : "\n"
32 : " <account> Email address of the account to use (e.g. user@example.com).\n"
33 : " Required when multiple accounts are configured.\n"
34 : " Alternative: --account <email>.\n"
35 : "\n"
36 : "Reading:\n"
37 : " list List messages in the configured mailbox\n"
38 : " show <uid> Display the full content of a message\n"
39 : " list-folders List IMAP folders (IMAP only)\n"
40 : " list-labels List Gmail labels with IDs (Gmail only)\n"
41 : " list-attachments <uid> List attachments in a message\n"
42 : " save-attachment <uid> Save a named attachment to disk\n"
43 : "\n"
44 : "Writing:\n"
45 : " send Send a message non-interactively\n"
46 : " mark-read <uid> Mark a message as read\n"
47 : " mark-unread <uid> Mark a message as unread\n"
48 : " mark-starred <uid> Star (flag) a message\n"
49 : " remove-starred <uid> Remove star from a message\n"
50 : " add-label <uid> <lbl> Add a Gmail label to a message\n"
51 : " remove-label <uid> <lbl> Remove a Gmail label from a message\n"
52 : " create-label <name> Create a Gmail label (Gmail only)\n"
53 : " delete-label <id> Delete a Gmail label (Gmail only)\n"
54 : " create-folder <name> Create an IMAP folder (IMAP only)\n"
55 : " delete-folder <name> Delete an IMAP folder (IMAP only)\n"
56 : " mark-junk <uid> Mark a message as junk/spam\n"
57 : " mark-notjunk <uid> Mark a message as not-junk (ham)\n"
58 : "\n"
59 : "Account management:\n"
60 : " list-accounts List all configured accounts\n"
61 : " add-account Add a new account (runs setup wizard)\n"
62 : " remove-account <email> Remove an account (local data preserved)\n"
63 : " config View or update configuration (incl. SMTP)\n"
64 : " migrate-credentials Re-encrypt (or decrypt) all stored passwords\n"
65 : "\n"
66 : "Rules:\n"
67 : " rules list List sorting rules for the account\n"
68 : " rules apply [--dry-run] Apply rules to locally cached messages\n"
69 : " rules add --name ... Add a sorting rule\n"
70 : " rules remove --name ... Remove a sorting rule by name\n"
71 : "\n"
72 : "Help:\n"
73 : " help [command] Show this help, or detailed help for a command\n"
74 : " help gmail Step-by-step Gmail OAuth2 setup guide\n"
75 : "\n"
76 : "Run 'email-cli help <command>' for more information.\n"
77 : "For the interactive TUI use email-tui.\n"
78 : "For background sync and cron management use email-sync.\n"
79 : );
80 4 : }
81 :
82 5 : static void help_rules(void) {
83 5 : printf(
84 : "Usage: email-cli [<account>] rules <subcommand> [options]\n"
85 : "\n"
86 : "Manage and apply mail sorting rules for an account.\n"
87 : "\n"
88 : "Subcommands:\n"
89 : " rules list List all sorting rules for the account\n"
90 : " rules apply [--dry-run] Apply rules to locally cached messages\n"
91 : " rules add --name <name> Add a new sorting rule\n"
92 : " rules remove --name <name> Remove a sorting rule by name\n"
93 : "\n"
94 : "Rules are stored in:\n"
95 : " ~/.config/email-cli/accounts/<account>/rules.ini\n"
96 : "\n"
97 : "Options:\n"
98 : " --account <email> Account to operate on (required if multiple configured)\n"
99 : " --help, -h Show help for this command\n"
100 : "\n"
101 : "Examples:\n"
102 : " email-cli rules list\n"
103 : " email-cli rules apply --dry-run\n"
104 : " email-cli rules add --name \"GitHub\" --if-from \"*@github.com\" --add-label GitHub\n"
105 : " email-cli rules remove --name \"GitHub\"\n"
106 : "\n"
107 : "Run 'email-cli help rules <subcommand>' for details on each subcommand.\n"
108 : );
109 5 : }
110 :
111 1 : static void help_rules_list(void) {
112 1 : printf(
113 : "Usage: email-cli [<account>] rules list\n"
114 : "\n"
115 : "List all sorting rules configured for the account.\n"
116 : "\n"
117 : "Rules are stored in:\n"
118 : " ~/.config/email-cli/accounts/<account>/rules.ini\n"
119 : "\n"
120 : "Each rule matches incoming messages by From / Subject / To / Label\n"
121 : "(glob patterns) and applies label additions, removals, or folder moves.\n"
122 : "\n"
123 : "Options:\n"
124 : " --account <email> Account to inspect (required if multiple configured)\n"
125 : " --help, -h Show this help message\n"
126 : "\n"
127 : "Examples:\n"
128 : " email-cli rules list\n"
129 : " email-cli user@example.com rules list\n"
130 : );
131 1 : }
132 :
133 1 : static void help_rules_apply(void) {
134 1 : printf(
135 : "Usage: email-cli [<account>] rules apply [--dry-run] [--verbose]\n"
136 : "\n"
137 : "Apply mail sorting rules retroactively to all locally cached messages.\n"
138 : "Does not re-download messages or contact the server.\n"
139 : "\n"
140 : "Options:\n"
141 : " --dry-run Print what would change without writing anything\n"
142 : " --verbose, -v Show per-rule output for each matching message\n"
143 : " --account <email> Account to process (required if multiple configured)\n"
144 : " --help, -h Show this help message\n"
145 : "\n"
146 : "Examples:\n"
147 : " email-cli rules apply\n"
148 : " email-cli rules apply --dry-run\n"
149 : " email-cli rules apply --verbose\n"
150 : " email-cli user@example.com rules apply --dry-run --verbose\n"
151 : );
152 1 : }
153 :
154 1 : static void help_rules_add(void) {
155 1 : printf(
156 : "Usage: email-cli [<account>] rules add --name <name>\n"
157 : " [--if-from <glob>] [--if-subject <glob>]\n"
158 : " [--if-to <glob>] [--if-label <glob>]\n"
159 : " [--add-label <label>]... [--remove-label <label>]...\n"
160 : " [--move-folder <folder>]\n"
161 : "\n"
162 : "Add a new mail sorting rule for the account.\n"
163 : "\n"
164 : "Conditions (all omitted conditions match everything):\n"
165 : " --if-from <glob> Match sender address (glob pattern)\n"
166 : " --if-subject <glob> Match message subject (glob pattern)\n"
167 : " --if-to <glob> Match recipient address (glob pattern)\n"
168 : " --if-label <glob> Match an existing label (glob pattern)\n"
169 : "\n"
170 : "Actions:\n"
171 : " --add-label <label> Add this label to matching messages\n"
172 : " --remove-label <label> Remove this label from matching messages\n"
173 : " --move-folder <folder> Move to folder (IMAP only; ignored on Gmail)\n"
174 : "\n"
175 : "Options:\n"
176 : " --name <name> Rule name (required, must be unique)\n"
177 : " --account <email> Account to add rule to (required if multiple)\n"
178 : " --help, -h Show this help message\n"
179 : "\n"
180 : "Examples:\n"
181 : " email-cli rules add --name \"GitHub\" --if-from \"*@github.com\"\n"
182 : " --add-label GitHub --remove-label INBOX\n"
183 : " email-cli rules add --name \"Newsletters\" --if-subject \"*newsletter*\"\n"
184 : " --add-label Newsletter\n"
185 : );
186 1 : }
187 :
188 1 : static void help_rules_remove(void) {
189 1 : printf(
190 : "Usage: email-cli [<account>] rules remove --name <name>\n"
191 : "\n"
192 : "Remove a mail sorting rule by name.\n"
193 : "\n"
194 : "Options:\n"
195 : " --name <name> Exact name of the rule to remove (required)\n"
196 : " --account <email> Account to remove rule from (required if multiple)\n"
197 : " --help, -h Show this help message\n"
198 : "\n"
199 : "Examples:\n"
200 : " email-cli rules remove --name \"GitHub\"\n"
201 : " email-cli user@example.com rules remove --name \"Newsletters\"\n"
202 : );
203 1 : }
204 :
205 3 : static void help_list(void) {
206 3 : printf(
207 : "Usage: email-cli [<account>] list [options]\n"
208 : "\n"
209 : "Lists messages in the configured mailbox.\n"
210 : "Shows unread (UNSEEN) messages by default; use --all for everything.\n"
211 : "\n"
212 : "Options:\n"
213 : " --all Show all messages (not just unread).\n"
214 : " Unread messages are marked with 'N' and listed first.\n"
215 : " --folder <name> IMAP: use <name> instead of the configured folder.\n"
216 : " --label <id-or-name> Gmail: filter by label (alias for --folder).\n"
217 : " --limit <n> Show at most <n> messages (default: %d).\n"
218 : " --offset <n> Start listing from the <n>-th message (1-based).\n"
219 : "\n"
220 : "Gmail notes:\n"
221 : " Use 'email-cli list-labels' to see available labels and their IDs.\n"
222 : " Predefined labels: INBOX, SENT, DRAFT, SPAM, TRASH, STARRED, IMPORTANT.\n"
223 : "\n"
224 : "Examples (IMAP):\n"
225 : " email-cli list\n"
226 : " email-cli list --all\n"
227 : " email-cli list --all --offset 21\n"
228 : " email-cli list --folder INBOX.Sent --limit 50\n"
229 : "\n"
230 : "Examples (Gmail):\n"
231 : " email-cli list\n"
232 : " email-cli list --label INBOX\n"
233 : " email-cli list --label SENT\n"
234 : " email-cli user@gmail.com list --label Label_42\n",
235 : BATCH_DEFAULT_LIMIT
236 : );
237 3 : }
238 :
239 2 : static void help_show(void) {
240 2 : printf(
241 : "Usage: email-cli show <uid>\n"
242 : "\n"
243 : "Displays the full content of the message identified by <uid>.\n"
244 : "\n"
245 : " <uid> Numeric IMAP UID shown by 'email-cli list'\n"
246 : "\n"
247 : "The message is fetched from the server on first access and stored\n"
248 : "locally at ~/.local/share/email-cli/messages/<folder>/<uid>.eml.\n"
249 : "Subsequent reads are served from the local store.\n"
250 : );
251 2 : }
252 :
253 1 : static void help_folders(void) {
254 1 : printf(
255 : "Usage: email-cli list-folders [options]\n"
256 : "\n"
257 : "Lists all IMAP folders on the server. IMAP accounts only.\n"
258 : "Use 'list-labels' for Gmail accounts.\n"
259 : "\n"
260 : "Options:\n"
261 : " --tree Render the folder hierarchy as a tree.\n"
262 : "\n"
263 : "Examples:\n"
264 : " email-cli list-folders\n"
265 : " email-cli list-folders --tree\n"
266 : );
267 1 : }
268 :
269 4 : static void help_send(void) {
270 4 : printf(
271 : "Usage: email-cli send --to <addr> --subject <text> --body <text>\n"
272 : "\n"
273 : "Sends a message non-interactively (scriptable / batch mode).\n"
274 : "\n"
275 : "Options:\n"
276 : " --to <addr> Recipient email address (required)\n"
277 : " --subject <text> Subject line (required)\n"
278 : " --body <text> Message body text (required)\n"
279 : "\n"
280 : "SMTP settings must be configured (run 'email-cli config smtp').\n"
281 : "\n"
282 : "Examples:\n"
283 : " email-cli send --to friend@example.com --subject \"Hello\" --body \"Hi there!\"\n"
284 : );
285 4 : }
286 :
287 5 : static void help_config(void) {
288 5 : printf(
289 : "Usage: email-cli [<account>] config <subcommand>\n"
290 : "\n"
291 : "View or update configuration settings.\n"
292 : "\n"
293 : "Subcommands:\n"
294 : " show Print current configuration (passwords masked)\n"
295 : " imap Interactively configure IMAP (incoming mail) settings\n"
296 : " smtp Interactively configure SMTP (outgoing mail) settings\n"
297 : "\n"
298 : "SMTP settings are used by email-tui for composing and sending mail.\n"
299 : "Configuring them here writes to the shared config file.\n"
300 : "\n"
301 : "Examples:\n"
302 : " email-cli config show\n"
303 : " email-cli config imap\n"
304 : " email-cli user@example.com config show\n"
305 : );
306 5 : }
307 :
308 2 : static void help_attachments(void) {
309 2 : printf(
310 : "Usage: email-cli list-attachments <uid>\n"
311 : "\n"
312 : "Lists all attachments in the message identified by <uid>.\n"
313 : "Prints one line per attachment: filename and decoded size.\n"
314 : "\n"
315 : " <uid> Numeric IMAP UID shown by 'email-cli list'\n"
316 : "\n"
317 : "Examples:\n"
318 : " email-cli list-attachments 42\n"
319 : );
320 2 : }
321 :
322 2 : static void help_save_attachment(void) {
323 2 : printf(
324 : "Usage: email-cli save-attachment <uid> <filename> [dir]\n"
325 : "\n"
326 : "Saves the named attachment from message <uid> to disk.\n"
327 : "\n"
328 : " <uid> Numeric IMAP UID shown by 'email-cli list'\n"
329 : " <filename> Exact attachment filename shown by 'email-cli list-attachments'\n"
330 : " [dir] Destination directory (default: ~/Downloads or ~)\n"
331 : "\n"
332 : "Examples:\n"
333 : " email-cli save-attachment 42 report.pdf\n"
334 : " email-cli save-attachment 42 report.pdf /tmp\n"
335 : );
336 2 : }
337 :
338 1 : static void help_mark_read(void) {
339 1 : printf(
340 : "Usage: email-cli mark-read <uid> [--folder <name>]\n"
341 : " email-cli mark-unread <uid> [--folder <name>]\n"
342 : "\n"
343 : "Mark a message as read (removes the UNSEEN flag) or unread (adds it).\n"
344 : "\n"
345 : " <uid> Numeric IMAP UID shown by 'email-cli list'\n"
346 : " --folder <name> Folder/label containing the message (default: configured folder)\n"
347 : "\n"
348 : "Examples:\n"
349 : " email-cli mark-read 42\n"
350 : " email-cli mark-unread 42 --folder INBOX\n"
351 : );
352 1 : }
353 :
354 2 : static void help_mark_starred(void) {
355 2 : printf(
356 : "Usage: email-cli mark-starred <uid> [--folder <name>]\n"
357 : " email-cli remove-starred <uid> [--folder <name>]\n"
358 : "\n"
359 : "Star (flag) or un-star a message.\n"
360 : "\n"
361 : " <uid> Numeric IMAP UID shown by 'email-cli list'\n"
362 : " --folder <name> Folder/label containing the message\n"
363 : "\n"
364 : "Examples:\n"
365 : " email-cli mark-starred 42\n"
366 : " email-cli remove-starred 42\n"
367 : );
368 2 : }
369 :
370 3 : static void help_add_label(void) {
371 3 : printf(
372 : "Usage: email-cli add-label <uid> <label>\n"
373 : " email-cli remove-label <uid> <label>\n"
374 : "\n"
375 : "Add or remove a Gmail label on a message (Gmail only).\n"
376 : "\n"
377 : " <uid> Numeric message ID shown by 'email-cli list'\n"
378 : " <label> Gmail label ID (e.g. 'Label_12345' or 'STARRED')\n"
379 : "\n"
380 : "Examples:\n"
381 : " email-cli add-label 1abc23 Work\n"
382 : " email-cli remove-label 1abc23 Work\n"
383 : );
384 3 : }
385 :
386 1 : static void help_list_labels(void) {
387 1 : printf(
388 : "Usage: email-cli list-labels\n"
389 : "\n"
390 : "List all Gmail labels with their IDs. Gmail accounts only.\n"
391 : "Use 'list-folders' for IMAP accounts.\n"
392 : "\n"
393 : "Examples:\n"
394 : " email-cli list-labels\n"
395 : );
396 1 : }
397 :
398 1 : static void help_create_label(void) {
399 1 : printf(
400 : "Usage: email-cli create-label <name>\n"
401 : "\n"
402 : "Create a new Gmail label. Gmail accounts only.\n"
403 : "\n"
404 : " <name> Display name for the new label\n"
405 : "\n"
406 : "Examples:\n"
407 : " email-cli create-label Work\n"
408 : " email-cli create-label \"My Projects\"\n"
409 : );
410 1 : }
411 :
412 1 : static void help_delete_label(void) {
413 1 : printf(
414 : "Usage: email-cli delete-label <label-id>\n"
415 : "\n"
416 : "Delete a Gmail label. Gmail accounts only.\n"
417 : "System labels (INBOX, TRASH, etc.) cannot be deleted.\n"
418 : "\n"
419 : " <label-id> Label ID (from list-labels)\n"
420 : "\n"
421 : "Examples:\n"
422 : " email-cli delete-label Label_12345\n"
423 : );
424 1 : }
425 :
426 2 : static void help_create_folder(void) {
427 2 : printf(
428 : "Usage: email-cli create-folder <name>\n"
429 : "\n"
430 : "Create a new IMAP folder. IMAP accounts only.\n"
431 : "\n"
432 : " <name> Folder path / name\n"
433 : "\n"
434 : "Examples:\n"
435 : " email-cli create-folder Work\n"
436 : " email-cli create-folder Archive/2025\n"
437 : );
438 2 : }
439 :
440 2 : static void help_delete_folder(void) {
441 2 : printf(
442 : "Usage: email-cli delete-folder <name>\n"
443 : "\n"
444 : "Delete an IMAP folder. IMAP accounts only.\n"
445 : "System folders (INBOX, Trash, etc.) cannot be deleted.\n"
446 : "\n"
447 : " <name> Folder path / name\n"
448 : "\n"
449 : "Examples:\n"
450 : " email-cli delete-folder OldWork\n"
451 : );
452 2 : }
453 :
454 1 : static void help_mark_junk(void) {
455 1 : printf(
456 : "Usage: email-cli mark-junk <uid>\n"
457 : "\n"
458 : "Mark a message as junk/spam.\n"
459 : "IMAP: sets $Junk and clears $NotJunk.\n"
460 : "Gmail: adds SPAM label and removes INBOX.\n"
461 : "\n"
462 : " <uid> UID of the message (decimal or 16-char hex)\n"
463 : "\n"
464 : "Examples:\n"
465 : " email-cli mark-junk 12345\n"
466 : );
467 1 : }
468 :
469 1 : static void help_mark_notjunk(void) {
470 1 : printf(
471 : "Usage: email-cli mark-notjunk <uid>\n"
472 : "\n"
473 : "Mark a message as not-junk (ham).\n"
474 : "IMAP: sets $NotJunk and clears $Junk.\n"
475 : "Gmail: removes SPAM label and adds INBOX.\n"
476 : "\n"
477 : " <uid> UID of the message (decimal or 16-char hex)\n"
478 : "\n"
479 : "Examples:\n"
480 : " email-cli mark-notjunk 12345\n"
481 : );
482 1 : }
483 :
484 1 : static void help_list_accounts(void) {
485 1 : printf(
486 : "Usage: email-cli list-accounts\n"
487 : "\n"
488 : "List all configured accounts with their type and server.\n"
489 : "\n"
490 : "Examples:\n"
491 : " email-cli list-accounts\n"
492 : );
493 1 : }
494 :
495 1 : static void help_add_account(void) {
496 1 : printf(
497 : "Usage: email-cli add-account\n"
498 : "\n"
499 : "Run the interactive setup wizard to add a new account.\n"
500 : "Supports both IMAP and Gmail (OAuth2) accounts.\n"
501 : "\n"
502 : "Examples:\n"
503 : " email-cli add-account\n"
504 : );
505 1 : }
506 :
507 2 : static void help_remove_account(void) {
508 2 : printf(
509 : "Usage: email-cli remove-account <email>\n"
510 : "\n"
511 : "Remove a configured account by email address.\n"
512 : "Local messages are NOT deleted — they are preserved on disk.\n"
513 : "\n"
514 : " <email> Account email address shown by 'email-cli list-accounts'\n"
515 : "\n"
516 : "Examples:\n"
517 : " email-cli remove-account user@example.com\n"
518 : );
519 2 : }
520 :
521 : /* ── Helpers ─────────────────────────────────────────────────────────── */
522 :
523 60 : static int parse_uid(const char *s, char uid_out[17]) {
524 60 : if (!s || !*s) return -1;
525 : /* Accept 16-character hex strings directly (Gmail message IDs shown by list). */
526 60 : if (strlen(s) == 16) {
527 25 : int all_hex = 1;
528 409 : for (int i = 0; i < 16; i++) {
529 385 : if (!isxdigit((unsigned char)s[i])) { all_hex = 0; break; }
530 : }
531 25 : if (all_hex) {
532 24 : memcpy(uid_out, s, 16);
533 24 : uid_out[16] = '\0';
534 24 : return 0;
535 : }
536 : }
537 : /* Accept positive decimal integers (IMAP UIDs). */
538 : char *end;
539 36 : unsigned long v = strtoul(s, &end, 10);
540 36 : if (*end != '\0' || v == 0 || v > 4294967295UL) return -1;
541 31 : snprintf(uid_out, 17, "%016lu", v);
542 31 : return 0;
543 : }
544 :
545 3 : static void unknown_option(const char *cmd, const char *opt) {
546 3 : fprintf(stderr, "Unknown option '%s' for '%s'.\n", opt, cmd);
547 3 : fprintf(stderr, "Run 'email-cli help %s' for usage.\n", cmd);
548 3 : }
549 :
550 : /* ── Entry point ─────────────────────────────────────────────────────── */
551 :
552 : #ifndef EMAIL_CLI_VERSION
553 : #define EMAIL_CLI_VERSION "unknown"
554 : #endif
555 :
556 287 : int main(int argc, char *argv[]) {
557 : /* 0. Set locale so wcwidth() and mbsrtowcs() work correctly for
558 : * multi-byte UTF-8 characters (needed for column-width calculations). */
559 287 : setlocale(LC_ALL, "");
560 :
561 287 : if (argc >= 2 && (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-V") == 0)) {
562 1 : printf("email-cli %s\n", EMAIL_CLI_VERSION);
563 1 : return EXIT_SUCCESS;
564 : }
565 :
566 : /* 1. Determine cache directory for logs */
567 286 : const char *cache_base = platform_cache_dir();
568 286 : if (!cache_base) {
569 0 : fprintf(stderr, "Fatal: Could not determine cache directory.\n");
570 0 : return EXIT_FAILURE;
571 : }
572 :
573 286 : RAII_STRING char *log_dir = NULL;
574 286 : RAII_STRING char *log_file = NULL;
575 572 : if (asprintf(&log_dir, "%s/email-cli/logs", cache_base) == -1 ||
576 286 : asprintf(&log_file, "%s/session.log", log_dir) == -1) {
577 0 : fprintf(stderr, "Fatal: Memory allocation failed.\n");
578 0 : return EXIT_FAILURE;
579 : }
580 :
581 : /* 2. Account + command detection.
582 : * Supported forms:
583 : * email-cli [<account>] <command> [options]
584 : * email-cli --account <email> <command> [options]
585 : * <account> is the first non-flag positional arg that contains '@'. */
586 286 : const char *account_arg = NULL;
587 286 : int account_arg_idx = -1;
588 :
589 : /* Pass A: scan for --account flag anywhere in args */
590 1097 : for (int i = 1; i < argc; i++) {
591 811 : if (strcmp(argv[i], "--batch") == 0) continue;
592 655 : if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
593 1 : account_arg = argv[++i]; continue;
594 : }
595 : }
596 :
597 : /* Pass B: if no --account flag, check whether first positional arg is an email */
598 286 : if (!account_arg) {
599 376 : for (int i = 1; i < argc; i++) {
600 373 : if (argv[i][0] == '-') {
601 91 : if (strcmp(argv[i], "--account") == 0) i++; /* skip --account pair */
602 91 : continue;
603 : }
604 : /* First non-flag arg: if it looks like an email, treat as account */
605 282 : if (strchr(argv[i], '@')) {
606 61 : account_arg = argv[i];
607 61 : account_arg_idx = i;
608 : }
609 282 : break; /* stop after first non-flag arg */
610 : }
611 : }
612 :
613 : /* Command: first non-flag, non-account arg */
614 286 : const char *cmd = NULL;
615 286 : int cmd_idx = 0;
616 489 : for (int i = 1; i < argc; i++) {
617 486 : if (strcmp(argv[i], "--batch") == 0) continue;
618 346 : if (strcmp(argv[i], "--help") == 0) continue;
619 345 : if (strcmp(argv[i], "--account") == 0) { i++; continue; }
620 344 : if (i == account_arg_idx) continue; /* skip positional account */
621 283 : cmd = argv[i]; cmd_idx = i; break;
622 : }
623 :
624 : /* --help anywhere in the args: treat as "help <cmd>" */
625 1089 : for (int i = 1; i < argc; i++) {
626 811 : if (strcmp(argv[i], "--account") == 0) { i++; continue; }
627 810 : if (i == account_arg_idx) continue;
628 749 : if (strcmp(argv[i], "--help") == 0) {
629 8 : if (cmd && strcmp(cmd, "--help") != 0) {
630 : /* e.g. email-cli list --help */
631 7 : if (strcmp(cmd, "list") == 0) { help_list(); return EXIT_SUCCESS; }
632 6 : if (strcmp(cmd, "show") == 0) { help_show(); return EXIT_SUCCESS; }
633 6 : if (strcmp(cmd, "list-folders") == 0) { help_folders(); return EXIT_SUCCESS; }
634 5 : if (strcmp(cmd, "list-attachments") == 0) { help_attachments(); return EXIT_SUCCESS; }
635 5 : if (strcmp(cmd, "save-attachment") == 0) { help_save_attachment(); return EXIT_SUCCESS; }
636 5 : if (strcmp(cmd, "send") == 0) { help_send(); return EXIT_SUCCESS; }
637 5 : if (strcmp(cmd, "config") == 0) { help_config(); return EXIT_SUCCESS; }
638 3 : if (strcmp(cmd, "gmail") == 0) { help_gmail(); return EXIT_SUCCESS; }
639 3 : if (strcmp(cmd, "mark-read") == 0 ||
640 3 : strcmp(cmd, "mark-unread") == 0) { help_mark_read(); return EXIT_SUCCESS; }
641 3 : if (strcmp(cmd, "mark-starred") == 0 ||
642 3 : strcmp(cmd, "remove-starred") == 0) { help_mark_starred(); return EXIT_SUCCESS; }
643 3 : if (strcmp(cmd, "add-label") == 0 ||
644 3 : strcmp(cmd, "remove-label") == 0) { help_add_label(); return EXIT_SUCCESS; }
645 3 : if (strcmp(cmd, "list-labels") == 0) { help_list_labels(); return EXIT_SUCCESS; }
646 3 : if (strcmp(cmd, "create-label") == 0) { help_create_label(); return EXIT_SUCCESS; }
647 3 : if (strcmp(cmd, "delete-label") == 0) { help_delete_label(); return EXIT_SUCCESS; }
648 3 : if (strcmp(cmd, "create-folder") == 0) { help_create_folder(); return EXIT_SUCCESS; }
649 2 : if (strcmp(cmd, "delete-folder") == 0) { help_delete_folder(); return EXIT_SUCCESS; }
650 1 : if (strcmp(cmd, "mark-junk") == 0) { help_mark_junk(); return EXIT_SUCCESS; }
651 1 : if (strcmp(cmd, "mark-notjunk") == 0) { help_mark_notjunk(); return EXIT_SUCCESS; }
652 1 : if (strcmp(cmd, "list-accounts") == 0) { help_list_accounts(); return EXIT_SUCCESS; }
653 1 : if (strcmp(cmd, "add-account") == 0) { help_add_account(); return EXIT_SUCCESS; }
654 1 : if (strcmp(cmd, "remove-account") == 0) { help_remove_account(); return EXIT_SUCCESS; }
655 1 : if (strcmp(cmd, "rules") == 0) { help_rules(); return EXIT_SUCCESS; }
656 : }
657 : /* email-cli --help or email-cli help --help */
658 1 : help_general();
659 1 : return EXIT_SUCCESS;
660 : }
661 : }
662 :
663 278 : if (cmd && strcmp(cmd, "help") == 0) {
664 : /* First arg after the command is the topic */
665 26 : const char *topic = NULL;
666 : /* Collect up to two topic words for multi-word topics like "rules apply" */
667 26 : const char *topic2 = NULL;
668 51 : for (int i = cmd_idx + 1; i < argc; i++) {
669 29 : if (strcmp(argv[i], "--batch") == 0) continue;
670 29 : if (!topic) { topic = argv[i]; continue; }
671 4 : topic2 = argv[i]; break;
672 : }
673 26 : if (topic) {
674 : /* Multi-word topics */
675 25 : if (strcmp(topic, "rules") == 0 && topic2) {
676 4 : if (strcmp(topic2, "apply") == 0) { help_rules_apply(); return EXIT_SUCCESS; }
677 3 : if (strcmp(topic2, "add") == 0) { help_rules_add(); return EXIT_SUCCESS; }
678 2 : if (strcmp(topic2, "remove") == 0) { help_rules_remove(); return EXIT_SUCCESS; }
679 1 : if (strcmp(topic2, "list") == 0) { help_rules_list(); return EXIT_SUCCESS; }
680 : }
681 21 : if (strcmp(topic, "list") == 0) { help_list(); return EXIT_SUCCESS; }
682 19 : if (strcmp(topic, "show") == 0) { help_show(); return EXIT_SUCCESS; }
683 18 : if (strcmp(topic, "list-folders") == 0) { help_folders(); return EXIT_SUCCESS; }
684 18 : if (strcmp(topic, "list-attachments") == 0) { help_attachments(); return EXIT_SUCCESS; }
685 17 : if (strcmp(topic, "save-attachment") == 0) { help_save_attachment(); return EXIT_SUCCESS; }
686 16 : if (strcmp(topic, "send") == 0) { help_send(); return EXIT_SUCCESS; }
687 15 : if (strcmp(topic, "config") == 0) { help_config(); return EXIT_SUCCESS; }
688 14 : if (strcmp(topic, "gmail") == 0) { help_gmail(); return EXIT_SUCCESS; }
689 14 : if (strcmp(topic, "mark-read") == 0 ||
690 14 : strcmp(topic, "mark-unread") == 0) { help_mark_read(); return EXIT_SUCCESS; }
691 13 : if (strcmp(topic, "mark-starred") == 0 ||
692 13 : strcmp(topic, "remove-starred") == 0) { help_mark_starred(); return EXIT_SUCCESS; }
693 11 : if (strcmp(topic, "add-label") == 0 ||
694 11 : strcmp(topic, "remove-label") == 0) { help_add_label(); return EXIT_SUCCESS; }
695 10 : if (strcmp(topic, "list-labels") == 0) { help_list_labels(); return EXIT_SUCCESS; }
696 9 : if (strcmp(topic, "create-label") == 0) { help_create_label(); return EXIT_SUCCESS; }
697 9 : if (strcmp(topic, "delete-label") == 0) { help_delete_label(); return EXIT_SUCCESS; }
698 9 : if (strcmp(topic, "create-folder") == 0) { help_create_folder(); return EXIT_SUCCESS; }
699 9 : if (strcmp(topic, "delete-folder") == 0) { help_delete_folder(); return EXIT_SUCCESS; }
700 9 : if (strcmp(topic, "mark-junk") == 0) { help_mark_junk(); return EXIT_SUCCESS; }
701 9 : if (strcmp(topic, "mark-notjunk") == 0) { help_mark_notjunk(); return EXIT_SUCCESS; }
702 8 : if (strcmp(topic, "list-accounts") == 0) { help_list_accounts(); return EXIT_SUCCESS; }
703 7 : if (strcmp(topic, "add-account") == 0) { help_add_account(); return EXIT_SUCCESS; }
704 6 : if (strcmp(topic, "remove-account") == 0) { help_remove_account(); return EXIT_SUCCESS; }
705 5 : if (strcmp(topic, "rules") == 0) { help_rules(); return EXIT_SUCCESS; }
706 1 : fprintf(stderr, "Unknown command '%s'.\n", topic);
707 1 : fprintf(stderr, "Run 'email-cli help' for available commands.\n");
708 1 : return EXIT_FAILURE;
709 : }
710 1 : help_general();
711 1 : return EXIT_SUCCESS;
712 : }
713 :
714 : /* No command: show help and exit. */
715 252 : if (!cmd) {
716 2 : help_general();
717 2 : return EXIT_SUCCESS;
718 : }
719 :
720 : /* 3. Initialize logger */
721 250 : if (fs_mkdir_p(log_dir, 0700) != 0)
722 0 : fprintf(stderr, "Warning: Could not create log directory %s\n", log_dir);
723 250 : if (logger_init(log_file, LOG_DEBUG) != 0)
724 0 : fprintf(stderr, "Warning: Logging system failed to initialize.\n");
725 250 : logger_log(LOG_INFO, "--- email-cli starting (cmd: %s) ---", cmd);
726 :
727 : /* Determine if this command needs a specific account config.
728 : * migrate-credentials operates on all accounts and handles its own loading. */
729 494 : int cmd_needs_cfg = !(cmd && (strcmp(cmd, "add-account") == 0 ||
730 244 : strcmp(cmd, "list-accounts") == 0 ||
731 242 : strcmp(cmd, "remove-account") == 0 ||
732 239 : strcmp(cmd, "migrate-credentials") == 0 ||
733 231 : strcmp(cmd, "rules") == 0));
734 :
735 : /* 4. Load configuration */
736 250 : Config *cfg = NULL;
737 250 : if (cmd_needs_cfg) {
738 207 : if (account_arg) {
739 : /* Specific account requested */
740 61 : cfg = config_load_account(account_arg);
741 61 : if (!cfg) {
742 1 : fprintf(stderr,
743 : "Error: Account '%s' not found.\n"
744 : "Run 'email-cli list-accounts' to list configured accounts.\n",
745 : account_arg);
746 1 : logger_close();
747 1 : return EXIT_FAILURE;
748 : }
749 : } else {
750 : /* No account specified: use the only account or run wizard */
751 146 : int count = 0;
752 146 : AccountEntry *list = config_list_accounts(&count);
753 146 : if (count == 1) {
754 144 : cfg = list[0].cfg; list[0].cfg = NULL;
755 144 : config_free_account_list(list, count);
756 2 : } else if (count > 1) {
757 1 : fprintf(stderr, "Multiple accounts configured. Specify which to use:\n");
758 3 : for (int i = 0; i < count; i++)
759 4 : fprintf(stderr, " email-cli %s %s\n",
760 2 : list[i].name ? list[i].name : "?", cmd ? cmd : "");
761 1 : fprintf(stderr, "Run 'email-cli list-accounts' for the full list.\n");
762 1 : config_free_account_list(list, count);
763 1 : logger_close();
764 2 : return EXIT_FAILURE;
765 : } else {
766 1 : config_free_account_list(list, count);
767 : /* No accounts: run setup wizard */
768 1 : logger_log(LOG_INFO, "No configuration found. Starting setup wizard.");
769 1 : cfg = setup_wizard_run();
770 1 : if (cfg) {
771 0 : if (config_save_to_store(cfg) != 0) {
772 0 : logger_log(LOG_ERROR, "Failed to save configuration.");
773 0 : fprintf(stderr, "Error: Failed to save configuration to disk.\n");
774 : } else {
775 0 : printf("Configuration saved. Run 'email-cli sync' to download your mail.\n");
776 : }
777 : } else {
778 1 : logger_log(LOG_ERROR, "Configuration aborted by user.");
779 1 : fprintf(stderr, "Configuration aborted. Exiting.\n");
780 1 : logger_close();
781 1 : return EXIT_FAILURE;
782 : }
783 : }
784 : }
785 : }
786 :
787 : /* 5. Initialize local store */
788 247 : if (cfg && local_store_init(cfg->host, cfg->user) != 0)
789 0 : logger_log(LOG_WARN, "Failed to initialize local store for %s", cfg->host);
790 :
791 : /* 6. Dispatch — batch mode only (no interactive TUI) */
792 247 : int result = -1;
793 :
794 247 : if (strcmp(cmd, "list") == 0) {
795 92 : EmailListOpts opts = {0, NULL, BATCH_DEFAULT_LIMIT, 0, 0, {0}, {0}};
796 92 : int ok = 1;
797 149 : for (int i = cmd_idx + 1; i < argc && ok; i++) {
798 57 : if (strcmp(argv[i], "--batch") == 0) {
799 9 : continue; /* accepted as no-op */
800 48 : } else if (strcmp(argv[i], "--all") == 0) {
801 6 : opts.all = 1;
802 42 : } else if (strcmp(argv[i], "--folder") == 0 ||
803 16 : strcmp(argv[i], "--label") == 0) {
804 28 : if (i + 1 >= argc) {
805 1 : fprintf(stderr, "Error: %s requires a name.\n", argv[i]);
806 1 : ok = 0;
807 : } else {
808 27 : opts.folder = argv[++i];
809 : }
810 14 : } else if (strcmp(argv[i], "--limit") == 0) {
811 8 : if (i + 1 >= argc) {
812 1 : fprintf(stderr, "Error: --limit requires a number.\n");
813 1 : ok = 0;
814 : } else {
815 : char *end;
816 7 : long v = strtol(argv[++i], &end, 10);
817 7 : if (*end != '\0' || v <= 0) {
818 1 : fprintf(stderr, "Error: --limit must be a positive integer.\n");
819 1 : ok = 0;
820 : } else {
821 6 : opts.limit = (int)v;
822 : }
823 : }
824 6 : } else if (strcmp(argv[i], "--offset") == 0) {
825 5 : if (i + 1 >= argc) {
826 1 : fprintf(stderr, "Error: --offset requires a number.\n");
827 1 : ok = 0;
828 : } else {
829 : char *end;
830 4 : long v = strtol(argv[++i], &end, 10);
831 4 : if (*end != '\0' || v < 1) {
832 1 : fprintf(stderr, "Error: --offset must be a positive integer.\n");
833 1 : ok = 0;
834 : } else {
835 3 : opts.offset = (int)v;
836 : }
837 : }
838 : } else {
839 1 : unknown_option("list", argv[i]);
840 1 : ok = 0;
841 : }
842 : }
843 92 : if (ok) result = email_service_list(cfg, &opts);
844 :
845 155 : } else if (strcmp(cmd, "show") == 0) {
846 : /* UID is the first non --batch arg after "show" */
847 15 : const char *uid_str = NULL;
848 15 : for (int i = cmd_idx + 1; i < argc; i++) {
849 14 : if (strcmp(argv[i], "--batch") == 0) continue;
850 14 : uid_str = argv[i]; break;
851 : }
852 15 : if (!uid_str) {
853 1 : fprintf(stderr, "Error: 'show' requires a UID argument.\n");
854 1 : help_show();
855 : } else {
856 : char uid[17];
857 14 : if (parse_uid(uid_str, uid) != 0)
858 1 : fprintf(stderr,
859 : "Error: invalid UID '%s' (expected decimal integer or 16-char hex).\n",
860 : uid_str);
861 : else
862 13 : result = email_service_read(cfg, uid, 0, BATCH_DEFAULT_LIMIT);
863 : }
864 :
865 140 : } else if (strcmp(cmd, "list-folders") == 0) {
866 10 : int tree = 0, ok = 1;
867 16 : for (int i = cmd_idx + 1; i < argc && ok; i++) {
868 6 : if (strcmp(argv[i], "--batch") == 0) continue;
869 3 : if (strcmp(argv[i], "--tree") == 0)
870 2 : tree = 1;
871 1 : else { unknown_option("list-folders", argv[i]); ok = 0; }
872 : }
873 10 : if (ok) result = email_service_list_folders(cfg, tree);
874 :
875 130 : } else if (strcmp(cmd, "list-attachments") == 0) {
876 6 : const char *uid_str = NULL;
877 6 : for (int i = cmd_idx + 1; i < argc; i++) {
878 5 : if (strcmp(argv[i], "--batch") == 0) continue;
879 5 : uid_str = argv[i]; break;
880 : }
881 6 : if (!uid_str) {
882 1 : fprintf(stderr, "Error: 'list-attachments' requires a UID argument.\n");
883 1 : help_attachments();
884 : } else {
885 : char uid[17];
886 5 : if (parse_uid(uid_str, uid) != 0)
887 1 : fprintf(stderr,
888 : "Error: invalid UID '%s' (expected decimal integer or 16-char hex).\n",
889 : uid_str);
890 : else
891 4 : result = email_service_list_attachments(cfg, uid);
892 : }
893 :
894 124 : } else if (strcmp(cmd, "save-attachment") == 0) {
895 5 : const char *uid_str = NULL;
896 5 : const char *filename = NULL;
897 5 : const char *outdir = NULL;
898 5 : int argn = 0;
899 16 : for (int i = cmd_idx + 1; i < argc; i++) {
900 11 : if (strcmp(argv[i], "--batch") == 0) continue;
901 11 : if (argn == 0) { uid_str = argv[i]; argn++; }
902 7 : else if (argn == 1) { filename = argv[i]; argn++; }
903 3 : else if (argn == 2) { outdir = argv[i]; argn++; }
904 : }
905 5 : if (!uid_str || !filename) {
906 1 : fprintf(stderr,
907 : "Error: 'save-attachment' requires a UID and a filename.\n");
908 1 : help_save_attachment();
909 : } else {
910 : char uid[17];
911 4 : if (parse_uid(uid_str, uid) != 0)
912 1 : fprintf(stderr,
913 : "Error: invalid UID '%s' (expected decimal integer or 16-char hex).\n",
914 : uid_str);
915 : else
916 3 : result = email_service_save_attachment(cfg, uid, filename, outdir);
917 : }
918 :
919 119 : } else if (strcmp(cmd, "config") == 0) {
920 : /* Account already loaded globally above. */
921 13 : const char *subcmd = (argc > cmd_idx + 1) ? argv[cmd_idx + 1] : "";
922 :
923 13 : if (strcmp(subcmd, "show") == 0) {
924 3 : printf("\nemail-cli configuration");
925 3 : if (cfg->user) printf(" (%s)", cfg->user);
926 3 : printf(":\n\n");
927 3 : printf(" IMAP:\n");
928 3 : printf(" Host: %s\n", cfg->host ? cfg->host : "(not set)");
929 3 : printf(" User: %s\n", cfg->user ? cfg->user : "(not set)");
930 3 : printf(" Password: %s\n", cfg->pass ? "****" : "(not set)");
931 3 : printf(" Folder: %s\n", cfg->folder ? cfg->folder : "INBOX");
932 3 : printf("\n SMTP:\n");
933 3 : if (cfg->smtp_host) {
934 2 : printf(" Host: %s\n", cfg->smtp_host);
935 2 : printf(" Port: %d\n", cfg->smtp_port ? cfg->smtp_port : 587);
936 2 : printf(" User: %s\n", cfg->smtp_user ? cfg->smtp_user : "(same as IMAP)");
937 2 : printf(" Password: %s\n", cfg->smtp_pass ? "****" : "(same as IMAP)");
938 : } else {
939 1 : printf(" (not configured — will be derived from IMAP host)\n");
940 : }
941 3 : printf("\n");
942 3 : result = 0;
943 :
944 10 : } else if (strcmp(subcmd, "imap") == 0) {
945 4 : if (setup_wizard_imap(cfg) == 0) {
946 4 : if (config_save_to_store(cfg) == 0) {
947 4 : printf("IMAP configuration saved.\n");
948 4 : result = 0;
949 : } else {
950 0 : fprintf(stderr, "Error: Could not save configuration.\n");
951 : }
952 : }
953 :
954 6 : } else if (strcmp(subcmd, "smtp") == 0) {
955 4 : if (setup_wizard_smtp(cfg) == 0) {
956 4 : if (config_save_to_store(cfg) == 0) {
957 4 : printf("SMTP configuration saved.\n");
958 4 : result = 0;
959 : } else {
960 0 : fprintf(stderr, "Error: Could not save configuration.\n");
961 : }
962 : }
963 :
964 : } else {
965 2 : if (subcmd[0])
966 1 : fprintf(stderr, "Unknown config subcommand '%s'.\n", subcmd);
967 2 : help_config();
968 2 : result = subcmd[0] ? -1 : 0;
969 : }
970 :
971 106 : } else if (strcmp(cmd, "send") == 0) {
972 9 : const char *to = NULL, *subject = NULL, *body = NULL;
973 9 : int ok = 1;
974 22 : for (int i = cmd_idx + 1; i < argc && ok; i++) {
975 13 : if (strcmp(argv[i], "--batch") == 0) continue;
976 13 : if (strcmp(argv[i], "--to") == 0) {
977 5 : if (i + 1 >= argc) {
978 1 : fprintf(stderr, "Error: --to requires an address.\n"); ok = 0;
979 4 : } else to = argv[++i];
980 8 : } else if (strcmp(argv[i], "--subject") == 0) {
981 4 : if (i + 1 >= argc) {
982 1 : fprintf(stderr, "Error: --subject requires text.\n"); ok = 0;
983 3 : } else subject = argv[++i];
984 4 : } else if (strcmp(argv[i], "--body") == 0) {
985 3 : if (i + 1 >= argc) {
986 1 : fprintf(stderr, "Error: --body requires text.\n"); ok = 0;
987 2 : } else body = argv[++i];
988 : } else {
989 1 : unknown_option("send", argv[i]); ok = 0;
990 : }
991 : }
992 9 : if (ok) {
993 5 : if (!to || !to[0] || !subject || !body) {
994 3 : fprintf(stderr, "Error: --to, --subject, and --body are all required.\n");
995 3 : help_send();
996 : } else {
997 2 : const char *from = cfg->smtp_user ? cfg->smtp_user : cfg->user;
998 2 : ComposeParams p = {from, to, NULL, NULL, subject, body, NULL};
999 2 : char *msg = NULL;
1000 2 : size_t msg_len = 0;
1001 2 : if (compose_build_message(&p, &msg, &msg_len) != 0) {
1002 0 : fprintf(stderr, "Error: Failed to build message.\n");
1003 : } else {
1004 2 : result = smtp_send(cfg, from, to, msg, msg_len);
1005 2 : if (result == 0) {
1006 2 : printf("Message sent.\n");
1007 2 : if (email_service_save_sent(cfg, msg, msg_len) == 0)
1008 2 : printf("Saved.\n");
1009 : else
1010 0 : fprintf(stderr, "(Could not save to Sent folder — "
1011 : "check EMAIL_SENT_FOLDER in config.)\n");
1012 : }
1013 2 : free(msg);
1014 : }
1015 : }
1016 : }
1017 :
1018 111 : } else if (strcmp(cmd, "mark-read") == 0 || strcmp(cmd, "mark-unread") == 0) {
1019 14 : const char *uid_str = NULL;
1020 14 : const char *folder = NULL;
1021 14 : int ok = 1;
1022 29 : for (int i = cmd_idx + 1; i < argc && ok; i++) {
1023 15 : if (strcmp(argv[i], "--batch") == 0) continue;
1024 15 : if (strcmp(argv[i], "--folder") == 0) {
1025 1 : if (i + 1 >= argc) {
1026 0 : fprintf(stderr, "Error: --folder requires a folder name.\n"); ok = 0;
1027 1 : } else folder = argv[++i];
1028 14 : } else if (!uid_str) {
1029 14 : uid_str = argv[i];
1030 : } else {
1031 0 : unknown_option(cmd, argv[i]); ok = 0;
1032 : }
1033 : }
1034 14 : if (ok) {
1035 14 : if (!uid_str) {
1036 0 : fprintf(stderr, "Error: '%s' requires a UID argument.\n", cmd);
1037 0 : help_mark_read();
1038 : } else {
1039 : char uid[17];
1040 14 : if (parse_uid(uid_str, uid) != 0)
1041 2 : fprintf(stderr, "Error: invalid UID '%s' (expected decimal integer or 16-char hex).\n", uid_str);
1042 : else {
1043 12 : int flag_add = (strcmp(cmd, "mark-unread") == 0) ? 1 : 0;
1044 12 : result = email_service_set_flag(cfg, uid, folder, MSG_FLAG_UNSEEN, flag_add);
1045 : }
1046 : }
1047 : }
1048 :
1049 95 : } else if (strcmp(cmd, "mark-starred") == 0 || strcmp(cmd, "remove-starred") == 0) {
1050 12 : const char *uid_str = NULL;
1051 12 : const char *folder = NULL;
1052 12 : int ok = 1;
1053 26 : for (int i = cmd_idx + 1; i < argc && ok; i++) {
1054 14 : if (strcmp(argv[i], "--batch") == 0) continue;
1055 14 : if (strcmp(argv[i], "--folder") == 0) {
1056 2 : if (i + 1 >= argc) {
1057 0 : fprintf(stderr, "Error: --folder requires a folder name.\n"); ok = 0;
1058 2 : } else folder = argv[++i];
1059 12 : } else if (!uid_str) {
1060 12 : uid_str = argv[i];
1061 : } else {
1062 0 : unknown_option(cmd, argv[i]); ok = 0;
1063 : }
1064 : }
1065 12 : if (ok) {
1066 12 : if (!uid_str) {
1067 0 : fprintf(stderr, "Error: '%s' requires a UID argument.\n", cmd);
1068 0 : help_mark_starred();
1069 : } else {
1070 : char uid[17];
1071 12 : if (parse_uid(uid_str, uid) != 0)
1072 0 : fprintf(stderr, "Error: invalid UID '%s' (expected decimal integer or 16-char hex).\n", uid_str);
1073 : else {
1074 12 : int flag_add = (strcmp(cmd, "mark-starred") == 0) ? 1 : 0;
1075 12 : result = email_service_set_flag(cfg, uid, folder, MSG_FLAG_FLAGGED, flag_add);
1076 : }
1077 : }
1078 : }
1079 :
1080 80 : } else if (strcmp(cmd, "add-label") == 0 || strcmp(cmd, "remove-label") == 0) {
1081 9 : const char *uid_str = NULL;
1082 9 : const char *label = NULL;
1083 9 : int argn = 0;
1084 23 : for (int i = cmd_idx + 1; i < argc; i++) {
1085 14 : if (strcmp(argv[i], "--batch") == 0) continue;
1086 14 : if (argn == 0) { uid_str = argv[i]; argn++; }
1087 7 : else if (argn == 1) { label = argv[i]; argn++; }
1088 : }
1089 9 : if (!uid_str || !label) {
1090 2 : fprintf(stderr, "Error: '%s' requires a UID and a label.\n", cmd);
1091 2 : help_add_label();
1092 : } else {
1093 : char uid[17];
1094 7 : if (parse_uid(uid_str, uid) != 0)
1095 0 : fprintf(stderr, "Error: invalid UID '%s' (expected decimal integer or 16-char hex).\n", uid_str);
1096 : else {
1097 7 : int add = (strcmp(cmd, "add-label") == 0) ? 1 : 0;
1098 7 : result = email_service_set_label(cfg, uid, label, add);
1099 : }
1100 : }
1101 :
1102 62 : } else if (strcmp(cmd, "list-labels") == 0) {
1103 2 : result = email_service_list_labels(cfg);
1104 :
1105 60 : } else if (strcmp(cmd, "create-label") == 0) {
1106 3 : const char *name = NULL;
1107 3 : for (int i = cmd_idx + 1; i < argc; i++) {
1108 2 : if (strcmp(argv[i], "--batch") == 0) continue;
1109 2 : name = argv[i]; break;
1110 : }
1111 3 : if (!name) {
1112 1 : fprintf(stderr, "Error: 'create-label' requires a label name.\n");
1113 1 : help_create_label();
1114 : } else {
1115 2 : result = email_service_create_label(cfg, name);
1116 : }
1117 :
1118 57 : } else if (strcmp(cmd, "delete-label") == 0) {
1119 3 : const char *label_id = NULL;
1120 3 : for (int i = cmd_idx + 1; i < argc; i++) {
1121 2 : if (strcmp(argv[i], "--batch") == 0) continue;
1122 2 : label_id = argv[i]; break;
1123 : }
1124 3 : if (!label_id) {
1125 1 : fprintf(stderr, "Error: 'delete-label' requires a label ID.\n");
1126 1 : help_delete_label();
1127 : } else {
1128 2 : result = email_service_delete_label(cfg, label_id);
1129 : }
1130 :
1131 54 : } else if (strcmp(cmd, "create-folder") == 0) {
1132 3 : const char *name = NULL;
1133 3 : for (int i = cmd_idx + 1; i < argc; i++) {
1134 2 : if (strcmp(argv[i], "--batch") == 0) continue;
1135 2 : name = argv[i]; break;
1136 : }
1137 3 : if (!name) {
1138 1 : fprintf(stderr, "Error: 'create-folder' requires a folder name.\n");
1139 1 : help_create_folder();
1140 : } else {
1141 2 : result = email_service_create_folder(cfg, name);
1142 : }
1143 :
1144 51 : } else if (strcmp(cmd, "delete-folder") == 0) {
1145 3 : const char *name = NULL;
1146 3 : for (int i = cmd_idx + 1; i < argc; i++) {
1147 2 : if (strcmp(argv[i], "--batch") == 0) continue;
1148 2 : name = argv[i]; break;
1149 : }
1150 3 : if (!name) {
1151 1 : fprintf(stderr, "Error: 'delete-folder' requires a folder name.\n");
1152 1 : help_delete_folder();
1153 : } else {
1154 2 : result = email_service_delete_folder(cfg, name);
1155 : }
1156 :
1157 53 : } else if (strcmp(cmd, "mark-junk") == 0 || strcmp(cmd, "mark-notjunk") == 0) {
1158 5 : const char *uid_str = NULL;
1159 5 : int ok = 1;
1160 9 : for (int i = cmd_idx + 1; i < argc && ok; i++) {
1161 4 : if (strcmp(argv[i], "--batch") == 0) continue;
1162 4 : if (!uid_str) { uid_str = argv[i]; }
1163 0 : else { unknown_option(cmd, argv[i]); ok = 0; }
1164 : }
1165 5 : if (ok) {
1166 5 : if (!uid_str) {
1167 1 : fprintf(stderr, "Error: '%s' requires a UID argument.\n", cmd);
1168 1 : if (strcmp(cmd, "mark-junk") == 0) help_mark_junk();
1169 0 : else help_mark_notjunk();
1170 : } else {
1171 : char uid[17];
1172 4 : if (parse_uid(uid_str, uid) != 0)
1173 0 : fprintf(stderr, "Error: invalid UID '%s' (expected decimal integer or 16-char hex).\n", uid_str);
1174 4 : else if (strcmp(cmd, "mark-junk") == 0)
1175 2 : result = email_service_mark_junk(cfg, uid);
1176 : else
1177 2 : result = email_service_mark_notjunk(cfg, uid);
1178 : }
1179 : }
1180 :
1181 43 : } else if (strcmp(cmd, "list-accounts") == 0) {
1182 2 : int count = 0;
1183 2 : AccountEntry *accs = config_list_accounts(&count);
1184 2 : if (count == 0) {
1185 0 : printf("No accounts configured.\n");
1186 0 : result = 0;
1187 : } else {
1188 2 : printf("%-40s %-8s %s\n", "Account", "Type", "Server");
1189 2 : printf("%-40s %-8s %s\n",
1190 : "----------------------------------------",
1191 : "--------",
1192 : "----------------------------");
1193 5 : for (int i = 0; i < count; i++) {
1194 3 : const char *type = (accs[i].cfg && accs[i].cfg->gmail_mode) ? "Gmail" : "IMAP";
1195 3 : const char *server = accs[i].cfg ? (accs[i].cfg->host ? accs[i].cfg->host : "-") : "-";
1196 3 : printf("%-40s %-8s %s\n",
1197 3 : accs[i].name ? accs[i].name : "?",
1198 : type, server);
1199 : }
1200 2 : config_free_account_list(accs, count);
1201 2 : result = 0;
1202 : }
1203 :
1204 41 : } else if (strcmp(cmd, "add-account") == 0) {
1205 6 : Config *new_cfg = setup_wizard_run();
1206 6 : if (new_cfg) {
1207 5 : if (config_save_account(new_cfg) == 0) {
1208 5 : printf("Account '%s' added.\n", new_cfg->user ? new_cfg->user : "?");
1209 5 : result = 0;
1210 : } else {
1211 0 : fprintf(stderr, "Error: Failed to save account.\n");
1212 : }
1213 5 : config_free(new_cfg);
1214 : } else {
1215 1 : fprintf(stderr, "Account setup cancelled.\n");
1216 1 : result = -1;
1217 : }
1218 :
1219 35 : } else if (strcmp(cmd, "remove-account") == 0) {
1220 3 : const char *account_name = NULL;
1221 3 : for (int i = cmd_idx + 1; i < argc; i++) {
1222 2 : if (strcmp(argv[i], "--batch") == 0) continue;
1223 2 : account_name = argv[i]; break;
1224 : }
1225 3 : if (!account_name) {
1226 1 : fprintf(stderr, "Error: 'remove-account' requires an account email.\n");
1227 1 : help_remove_account();
1228 : } else {
1229 : /* Verify account exists */
1230 2 : int acc_count = 0;
1231 2 : AccountEntry *accs = config_list_accounts(&acc_count);
1232 2 : int found = 0;
1233 3 : for (int i = 0; i < acc_count && !found; i++)
1234 1 : if (accs[i].name && strcmp(accs[i].name, account_name) == 0) found = 1;
1235 2 : config_free_account_list(accs, acc_count);
1236 :
1237 2 : if (!found) {
1238 1 : fprintf(stderr, "Error: Account '%s' not found.\n", account_name);
1239 : } else {
1240 1 : config_delete_account(account_name);
1241 :
1242 1 : const char *data_base = platform_data_dir();
1243 1 : printf("Account '%s' removed.\n", account_name);
1244 1 : printf("\n");
1245 1 : printf("Local messages have been PRESERVED and are NOT deleted.\n");
1246 1 : if (data_base) {
1247 1 : printf("Local data directory: %s/email-cli/accounts/%s/\n",
1248 : data_base, account_name);
1249 1 : printf("To delete local messages manually:\n");
1250 1 : printf(" rm -rf %s/email-cli/accounts/%s/\n",
1251 : data_base, account_name);
1252 : }
1253 1 : result = 0;
1254 : }
1255 : }
1256 :
1257 32 : } else if (strcmp(cmd, "migrate-credentials") == 0) {
1258 8 : int obfus = app_settings_get_obfuscation();
1259 8 : printf("Migrating credentials (%s)...\n",
1260 : obfus ? "encrypting stored passwords" : "removing encryption");
1261 8 : int rc = config_migrate_credentials();
1262 8 : if (rc == 0) {
1263 8 : printf("Done. All account credentials are now %s.\n",
1264 : obfus ? "obfuscated (enc: prefix)" : "stored as plaintext");
1265 8 : result = 0;
1266 : } else {
1267 0 : fprintf(stderr, "Error: one or more accounts could not be migrated.\n");
1268 : }
1269 :
1270 24 : } else if (strcmp(cmd, "rules") == 0) {
1271 : /* Determine subcommand: the first non-flag arg after "rules" */
1272 24 : const char *subcmd = NULL;
1273 24 : int subcmd_idx = -1;
1274 24 : for (int i = cmd_idx + 1; i < argc; i++) {
1275 23 : if (strcmp(argv[i], "--batch") == 0) continue;
1276 23 : if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
1277 0 : help_rules_list();
1278 0 : return EXIT_SUCCESS;
1279 : }
1280 23 : if (argv[i][0] != '-') { subcmd = argv[i]; subcmd_idx = i; break; }
1281 : }
1282 :
1283 35 : if (!subcmd || strcmp(subcmd, "list") == 0) {
1284 : /* rules list: show rules for the configured account(s) */
1285 13 : for (int i = subcmd_idx + 1; i < argc; i++) {
1286 2 : if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
1287 0 : help_rules_list(); return EXIT_SUCCESS;
1288 : }
1289 : }
1290 11 : int acc_count = 0;
1291 11 : AccountEntry *accounts = config_list_accounts(&acc_count);
1292 :
1293 11 : int printed = 0;
1294 22 : for (int i = 0; i < acc_count; i++) {
1295 11 : if (account_arg && account_arg[0] &&
1296 0 : strcmp(accounts[i].name, account_arg) != 0)
1297 0 : continue;
1298 :
1299 11 : MailRules *rules = mail_rules_load(accounts[i].name);
1300 :
1301 11 : if (!rules || rules->count == 0) {
1302 1 : printf("No rules configured for %s.\n", accounts[i].name);
1303 1 : printf(" (Create ~/.config/email-cli/accounts/%s/rules.ini"
1304 1 : " or use 'email-import-rules'.)\n", accounts[i].name);
1305 1 : mail_rules_free(rules);
1306 1 : printed++;
1307 1 : continue;
1308 : }
1309 :
1310 10 : printf("Rules for %s (%d rule%s):\n\n",
1311 10 : accounts[i].name, rules->count,
1312 10 : rules->count == 1 ? "" : "s");
1313 :
1314 32 : for (int r = 0; r < rules->count; r++) {
1315 22 : const MailRule *rule = &rules->rules[r];
1316 22 : printf(" %d. %s\n", r + 1,
1317 22 : rule->name && rule->name[0] ? rule->name : "(unnamed)");
1318 :
1319 22 : if (rule->when && rule->when[0])
1320 22 : printf(" when = %s\n", rule->when);
1321 : else {
1322 0 : if (rule->if_from) printf(" if-from = %s\n", rule->if_from);
1323 0 : if (rule->if_subject) printf(" if-subject = %s\n", rule->if_subject);
1324 0 : if (rule->if_to) printf(" if-to = %s\n", rule->if_to);
1325 0 : if (rule->if_label) printf(" if-label = %s\n", rule->if_label);
1326 : }
1327 :
1328 44 : for (int j = 0; j < rule->then_add_count; j++)
1329 22 : printf(" then-add-label = %s\n", rule->then_add_label[j]);
1330 27 : for (int j = 0; j < rule->then_rm_count; j++)
1331 5 : printf(" then-remove-label = %s\n", rule->then_rm_label[j]);
1332 22 : if (rule->then_move_folder)
1333 5 : printf(" then-move-folder = %s\n", rule->then_move_folder);
1334 22 : printf("\n");
1335 : }
1336 :
1337 10 : mail_rules_free(rules);
1338 10 : printed++;
1339 : }
1340 :
1341 11 : config_free_account_list(accounts, acc_count);
1342 :
1343 11 : if (!printed) {
1344 0 : fprintf(stderr, "Error: Account '%s' not found.\n",
1345 : account_arg ? account_arg : "");
1346 0 : result = -1;
1347 : } else {
1348 11 : result = 0;
1349 : }
1350 :
1351 13 : } else if (strcmp(subcmd, "apply") == 0) {
1352 : /* rules apply [--dry-run] [--verbose] */
1353 5 : int dry_run = 0, verbose = 0;
1354 8 : for (int i = subcmd_idx + 1; i < argc; i++) {
1355 3 : if (strcmp(argv[i], "--dry-run") == 0) { dry_run = 1; continue; }
1356 2 : if (strcmp(argv[i], "--verbose") == 0 ||
1357 2 : strcmp(argv[i], "-v") == 0) { verbose = 1; continue; }
1358 0 : if (strcmp(argv[i], "--help") == 0 ||
1359 0 : strcmp(argv[i], "-h") == 0) { help_rules_apply(); return EXIT_SUCCESS; }
1360 0 : if (strcmp(argv[i], "--batch") == 0) continue;
1361 : /* Skip account arg which is already parsed */
1362 0 : if (strcmp(argv[i], "--account") == 0) { i++; continue; }
1363 : }
1364 5 : int rc = email_service_apply_rules(account_arg, dry_run, verbose);
1365 5 : result = rc >= 0 ? 0 : -1;
1366 :
1367 8 : } else if (strcmp(subcmd, "add") == 0) {
1368 : /* rules add --name <name> [conditions] [actions] */
1369 5 : const char *rule_name = NULL;
1370 5 : const char *if_from = NULL;
1371 5 : const char *if_subject = NULL;
1372 5 : const char *if_to = NULL;
1373 5 : const char *if_label = NULL;
1374 5 : const char *move_folder = NULL;
1375 : const char *add_labels[MAIL_RULE_MAX_LABELS];
1376 5 : int add_label_count = 0;
1377 : const char *rm_labels[MAIL_RULE_MAX_LABELS];
1378 5 : int rm_label_count = 0;
1379 :
1380 19 : for (int i = subcmd_idx + 1; i < argc; i++) {
1381 14 : if (strcmp(argv[i], "--help") == 0 ||
1382 14 : strcmp(argv[i], "-h") == 0) { help_rules_add(); return EXIT_SUCCESS; }
1383 14 : if (strcmp(argv[i], "--batch") == 0) continue;
1384 14 : if (strcmp(argv[i], "--account") == 0) { i++; continue; }
1385 14 : if (strcmp(argv[i], "--name") == 0 && i+1 < argc) { rule_name = argv[++i]; continue; }
1386 10 : if (strcmp(argv[i], "--if-from") == 0 && i+1 < argc) { if_from = argv[++i]; continue; }
1387 6 : if (strcmp(argv[i], "--if-subject") == 0 && i+1 < argc) { if_subject = argv[++i]; continue; }
1388 5 : if (strcmp(argv[i], "--if-to") == 0 && i+1 < argc) { if_to = argv[++i]; continue; }
1389 5 : if (strcmp(argv[i], "--if-label") == 0 && i+1 < argc) { if_label = argv[++i]; continue; }
1390 5 : if (strcmp(argv[i], "--move-folder") == 0 && i+1 < argc) { move_folder = argv[++i]; continue; }
1391 4 : if (strcmp(argv[i], "--add-label") == 0 && i+1 < argc) {
1392 3 : if (add_label_count < MAIL_RULE_MAX_LABELS)
1393 3 : add_labels[add_label_count++] = argv[++i];
1394 0 : else i++;
1395 3 : continue;
1396 : }
1397 1 : if (strcmp(argv[i], "--remove-label") == 0 && i+1 < argc) {
1398 1 : if (rm_label_count < MAIL_RULE_MAX_LABELS)
1399 1 : rm_labels[rm_label_count++] = argv[++i];
1400 0 : else i++;
1401 1 : continue;
1402 : }
1403 0 : fprintf(stderr, "Unknown option '%s' for 'rules add'.\n", argv[i]);
1404 0 : fprintf(stderr, "Run 'email-cli help rules add' for usage.\n");
1405 0 : result = -1;
1406 2 : goto rules_done;
1407 : }
1408 :
1409 5 : if (!rule_name || !rule_name[0]) {
1410 1 : fprintf(stderr, "Error: --name is required for 'rules add'.\n");
1411 1 : fprintf(stderr, "Run 'email-cli help rules add' for usage.\n");
1412 1 : result = -1;
1413 1 : goto rules_done;
1414 : }
1415 :
1416 : /* Resolve target account name into a local copy */
1417 4 : char target_acct_buf[256] = {0};
1418 4 : if (account_arg && account_arg[0]) {
1419 0 : strncpy(target_acct_buf, account_arg, sizeof(target_acct_buf) - 1);
1420 : } else {
1421 4 : int acc_count = 0;
1422 4 : AccountEntry *accs = config_list_accounts(&acc_count);
1423 4 : if (acc_count == 1) {
1424 4 : strncpy(target_acct_buf, accs[0].name, sizeof(target_acct_buf) - 1);
1425 0 : } else if (acc_count > 1) {
1426 0 : fprintf(stderr, "Multiple accounts configured. Use --account.\n");
1427 0 : config_free_account_list(accs, acc_count);
1428 0 : result = -1;
1429 0 : goto rules_done;
1430 : } else {
1431 0 : fprintf(stderr, "No accounts configured.\n");
1432 0 : config_free_account_list(accs, acc_count);
1433 0 : result = -1;
1434 0 : goto rules_done;
1435 : }
1436 4 : config_free_account_list(accs, acc_count);
1437 : }
1438 :
1439 : {
1440 4 : MailRules *rules = mail_rules_load(target_acct_buf);
1441 4 : if (!rules) {
1442 1 : rules = calloc(1, sizeof(MailRules));
1443 1 : if (!rules) {
1444 0 : fprintf(stderr, "Error: out of memory.\n");
1445 0 : result = -1;
1446 0 : goto rules_done;
1447 : }
1448 : }
1449 : /* Check for duplicate name */
1450 4 : int dup = 0;
1451 6 : for (int r = 0; r < rules->count; r++) {
1452 3 : if (rules->rules[r].name &&
1453 3 : strcmp(rules->rules[r].name, rule_name) == 0) { dup = 1; break; }
1454 : }
1455 4 : if (dup) {
1456 1 : fprintf(stderr, "Error: A rule named \"%s\" already exists.\n", rule_name);
1457 1 : mail_rules_free(rules);
1458 1 : result = -1;
1459 1 : goto rules_done;
1460 : }
1461 : /* Grow array if needed */
1462 3 : if (rules->count >= rules->cap) {
1463 1 : int nc = rules->cap ? rules->cap * 2 : 8;
1464 1 : MailRule *tmp = realloc(rules->rules, (size_t)nc * sizeof(MailRule));
1465 1 : if (!tmp) {
1466 0 : fprintf(stderr, "Error: out of memory.\n");
1467 0 : mail_rules_free(rules);
1468 0 : result = -1;
1469 0 : goto rules_done;
1470 : }
1471 1 : rules->rules = tmp;
1472 1 : rules->cap = nc;
1473 : }
1474 3 : MailRule *nr = &rules->rules[rules->count++];
1475 3 : memset(nr, 0, sizeof(*nr));
1476 3 : nr->name = strdup(rule_name);
1477 3 : nr->if_from = if_from ? strdup(if_from) : NULL;
1478 3 : nr->if_subject = if_subject ? strdup(if_subject) : NULL;
1479 3 : nr->if_to = if_to ? strdup(if_to) : NULL;
1480 3 : nr->if_label = if_label ? strdup(if_label) : NULL;
1481 3 : nr->when = when_from_flat(if_from, if_subject, if_to, if_label,
1482 : NULL, NULL, NULL, NULL, 0, 0);
1483 3 : nr->then_move_folder = move_folder ? strdup(move_folder) : NULL;
1484 6 : for (int j = 0; j < add_label_count; j++)
1485 3 : nr->then_add_label[nr->then_add_count++] = strdup(add_labels[j]);
1486 4 : for (int j = 0; j < rm_label_count; j++)
1487 1 : nr->then_rm_label[nr->then_rm_count++] = strdup(rm_labels[j]);
1488 :
1489 3 : if (mail_rules_save(target_acct_buf, rules) == 0) {
1490 3 : printf("Rule added: \"%s\"\n", rule_name);
1491 3 : result = 0;
1492 : } else {
1493 0 : fprintf(stderr, "Error: failed to save rules.\n");
1494 0 : result = -1;
1495 : }
1496 3 : mail_rules_free(rules);
1497 : }
1498 :
1499 3 : } else if (strcmp(subcmd, "remove") == 0) {
1500 : /* rules remove --name <name> */
1501 3 : const char *rule_name = NULL;
1502 5 : for (int i = subcmd_idx + 1; i < argc; i++) {
1503 2 : if (strcmp(argv[i], "--help") == 0 ||
1504 2 : strcmp(argv[i], "-h") == 0) { help_rules_remove(); return EXIT_SUCCESS; }
1505 2 : if (strcmp(argv[i], "--batch") == 0) continue;
1506 2 : if (strcmp(argv[i], "--account") == 0) { i++; continue; }
1507 2 : if (strcmp(argv[i], "--name") == 0 && i+1 < argc) { rule_name = argv[++i]; continue; }
1508 0 : fprintf(stderr, "Unknown option '%s' for 'rules remove'.\n", argv[i]);
1509 0 : fprintf(stderr, "Run 'email-cli help rules remove' for usage.\n");
1510 0 : result = -1;
1511 2 : goto rules_done;
1512 : }
1513 :
1514 3 : if (!rule_name || !rule_name[0]) {
1515 1 : fprintf(stderr, "Error: --name is required for 'rules remove'.\n");
1516 1 : fprintf(stderr, "Run 'email-cli help rules remove' for usage.\n");
1517 1 : result = -1;
1518 1 : goto rules_done;
1519 : }
1520 :
1521 : /* Resolve target account name into a local copy */
1522 2 : char rem_acct_buf[256] = {0};
1523 2 : if (account_arg && account_arg[0]) {
1524 0 : strncpy(rem_acct_buf, account_arg, sizeof(rem_acct_buf) - 1);
1525 : } else {
1526 2 : int acc_count = 0;
1527 2 : AccountEntry *accs = config_list_accounts(&acc_count);
1528 2 : if (acc_count == 1) {
1529 2 : strncpy(rem_acct_buf, accs[0].name, sizeof(rem_acct_buf) - 1);
1530 0 : } else if (acc_count > 1) {
1531 0 : fprintf(stderr, "Multiple accounts configured. Use --account.\n");
1532 0 : config_free_account_list(accs, acc_count);
1533 0 : result = -1;
1534 0 : goto rules_done;
1535 : } else {
1536 0 : fprintf(stderr, "No accounts configured.\n");
1537 0 : config_free_account_list(accs, acc_count);
1538 0 : result = -1;
1539 0 : goto rules_done;
1540 : }
1541 2 : config_free_account_list(accs, acc_count);
1542 : }
1543 :
1544 : {
1545 2 : MailRules *rules = mail_rules_load(rem_acct_buf);
1546 2 : if (!rules || rules->count == 0) {
1547 0 : fprintf(stderr, "No rules found for %s.\n", rem_acct_buf);
1548 0 : mail_rules_free(rules);
1549 0 : result = -1;
1550 0 : goto rules_done;
1551 : }
1552 2 : int found = -1;
1553 3 : for (int r = 0; r < rules->count; r++) {
1554 2 : if (rules->rules[r].name &&
1555 2 : strcmp(rules->rules[r].name, rule_name) == 0) {
1556 1 : found = r; break;
1557 : }
1558 : }
1559 2 : if (found < 0) {
1560 1 : fprintf(stderr, "Error: No rule named \"%s\" found.\n", rule_name);
1561 1 : mail_rules_free(rules);
1562 1 : result = -1;
1563 1 : goto rules_done;
1564 : }
1565 : {
1566 1 : MailRule *r = &rules->rules[found];
1567 1 : free(r->name); free(r->when);
1568 1 : free(r->if_from); free(r->if_not_from);
1569 1 : free(r->if_subject); free(r->if_not_subject);
1570 1 : free(r->if_to); free(r->if_not_to);
1571 1 : free(r->if_label); free(r->if_body);
1572 1 : free(r->then_move_folder); free(r->then_forward_to);
1573 2 : for (int j = 0; j < r->then_add_count; j++) free(r->then_add_label[j]);
1574 2 : for (int j = 0; j < r->then_rm_count; j++) free(r->then_rm_label[j]);
1575 : }
1576 2 : for (int r = found; r < rules->count - 1; r++)
1577 1 : rules->rules[r] = rules->rules[r + 1];
1578 1 : rules->count--;
1579 1 : if (mail_rules_save(rem_acct_buf, rules) == 0) {
1580 1 : printf("Rule removed: \"%s\"\n", rule_name);
1581 1 : result = 0;
1582 : } else {
1583 0 : fprintf(stderr, "Error: failed to save rules.\n");
1584 0 : result = -1;
1585 : }
1586 1 : mail_rules_free(rules);
1587 : }
1588 :
1589 : } else {
1590 0 : fprintf(stderr, "Unknown 'rules' subcommand '%s'.\n", subcmd);
1591 0 : fprintf(stderr, "Available: rules list, rules apply, rules add, rules remove\n");
1592 0 : result = -1;
1593 : }
1594 24 : rules_done: ;
1595 :
1596 : } else {
1597 0 : fprintf(stderr, "Unknown command '%s'.\n", cmd);
1598 0 : fprintf(stderr, "Run 'email-cli help' for available commands.\n");
1599 : }
1600 :
1601 : /* 7. Cleanup */
1602 247 : config_free(cfg);
1603 247 : logger_log(LOG_INFO, "--- email-cli session finished ---");
1604 247 : logger_close();
1605 :
1606 247 : if (result >= 0)
1607 203 : return EXIT_SUCCESS;
1608 44 : fprintf(stderr, "\nFailed. Check logs in %s\n", log_file);
1609 44 : return EXIT_FAILURE;
1610 : }
|