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

Generated by: LCOV version 2.0-1