Line data Source code
1 : /**
2 : * @file main_ro.c
3 : * @brief Entry point for email-cli-ro — read-only, non-interactive CLI.
4 : *
5 : * email-cli-ro is a strict subset of email-cli:
6 : * - All output is batch/non-interactive (no TUI, no pager prompts).
7 : * - No write operations (no SMTP, no IMAP flag changes, no cron writes).
8 : * - No setup wizard — configuration must already exist.
9 : * - Safe to give to AI agents: there is no code path that sends email.
10 : *
11 : * Supported commands: list, show, folders, attachments, save-attachment, help.
12 : */
13 :
14 : #include <stdio.h>
15 : #include <stdlib.h>
16 : #include <string.h>
17 : #include <ctype.h>
18 : #include <locale.h>
19 : #include "config_store.h"
20 : #include "email_service.h"
21 : #include "platform/terminal.h"
22 : #include "platform/path.h"
23 : #include "raii.h"
24 : #include "logger.h"
25 : #include "local_store.h"
26 : #include "fs_util.h"
27 : #include "config.h"
28 :
29 : #define BATCH_DEFAULT_LIMIT 100
30 :
31 : /* ── Help pages ──────────────────────────────────────────────────────── */
32 :
33 4 : static void help_general(void) {
34 4 : printf(
35 : "Usage: email-cli-ro [<account>] <command> [options]\n"
36 : "\n"
37 : "Read-only email CLI. All output is non-interactive (batch mode).\n"
38 : "Safe for use by AI agents: no send or write operations are available.\n"
39 : "\n"
40 : " <account> Email address of the account to use (e.g. user@example.com).\n"
41 : " Required when multiple accounts are configured.\n"
42 : " Alternative: --account <email>.\n"
43 : "\n"
44 : "Reading:\n"
45 : " list List messages in the configured mailbox\n"
46 : " show <uid> Display the full content of a message\n"
47 : " list-folders List available IMAP folders / Gmail labels\n"
48 : " list-labels List all labels (Gmail) or folders (IMAP)\n"
49 : " list-attachments <uid> List attachments in a message\n"
50 : " save-attachment <uid> <filename> [dir]\n"
51 : " Save an attachment to disk\n"
52 : "\n"
53 : "\n"
54 : "Finding messages (see 'email-cli-ro help list' for details):\n"
55 : " list --from <text> Filter by sender substring\n"
56 : " list --since/--before <date> Filter by date (YYYY-MM-DD)\n"
57 : " list --json Machine-readable output\n"
58 : " list --all-accounts Every configured account in turn\n"
59 : " list --folder __unread__ Virtual folders: __unread__ __flagged__\n"
60 : " __answered__ __forwarded__ __junk__\n"
61 : " __phishing__ __all__\n"
62 : " list --folder \"__search__:3:text\" Search cached mail\n"
63 : " (scope 0=Subject 1=From 2=To 3=Body)\n"
64 : " show <uid> --raw Message source, undecoded\n"
65 : "Account management:\n"
66 : " list-accounts List all configured accounts\n"
67 : "\n"
68 : "Help:\n"
69 : " help [command] Show this help, or detailed help for a command\n"
70 : "\n"
71 : "Run 'email-cli-ro help <command>' for more information.\n"
72 : "For write operations (send, mark-read, add-label, etc.) use 'email-cli'.\n"
73 : );
74 4 : }
75 :
76 : /* Accept only YYYY-MM-DD for --since/--before: the manifest stores dates in
77 : * that form, so anything else would silently match nothing. */
78 5 : static int valid_date_arg(const char *s) {
79 5 : if (!s) return 0;
80 49 : for (int i = 0; i < 10; i++) {
81 45 : if (i == 4 || i == 7) { if (s[i] != '-') return 0; }
82 36 : else if (s[i] < '0' || s[i] > '9') return 0;
83 : }
84 4 : return s[10] == '\0';
85 : }
86 :
87 3 : static void help_list(void) {
88 3 : printf(
89 : "Usage: email-cli-ro [<account>] list [options]\n"
90 : "\n"
91 : "Lists messages in the configured mailbox.\n"
92 : "Shows unread (UNSEEN) messages by default; use --all for everything.\n"
93 : "\n"
94 : "Options:\n"
95 : " --all Show all messages (not just unread).\n"
96 : " --folder <name> IMAP: use <name> instead of the configured folder.\n"
97 : " --label <id-or-name> Gmail: filter by label (alias for --folder).\n"
98 : " --limit <n> Show at most <n> messages (default: %d).\n"
99 : " --offset <n> Start listing from the <n>-th message (1-based).\n"
100 : " --from <text> Only messages whose From contains <text>\n"
101 : " (case-insensitive substring).\n"
102 : " --since <YYYY-MM-DD> Only messages on or after this date.\n"
103 : " --before <YYYY-MM-DD> Only messages strictly before this date.\n"
104 : " --all-accounts List every configured account in turn, each\n"
105 : " under an \"=== <account> ===\" heading.\n"
106 : " Cannot be combined with --json.\n"
107 : " --json Emit JSON instead of the text table: one\n"
108 : " object per message, fields never truncated.\n"
109 : " stdout stays a single parseable document;\n"
110 : " notices go to stderr.\n"
111 :
112 : "\n"
113 : "Virtual folders (pass to --folder; they span every cached folder):\n"
114 : " __unread__ Unread messages\n"
115 : " __flagged__ Starred / flagged messages\n"
116 : " __answered__ Messages that were replied to\n"
117 : " __forwarded__ Messages that were forwarded\n"
118 : " __junk__ Messages marked as junk / spam\n"
119 : " __phishing__ Messages flagged as phishing\n"
120 : " __all__ Every cached message, from every folder\n"
121 : "\n"
122 : "Content search (pass to --folder):\n"
123 : " __search__:<scope>:<query> scope 0=Subject 1=From 2=To 3=Body\n"
124 : " Searches the locally cached messages, so it works offline. Body\n"
125 : " search runs on the decoded text: base64 / quoted-printable parts and\n"
126 : " non-UTF-8 charsets are matched too, and HTML mail is searched as\n"
127 : " rendered text. Quote the argument -- it contains colons.\n"
128 : "\n"
129 : "Gmail notes:\n"
130 : " Use 'email-cli-ro list-labels' to see available labels and their IDs.\n"
131 : " Predefined labels: INBOX, SENT, DRAFT, SPAM, TRASH, STARRED, IMPORTANT.\n"
132 : "\n"
133 : "Examples (IMAP):\n"
134 : " email-cli-ro list\n"
135 : " email-cli-ro list --folder INBOX.Sent --limit 50\n"
136 : " email-cli-ro list --folder __unread__ --limit 20\n"
137 : " email-cli-ro list --all --from @shop.example --since 2024-01-01\n"
138 : " email-cli-ro list --all --folder __all__ --json\n"
139 : " email-cli-ro list --folder \"__search__:3:invoice\" (search message bodies)\n"
140 : "\n"
141 : "Examples (Gmail):\n"
142 : " email-cli-ro list\n"
143 : " email-cli-ro list --label INBOX\n"
144 : " email-cli-ro list --label SENT\n"
145 : " email-cli-ro user@gmail.com list --label Label_42\n",
146 : BATCH_DEFAULT_LIMIT
147 : );
148 3 : }
149 :
150 3 : static void help_show(void) {
151 3 : printf(
152 : "Usage: email-cli-ro show <uid> [--folder <name>] [--raw]\n"
153 : "\n"
154 : "Displays the full content of the message identified by <uid>.\n"
155 : "\n"
156 : " <uid> Numeric IMAP UID shown by 'email-cli-ro list'\n"
157 : " --folder <name> Folder/label containing the message.\n"
158 : " IMAP UIDs are unique only within a mailbox, so the same UID\n"
159 : " usually means a different message in each folder. Omitted,\n"
160 : " the folder is resolved from the local store: a UID cached in\n"
161 : " one folder is shown from there; if several hold it the\n"
162 : " configured folder wins and the others are named on stderr;\n"
163 : " if none of them is the configured folder the command fails\n"
164 : " and lists the candidates. Cross-folder listings print a\n"
165 : " Folder column to pass back here.\n"
166 : " --label <name> Gmail: alias for --folder.\n"
167 : " --raw Print the message exactly as stored (RFC 2822 source):\n"
168 : " no MIME parsing, no transfer-encoding or charset\n"
169 : " decoding, no HTML rendering. Use it to inspect the\n"
170 : " real headers when text looks mis-decoded.\n"
171 : "\n"
172 : "The message is fetched from the server on first access and stored\n"
173 : "locally under ~/.local/share/email-cli/accounts/<account>/store/.\n"
174 : "The exact path of a message is printed by 'show' as the File: line.\n"
175 : "Subsequent reads are served from the local store.\n"
176 : );
177 3 : }
178 :
179 1 : static void help_folders(void) {
180 1 : printf(
181 : "Usage: email-cli-ro list-folders [options]\n"
182 : "\n"
183 : "Lists all available IMAP folders on the server.\n"
184 : "\n"
185 : "Options:\n"
186 : " --tree Render the folder hierarchy as a tree.\n"
187 : "\n"
188 : "Examples:\n"
189 : " email-cli-ro list-folders\n"
190 : " email-cli-ro list-folders --tree\n"
191 : );
192 1 : }
193 :
194 3 : static void help_attachments(void) {
195 3 : printf(
196 : "Usage: email-cli-ro list-attachments <uid>\n"
197 : "\n"
198 : "Lists all attachments in the message identified by <uid>.\n"
199 : "Prints one line per attachment: filename and decoded size.\n"
200 : "\n"
201 : " <uid> Numeric IMAP UID shown by 'email-cli-ro list'\n"
202 : "\n"
203 : "Examples:\n"
204 : " email-cli-ro list-attachments 42\n"
205 : );
206 3 : }
207 :
208 3 : static void help_save_attachment(void) {
209 3 : printf(
210 : "Usage: email-cli-ro save-attachment <uid> <filename> [dir]\n"
211 : "\n"
212 : "Saves the named attachment from message <uid> to disk.\n"
213 : "\n"
214 : " <uid> Numeric IMAP UID shown by 'email-cli-ro list'\n"
215 : " <filename> Exact attachment filename shown by 'email-cli-ro list-attachments'\n"
216 : " [dir] Destination directory (default: ~/Downloads or ~)\n"
217 : "\n"
218 : "Examples:\n"
219 : " email-cli-ro save-attachment 42 report.pdf\n"
220 : " email-cli-ro save-attachment 42 report.pdf /tmp\n"
221 : );
222 3 : }
223 :
224 2 : static void help_list_labels(void) {
225 2 : printf(
226 : "Usage: email-cli-ro list-labels\n"
227 : "\n"
228 : "List all available labels (Gmail) or folders (IMAP).\n"
229 : "For Gmail, shows both the display name and the label ID.\n"
230 : "\n"
231 : "Examples:\n"
232 : " email-cli-ro list-labels\n"
233 : );
234 2 : }
235 :
236 2 : static void help_list_accounts(void) {
237 2 : printf(
238 : "Usage: email-cli-ro list-accounts\n"
239 : "\n"
240 : "List all configured accounts with their type and server.\n"
241 : "\n"
242 : "Examples:\n"
243 : " email-cli-ro list-accounts\n"
244 : );
245 2 : }
246 :
247 : /* ── Helpers ─────────────────────────────────────────────────────────── */
248 :
249 30 : static int parse_uid(const char *s, char uid_out[17]) {
250 30 : if (!s || !*s) return -1;
251 : /* Accept 16-character hex strings directly (Gmail message IDs shown by list). */
252 30 : if (strlen(s) == 16) {
253 12 : int all_hex = 1;
254 204 : for (int i = 0; i < 16; i++) {
255 192 : if (!isxdigit((unsigned char)s[i])) { all_hex = 0; break; }
256 : }
257 12 : if (all_hex) {
258 12 : memcpy(uid_out, s, 16);
259 12 : uid_out[16] = '\0';
260 12 : return 0;
261 : }
262 : }
263 : /* Accept positive decimal integers (IMAP UIDs). */
264 : char *end;
265 18 : unsigned long v = strtoul(s, &end, 10);
266 18 : if (*end != '\0' || v == 0 || v > 4294967295UL) return -1;
267 15 : snprintf(uid_out, 17, "%016lu", v);
268 15 : return 0;
269 : }
270 :
271 2 : static void unknown_option(const char *cmd, const char *opt) {
272 2 : fprintf(stderr, "Unknown option '%s' for '%s'.\n", opt, cmd);
273 2 : fprintf(stderr, "Run 'email-cli-ro help %s' for usage.\n", cmd);
274 2 : }
275 :
276 : /* ── Entry point ─────────────────────────────────────────────────────── */
277 :
278 : #ifndef EMAIL_CLI_VERSION
279 : #define EMAIL_CLI_VERSION "unknown"
280 : #endif
281 :
282 110 : int main(int argc, char *argv[]) {
283 110 : setlocale(LC_ALL, "");
284 :
285 110 : if (argc >= 2 && (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-V") == 0)) {
286 1 : printf("email-cli-ro %s\n", EMAIL_CLI_VERSION);
287 1 : return EXIT_SUCCESS;
288 : }
289 :
290 : /* 1. Determine cache directory for logs */
291 109 : const char *cache_base = platform_cache_dir();
292 109 : if (!cache_base) {
293 0 : fprintf(stderr, "Fatal: Could not determine cache directory.\n");
294 0 : return EXIT_FAILURE;
295 : }
296 :
297 109 : RAII_STRING char *log_dir = NULL;
298 109 : RAII_STRING char *log_file = NULL;
299 218 : if (asprintf(&log_dir, "%s/email-cli/logs", cache_base) == -1 ||
300 109 : asprintf(&log_file, "%s/session.log", log_dir) == -1) {
301 0 : fprintf(stderr, "Fatal: Memory allocation failed.\n");
302 0 : return EXIT_FAILURE;
303 : }
304 :
305 : /* 2. Account + command detection (mirrors main.c logic).
306 : * Supported forms:
307 : * email-cli-ro [<account>] <command> [options]
308 : * email-cli-ro --account <email> <command> [options] */
309 109 : const char *account_arg = NULL;
310 109 : int account_arg_idx = -1;
311 :
312 : /* Pass A: scan for --account flag anywhere in args */
313 454 : for (int i = 1; i < argc; i++) {
314 345 : if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
315 2 : account_arg = argv[++i]; continue;
316 : }
317 : }
318 :
319 : /* Pass B: if no --account flag, check whether first positional arg is an email */
320 109 : if (!account_arg) {
321 140 : for (int i = 1; i < argc; i++) {
322 137 : if (argv[i][0] == '-') {
323 33 : if (strcmp(argv[i], "--account") == 0) i++;
324 33 : continue;
325 : }
326 104 : if (strchr(argv[i], '@')) {
327 24 : account_arg = argv[i];
328 24 : account_arg_idx = i;
329 : }
330 104 : break;
331 : }
332 : }
333 :
334 : /* Command: first non-flag, non-account arg */
335 109 : const char *cmd = NULL;
336 109 : int cmd_idx = 0;
337 184 : for (int i = 1; i < argc; i++) {
338 181 : if (strcmp(argv[i], "--help") == 0) continue;
339 179 : if (strcmp(argv[i], "--account") == 0) { i++; continue; }
340 177 : if (strcmp(argv[i], "--batch") == 0) continue; /* no-op: always batch */
341 130 : if (i == account_arg_idx) continue;
342 106 : cmd = argv[i]; cmd_idx = i; break;
343 : }
344 :
345 : /* --help anywhere: treat as "help <cmd>" */
346 447 : for (int i = 1; i < argc; i++) {
347 347 : if (strcmp(argv[i], "--help") == 0) {
348 9 : if (cmd && strcmp(cmd, "--help") != 0) {
349 7 : if (strcmp(cmd, "list") == 0) { help_list(); return EXIT_SUCCESS; }
350 6 : if (strcmp(cmd, "show") == 0) { help_show(); return EXIT_SUCCESS; }
351 5 : if (strcmp(cmd, "list-folders") == 0) { help_folders(); return EXIT_SUCCESS; }
352 4 : if (strcmp(cmd, "list-attachments") == 0) { help_attachments(); return EXIT_SUCCESS; }
353 3 : if (strcmp(cmd, "save-attachment") == 0) { help_save_attachment(); return EXIT_SUCCESS; }
354 2 : if (strcmp(cmd, "list-labels") == 0) { help_list_labels(); return EXIT_SUCCESS; }
355 1 : if (strcmp(cmd, "list-accounts") == 0) { help_list_accounts(); return EXIT_SUCCESS; }
356 : }
357 2 : help_general();
358 2 : return EXIT_SUCCESS;
359 : }
360 : }
361 :
362 100 : if (cmd && strcmp(cmd, "help") == 0) {
363 9 : const char *topic = NULL;
364 9 : for (int i = cmd_idx + 1; i < argc; i++) { topic = argv[i]; break; }
365 9 : if (topic) {
366 8 : if (strcmp(topic, "list") == 0) { help_list(); return EXIT_SUCCESS; }
367 6 : if (strcmp(topic, "show") == 0) { help_show(); return EXIT_SUCCESS; }
368 5 : if (strcmp(topic, "list-folders") == 0) { help_folders(); return EXIT_SUCCESS; }
369 5 : if (strcmp(topic, "list-attachments") == 0) { help_attachments(); return EXIT_SUCCESS; }
370 4 : if (strcmp(topic, "save-attachment") == 0) { help_save_attachment(); return EXIT_SUCCESS; }
371 3 : if (strcmp(topic, "list-labels") == 0) { help_list_labels(); return EXIT_SUCCESS; }
372 2 : if (strcmp(topic, "list-accounts") == 0) { help_list_accounts(); return EXIT_SUCCESS; }
373 1 : fprintf(stderr, "Unknown command '%s'.\n", topic);
374 1 : fprintf(stderr, "Run 'email-cli-ro help' for available commands.\n");
375 1 : return EXIT_FAILURE;
376 : }
377 1 : help_general();
378 1 : return EXIT_SUCCESS;
379 : }
380 :
381 91 : if (!cmd) {
382 1 : help_general();
383 1 : return EXIT_SUCCESS;
384 : }
385 :
386 : /* 3. Initialize logger */
387 90 : if (fs_mkdir_p(log_dir, 0700) != 0)
388 0 : fprintf(stderr, "Warning: Could not create log directory %s\n", log_dir);
389 90 : if (logger_init(log_file, LOG_DEBUG) != 0)
390 0 : fprintf(stderr, "Warning: Logging system failed to initialize.\n");
391 90 : logger_log(LOG_INFO, "--- email-cli-ro starting (cmd: %s) ---", cmd);
392 :
393 : /* --all-accounts iterates over every configured account, so the usual
394 : * "which account?" resolution below must be skipped for it. */
395 90 : int all_accounts = 0;
396 404 : for (int i = 1; i < argc; i++)
397 314 : if (strcmp(argv[i], "--all-accounts") == 0) all_accounts = 1;
398 :
399 : /* 4. Load configuration — no wizard: must already exist */
400 90 : Config *cfg = NULL;
401 90 : if (strcmp(cmd, "list-accounts") != 0 && !all_accounts) {
402 85 : if (account_arg) {
403 26 : cfg = config_load_account(account_arg);
404 26 : if (!cfg) {
405 1 : fprintf(stderr,
406 : "Error: Account '%s' not found.\n"
407 : "Run 'email-cli-ro list-accounts' to list configured accounts.\n",
408 : account_arg);
409 1 : logger_close();
410 1 : return EXIT_FAILURE;
411 : }
412 : } else {
413 59 : int count = 0;
414 59 : AccountEntry *list = config_list_accounts(&count);
415 59 : if (count == 1) {
416 57 : cfg = list[0].cfg; list[0].cfg = NULL;
417 57 : config_free_account_list(list, count);
418 2 : } else if (count > 1) {
419 2 : fprintf(stderr, "Multiple accounts configured. Specify which to use:\n");
420 6 : for (int i = 0; i < count; i++)
421 8 : fprintf(stderr, " email-cli-ro %s %s\n",
422 4 : list[i].name ? list[i].name : "?", cmd ? cmd : "");
423 2 : fprintf(stderr, "Run 'email-cli-ro list-accounts' for the full list.\n");
424 2 : config_free_account_list(list, count);
425 2 : logger_close();
426 2 : return EXIT_FAILURE;
427 : } else {
428 0 : config_free_account_list(list, count);
429 0 : fprintf(stderr,
430 : "Error: No configuration found.\n"
431 : "Run 'email-cli' once to complete the setup wizard.\n");
432 0 : logger_close();
433 0 : return EXIT_FAILURE;
434 : }
435 : }
436 : }
437 :
438 : /* 5. Initialize local store */
439 87 : if (cfg && local_store_init(cfg->host, cfg->user) != 0)
440 0 : logger_log(LOG_WARN, "Failed to initialize local store for %s", cfg->host);
441 :
442 : /* 6. Dispatch — batch mode only (pager = 0) */
443 87 : int result = -1;
444 :
445 87 : if (strcmp(cmd, "list") == 0) {
446 42 : EmailListOpts opts = {0};
447 42 : opts.limit = BATCH_DEFAULT_LIMIT;
448 42 : int ok = 1;
449 109 : for (int i = cmd_idx + 1; i < argc && ok; i++) {
450 67 : if (strcmp(argv[i], "--batch") == 0) {
451 : /* accepted as no-op: email-cli-ro is always batch mode */
452 63 : } else if (strcmp(argv[i], "--all") == 0) {
453 21 : opts.all = 1;
454 42 : } else if (strcmp(argv[i], "--folder") == 0 ||
455 30 : strcmp(argv[i], "--label") == 0) {
456 13 : if (i + 1 >= argc) {
457 1 : fprintf(stderr, "Error: %s requires a name.\n", argv[i]);
458 1 : ok = 0;
459 : } else {
460 12 : opts.folder = argv[++i];
461 : }
462 29 : } else if (strcmp(argv[i], "--limit") == 0) {
463 9 : if (i + 1 >= argc) {
464 1 : fprintf(stderr, "Error: --limit requires a number.\n");
465 1 : ok = 0;
466 : } else {
467 : char *end;
468 8 : long v = strtol(argv[++i], &end, 10);
469 8 : if (*end != '\0' || v <= 0) {
470 1 : fprintf(stderr, "Error: --limit must be a positive integer.\n");
471 1 : ok = 0;
472 : } else {
473 7 : opts.limit = (int)v;
474 : }
475 : }
476 20 : } else if (strcmp(argv[i], "--all-accounts") == 0) {
477 : /* handled before account resolution; accepted here as a no-op */
478 18 : } else if (strcmp(argv[i], "--json") == 0) {
479 5 : opts.json = 1;
480 13 : } else if (strcmp(argv[i], "--from") == 0) {
481 4 : if (i + 1 >= argc) {
482 0 : fprintf(stderr, "Error: --from requires a substring.\n");
483 0 : ok = 0;
484 : } else {
485 4 : opts.filter_from = argv[++i];
486 : }
487 9 : } else if (strcmp(argv[i], "--since") == 0 ||
488 11 : strcmp(argv[i], "--before") == 0) {
489 5 : int is_since = (strcmp(argv[i], "--since") == 0);
490 5 : const char *name = argv[i];
491 5 : if (i + 1 >= argc) {
492 0 : fprintf(stderr, "Error: %s requires a date (YYYY-MM-DD).\n", name);
493 0 : ok = 0;
494 : } else {
495 5 : const char *v = argv[++i];
496 5 : if (!valid_date_arg(v)) {
497 1 : fprintf(stderr,
498 : "Error: %s must be a date in YYYY-MM-DD form (got '%s').\n",
499 : name, v);
500 1 : ok = 0;
501 4 : } else if (is_since) {
502 2 : opts.filter_since = v;
503 : } else {
504 2 : opts.filter_before = v;
505 : }
506 : }
507 4 : } else if (strcmp(argv[i], "--offset") == 0) {
508 3 : if (i + 1 >= argc) {
509 1 : fprintf(stderr, "Error: --offset requires a number.\n");
510 1 : ok = 0;
511 : } else {
512 : char *end;
513 2 : long v = strtol(argv[++i], &end, 10);
514 2 : if (*end != '\0' || v < 1) {
515 1 : fprintf(stderr, "Error: --offset must be a positive integer.\n");
516 1 : ok = 0;
517 : } else {
518 1 : opts.offset = (int)v;
519 : }
520 : }
521 : } else {
522 1 : unknown_option("list", argv[i]);
523 1 : ok = 0;
524 : }
525 : }
526 42 : if (all_accounts) {
527 : /* Run the same listing for every configured account. JSON mode is
528 : * refused rather than emitting several documents back to back:
529 : * stdout must stay one parseable value. */
530 2 : if (opts.json) {
531 1 : fprintf(stderr,
532 : "Error: --all-accounts cannot be combined with --json yet.\n"
533 : "Run one account at a time, e.g.:\n"
534 : " for a in $(%s list-accounts --batch); do %s \"$a\" list --json; done\n",
535 : "email-cli-ro", "email-cli-ro");
536 1 : result = -1;
537 1 : } else if (!ok) {
538 : /* option error already reported */
539 : } else {
540 1 : int acc_count = 0;
541 1 : AccountEntry *accs = config_list_accounts(&acc_count);
542 1 : if (!accs || acc_count == 0) {
543 0 : fprintf(stderr, "Error: No accounts configured.\n");
544 0 : result = -1;
545 : } else {
546 1 : result = 0;
547 3 : for (int ai = 0; ai < acc_count; ai++) {
548 2 : if (!accs[ai].cfg) continue;
549 2 : printf("=== %s ===\n", accs[ai].name ? accs[ai].name : "?");
550 2 : if (local_store_init(accs[ai].cfg->host, accs[ai].cfg->user) != 0) {
551 0 : fprintf(stderr, "Warning: local store unavailable for %s\n",
552 0 : accs[ai].name ? accs[ai].name : "?");
553 0 : continue;
554 : }
555 2 : EmailListOpts aopts = opts;
556 2 : if (email_service_list(accs[ai].cfg, &aopts) < 0) result = -1;
557 2 : printf("\n");
558 : }
559 : }
560 1 : config_free_account_list(accs, acc_count);
561 : }
562 40 : } else if (ok) {
563 33 : result = email_service_list(cfg, &opts);
564 : }
565 :
566 45 : } else if (strcmp(cmd, "show") == 0) {
567 20 : const char *uid_str = NULL;
568 20 : const char *folder = NULL;
569 20 : int raw_mode = 0;
570 20 : int ok = 1;
571 42 : for (int i = cmd_idx + 1; i < argc && ok; i++) {
572 22 : if (strcmp(argv[i], "--batch") == 0) {
573 1 : continue; /* no-op */
574 21 : } else if (strcmp(argv[i], "--raw") == 0) {
575 0 : raw_mode = 1;
576 21 : } else if (strcmp(argv[i], "--folder") == 0 ||
577 19 : strcmp(argv[i], "--label") == 0) {
578 2 : if (i + 1 >= argc) {
579 0 : fprintf(stderr, "Error: %s requires a name.\n", argv[i]);
580 0 : ok = 0;
581 : } else {
582 2 : folder = argv[++i];
583 : }
584 19 : } else if (!uid_str) {
585 19 : uid_str = argv[i];
586 : } else {
587 0 : unknown_option("show", argv[i]);
588 0 : ok = 0;
589 : }
590 : }
591 20 : if (!ok) {
592 : /* error already printed above */
593 20 : } else if (!uid_str) {
594 1 : fprintf(stderr, "Error: 'show' requires a UID argument.\n");
595 1 : help_show();
596 : } else {
597 : char uid[17];
598 19 : if (parse_uid(uid_str, uid) != 0)
599 1 : fprintf(stderr,
600 : "Error: UID must be a positive integer (got '%s').\n",
601 : uid_str);
602 : else
603 18 : result = raw_mode
604 0 : ? email_service_read_raw(cfg, folder, uid)
605 18 : : email_service_read(cfg, folder, uid, 0, BATCH_DEFAULT_LIMIT);
606 : }
607 :
608 25 : } else if (strcmp(cmd, "list-folders") == 0) {
609 4 : int tree = 0, ok = 1;
610 8 : for (int i = cmd_idx + 1; i < argc && ok; i++) {
611 4 : if (strcmp(argv[i], "--batch") == 0) { /* no-op */
612 2 : } else if (strcmp(argv[i], "--tree") == 0)
613 1 : tree = 1;
614 1 : else { unknown_option("list-folders", argv[i]); ok = 0; }
615 : }
616 4 : if (ok) result = email_service_list_folders(cfg, tree);
617 :
618 21 : } else if (strcmp(cmd, "list-attachments") == 0) {
619 7 : const char *uid_str = NULL;
620 7 : for (int i = cmd_idx + 1; i < argc; i++) {
621 6 : if (strcmp(argv[i], "--batch") == 0) continue;
622 6 : uid_str = argv[i]; break;
623 : }
624 7 : if (!uid_str) {
625 1 : fprintf(stderr, "Error: 'list-attachments' requires a UID argument.\n");
626 1 : help_attachments();
627 : } else {
628 : char uid[17];
629 6 : if (parse_uid(uid_str, uid) != 0)
630 1 : fprintf(stderr,
631 : "Error: UID must be a positive integer (got '%s').\n",
632 : uid_str);
633 : else
634 5 : result = email_service_list_attachments(cfg, uid);
635 : }
636 :
637 14 : } else if (strcmp(cmd, "save-attachment") == 0) {
638 6 : const char *uid_str = NULL;
639 6 : const char *filename = NULL;
640 6 : const char *outdir = NULL;
641 6 : int argn = 0;
642 20 : for (int i = cmd_idx + 1; i < argc; i++) {
643 14 : if (strcmp(argv[i], "--batch") == 0) continue;
644 14 : if (argn == 0) { uid_str = argv[i]; argn++; }
645 9 : else if (argn == 1) { filename = argv[i]; argn++; }
646 4 : else if (argn == 2) { outdir = argv[i]; argn++; }
647 : }
648 6 : if (!uid_str || !filename) {
649 1 : fprintf(stderr,
650 : "Error: 'save-attachment' requires a UID and a filename.\n");
651 1 : help_save_attachment();
652 : } else {
653 : char uid[17];
654 5 : if (parse_uid(uid_str, uid) != 0)
655 1 : fprintf(stderr,
656 : "Error: UID must be a positive integer (got '%s').\n",
657 : uid_str);
658 : else
659 4 : result = email_service_save_attachment(cfg, uid, filename, outdir);
660 : }
661 :
662 8 : } else if (strcmp(cmd, "list-labels") == 0) {
663 3 : result = email_service_list_labels(cfg);
664 :
665 5 : } else if (strcmp(cmd, "list-accounts") == 0) {
666 3 : int count = 0;
667 3 : AccountEntry *accs = config_list_accounts(&count);
668 3 : if (count == 0) {
669 1 : printf("No accounts configured.\n");
670 1 : result = 0;
671 : } else {
672 2 : printf("%-40s %-8s %s\n", "Account", "Type", "Server");
673 2 : printf("%-40s %-8s %s\n",
674 : "----------------------------------------",
675 : "--------",
676 : "----------------------------");
677 5 : for (int i = 0; i < count; i++) {
678 3 : const char *type = (accs[i].cfg && accs[i].cfg->gmail_mode) ? "Gmail" : "IMAP";
679 3 : const char *server = accs[i].cfg ? (accs[i].cfg->host ? accs[i].cfg->host : "-") : "-";
680 3 : printf("%-40s %-8s %s\n",
681 3 : accs[i].name ? accs[i].name : "?",
682 : type, server);
683 : }
684 2 : config_free_account_list(accs, count);
685 2 : result = 0;
686 : }
687 :
688 : } else {
689 : /* Check if the command is a write-only command blocked in ro mode */
690 : static const char *ro_blocked[] = {
691 : "mark-read", "mark-unread", "mark-starred", "remove-starred",
692 : "add-label", "remove-label", "create-label", "delete-label",
693 : "create-folder", "delete-folder",
694 : "mark-junk", "mark-notjunk",
695 : "add-account", "remove-account", NULL
696 : };
697 2 : int blocked = 0;
698 16 : for (int i = 0; ro_blocked[i]; i++) {
699 15 : if (strcmp(cmd, ro_blocked[i]) == 0) {
700 1 : fprintf(stderr, "Error: '%s' is not available in read-only mode (email-cli-ro).\n", cmd);
701 1 : fprintf(stderr, "Use 'email-cli' for write operations.\n");
702 1 : config_free(cfg);
703 1 : logger_log(LOG_INFO, "--- email-cli-ro session finished ---");
704 1 : logger_close();
705 1 : return EXIT_FAILURE;
706 : }
707 : }
708 1 : if (!blocked) {
709 1 : fprintf(stderr, "Unknown command '%s'.\n", cmd);
710 1 : fprintf(stderr, "Run 'email-cli-ro help' for available commands.\n");
711 : }
712 : }
713 :
714 : /* 7. Cleanup */
715 86 : config_free(cfg);
716 86 : logger_log(LOG_INFO, "--- email-cli-ro session finished ---");
717 86 : logger_close();
718 :
719 86 : if (result >= 0)
720 66 : return EXIT_SUCCESS;
721 20 : fprintf(stderr, "\nFailed. Check logs in %s\n", log_file);
722 20 : return EXIT_FAILURE;
723 : }
|