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