LCOV - code coverage report
Current view: top level - src - main.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 84.0 % 912 766
Test Date: 2026-08-21 10:11:28 Functions: 96.7 % 30 29

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

Generated by: LCOV version 2.0-1