LCOV - code coverage report
Current view: top level - src - main_ro.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 92.2 % 333 307
Test Date: 2026-08-21 10:11:28 Functions: 100.0 % 12 12

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

Generated by: LCOV version 2.0-1