Files
emcli/USER-MANUAL.md
2026-06-23 21:39:12 +01:00

24 KiB

emcli — User Manual

emcli is a single command-line program that mediates all email access for an AI agent. The agent never holds your email password and never talks to the mail server directly — every read and send goes through emcli, which enforces the limits you configure. Even if the agent is given faulty instructions, it cannot read mail it isn't permitted to see or send mail to people it isn't permitted to contact.

This manual is for using and administering emcli. It assumes you have the emcli binary.


Contents

  1. Key concepts
  2. Setup: encryption keys and database
  3. Quick start
  4. Adding accounts
  5. Administering accounts
  6. Whitelists, modes, and filters
  7. Agent commands (reading and sending)
  8. The JSON envelope
  9. Diagnostics: doctor
  10. Audit log and settings
  11. Troubleshooting
  12. Command cheat sheet

1. Key concepts

Two kinds of commands.

  • Admin commands (init, account add/edit/remove, whitelist, config, audit) require EMCLI_ADMIN_KEY and are for you, the human. They print human-readable text or open an interactive form. (account list is the one exception — it is also an agent command; see below.)
  • Agent commands (list, get, search, ack, send, doctor) require EMCLI_KEY (or EMCLI_ADMIN_KEY as a superset) and are for the agent. They print one line of JSON and nothing else, so a program can consume them reliably. (doctor prints human-readable text but is authorised by the agent key — EMCLI_KEY alone is sufficient; EMCLI_ADMIN_KEY also works as a superset, so either key suffices for agent commands.)

Accounts are named (e.g. gmail, work). The agent refers to an account by name and never sees its password.

Read-only vs read-write (mode).

  • RO — the agent can read this account but cannot send. Any send is rejected.
  • RW — the agent can read and send.

Whitelists (optional, per account):

  • Inbound (whitelist in) — if enabled, the agent only sees mail from listed senders. Everything else is invisible: it won't appear in list/search, can't be opened with get, and can't be acknowledged.
  • Outbound (whitelist out) — if enabled, every recipient of a send must be listed, or the whole send is blocked.

Subject filter (optional, per account) — a regular expression; if set, the agent only sees mail whose subject matches.

"New" mail and acknowledging. emcli tracks which messages an account has already processed. list --new shows only messages not yet acknowledged. The agent calls ack to mark messages done; they then drop out of --new. Reading a message (get) does not acknowledge it — acking is a deliberate, separate step.


2. Setup: encryption keys and database

emcli reads three environment variables:

Variable Purpose Default
EMCLI_ADMIN_KEY Required for admin. Base64-encoded 32-byte key (AES-256). Authorises ALL commands. none — admin commands fail without it
EMCLI_KEY Required for agents. Base64-encoded 32-byte key (AES-256). Authorises agent commands only. none — agent commands fail without it
EMCLI_DB Path to the database file. ~/.config/emcli/emcli.db (Linux/macOS), %AppData%\emcli\emcli.db (Windows)

Generate both keys once and keep them safe:

export EMCLI_ADMIN_KEY="$(head -c 32 /dev/urandom | base64)"   # you (human) keep this
export EMCLI_KEY="$(head -c 32 /dev/urandom | base64)"         # the agent launcher gets ONLY this

Important: the keys protect your account passwords via envelope encryption (see "Privilege model" below). If you lose EMCLI_ADMIN_KEY, account secrets can't be decrypted and you'll have to re-add accounts. emcli never falls back to plaintext — a missing or wrong key makes every command fail safely.

Account passwords are stored encrypted; they never appear in command output, error messages, or the audit log.


2a. Privilege model

emcli enforces a two-role privilege split so a process holding only the agent key cannot reconfigure accounts, whitelists, or audit settings.

The two keys

Key Holder Authorises
EMCLI_ADMIN_KEY Human / secrets manager ALL commands (account, whitelist, config, audit, init, plus all agent commands)
EMCLI_KEY Agent orchestrator Agent commands only (list, get, search, ack, send, doctor)

EMCLI_ADMIN_KEY is a strict superset: a process with only the admin key can run agent commands too. A process with only EMCLI_KEY is refused with emcli: this command requires EMCLI_ADMIN_KEY (admin privilege) if it attempts an admin command.

Envelope encryption (DEK)

emcli init generates a random data-encryption key (DEK) that seals all account secrets. The DEK is stored in the settings table in two sealed copies:

  • dek_wrap_admin — the DEK encrypted under EMCLI_ADMIN_KEY.
  • dek_wrap_agent — the DEK encrypted under EMCLI_KEY.

The DEK is never written in cleartext. Admin commands unwrap the DEK from the admin slot only; they have no fallback to the agent slot. This means a process holding only EMCLI_KEY cannot unlock the DEK for an admin command, even if it somehow knows the agent key.

Command → role table

Command Role required
list, get, search, ack, send, doctor, account list Agent (EMCLI_KEY or EMCLI_ADMIN_KEY)
account add/edit/remove, whitelist, config, audit Admin (EMCLI_ADMIN_KEY required)

account list is dual-role: with the admin key it prints the full NAME MODE IMAP USER table; with only EMCLI_KEY (an agent) it prints a JSON envelope exposing just name, from, and can_send — no host or login username. | init | Both keys required (writes both wrap slots) |

Agent launcher guidance

Configure your agent's orchestrator with only EMCLI_KEY. Never give the orchestrator EMCLI_ADMIN_KEY. If the agent tries to run an admin command — even by mistake — emcli will refuse it at the key level, not just by convention.


3. Quick start

# 1. Generate and export both keys (see section 2)
export EMCLI_ADMIN_KEY="$(head -c 32 /dev/urandom | base64)"   # keep this yourself
export EMCLI_KEY="$(head -c 32 /dev/urandom | base64)"         # give only this to the agent

# 2. Create the database and add your first account (interactive form)
emcli init

# 3. Check it connects and authenticates (agent key is enough for doctor)
emcli doctor

# 4. The agent can now read (needs only EMCLI_KEY)
emcli list --account gmail --folder INBOX --limit 10

emcli init opens an interactive form. Use Tab/Shift+Tab to move between fields, Enter to save, Esc to cancel. Boolean fields take y/n.

Prefer flags? You can do everything non-interactively too — see the next section.


4. Adding accounts

Interactive (recommended for first time):

emcli account add          # opens the form

Or with flags (good for scripting):

emcli account add --name work --mode RW \
  --imap-host imap.example.com --imap-port 993 --imap-security tls \
  --smtp-host smtp.example.com --smtp-port 465 --smtp-security tls \
  --username you@example.com --password 'your-password'

account add flags:

Flag Default Notes
--name Account name the agent will use (required)
--mode RO RO (read-only) or RW (read + send)
--imap-host IMAP server (required)
--imap-port 993
--imap-security tls tls or starttls
--smtp-host SMTP server (used for RW accounts)
--smtp-port 465
--smtp-security tls tls or starttls
--username Login username, usually your full email (required)
--password Login password or app password
--from Send-as address (blank = use username); bare or "Display Name <addr>"
--subject-regex Inbound subject filter (optional)
--whitelist-in off Enable inbound whitelist
--whitelist-out off Enable outbound whitelist
--process-backlog off Treat existing mail as "new" (see below)

--process-backlog. When emcli first sees a folder:

  • off (default): existing mail is treated as already handled — list --new starts empty and only mail that arrives after this point counts as new.
  • on: all existing mail in the folder is treated as new for the agent to process.

Gmail (app password)

Gmail needs an app password, not your normal Google password.

  1. Turn on 2-Step Verification on your Google account (required — the app-passwords page is hidden otherwise).
  2. Create a 16-character app password at https://myaccount.google.com/apppasswords (name it, e.g., "emcli").
  3. Enable IMAP in Gmail: Settings → See all settings → Forwarding and POP/IMAP → Enable IMAP.
  4. Add the account (paste the app password; the spaces Google shows are optional):
emcli account add --name gmail --mode RW \
  --imap-host imap.gmail.com --imap-port 993 --imap-security tls \
  --smtp-host smtp.gmail.com --smtp-port 465 --smtp-security tls \
  --username you@gmail.com --password 'xxxxxxxxxxxxxxxx'

An app password keeps working until you revoke it, change your main Google password, or turn off 2-Step Verification. If that happens, generate a new one and update the account (section 5).

Other providers

Most IMAP/SMTP providers work the same way. A typical cPanel-style host:

emcli account add --name work --mode RW \
  --imap-host mail.yourdomain.com --imap-port 993 --imap-security tls \
  --smtp-host mail.yourdomain.com --smtp-port 465 --smtp-security tls \
  --username you@yourdomain.com --password 'your-password'

If 465/tls doesn't connect for SMTP, try 587/starttls. Use emcli doctor (section 9) to confirm.


5. Administering accounts

List accounts (never shows secrets):

emcli account list

Edit an account — interactive (opens the form pre-filled):

emcli account edit --name gmail

Edit with flags — only the flags you pass are changed; everything else is preserved. Leaving --password out keeps the existing password.

emcli account edit --name work --mode RW --smtp-host smtp.example.com --smtp-port 587 --smtp-security starttls
emcli account edit --name gmail --password 'new-app-password'   # rotate the app password
emcli account edit --name work --from 'Work Team <you@yourdomain.com>'   # set the send-as address
emcli account edit --name work --from ''                                 # clear it (revert to username)

Note: the flag form of account edit covers connection/auth fields, --from, and --subject-regex. Passing --from '' clears the send-as address so mail falls back to the login username. To toggle whitelists or process-backlog, use the interactive form (emcli account edit --name X with no other flags), or the whitelist commands in section 6.

Remove an account (requires --yes):

emcli account remove --name work --yes

6. Whitelists, modes, and filters

Modes

Set with --mode RO|RW on account add/edit. RO accounts reject every send.

Inbound whitelist

Enable it on the account (--whitelist-in, or the interactive form), then manage entries:

emcli whitelist in add    --account gmail --address boss@example.com
emcli whitelist in add    --account gmail --address @partner.com
emcli whitelist in list   --account gmail
emcli whitelist in remove --account gmail --address boss@example.com

When enabled, the agent only sees mail from listed senders. Everything else is invisible.

Outbound whitelist

Enable it (--whitelist-out), then manage entries the same way with whitelist out:

emcli whitelist out add  --account gmail --address @example.com
emcli whitelist out list --account gmail

When enabled, every recipient of a send (to + cc + bcc) must match an entry or the whole send is blocked.

Address matching (both directions)

  • Case-insensitive.
  • An entry like @example.com matches any address at that domain.
  • Any other entry matches that exact address.

Subject filter

A regular expression on the account. If set, the agent only sees mail whose subject matches:

emcli account edit --name gmail --subject-regex '^\[ticket\]'

Clear it by setting it empty in the interactive form.


7. Agent commands

These are what the agent runs. Each prints exactly one JSON object (see section 8). They all take --account and most take --folder (default INBOX).

Reading never changes state. list, get, and search are read-only. Only ack advances "what's been processed."

list — message headers

emcli list --account gmail --folder INBOX --limit 20
emcli list --account gmail --new                 # only un-acked messages
emcli list --account gmail --before 1000         # older than UID 1000 (paging)
emcli list --account gmail --since 1000          # newer than UID 1000
Flag Default Meaning
--folder INBOX Mailbox/folder
--new off Only messages not yet acknowledged
--limit 50 Max results (capped at 500)
--before <uid> Only messages with a lower UID (page to older mail)
--since <uid> Only messages with a higher UID (page to newer mail)

Returns headers only (no body): uid, from, to, subject, date, message_id, has_attachments. Newest first.

get — one full message

emcli get --account gmail --folder INBOX --uid 70314

Returns the full message: headers, decoded plain-text body, and attachments (each as name, size, mime, and content_b64 — base64-encoded contents). Does not acknowledge it.

search — find mail server-side

emcli search --account gmail --from boss@example.com
emcli search --account gmail --subject-contains invoice
emcli search --account gmail --text "quarterly report"
emcli search --account gmail --since-date 2026-01-01T00:00:00Z --before-date 2026-02-01T00:00:00Z
Flag Meaning
--from Sender contains
--subject-contains Subject contains
--text Full-text search
--since-date / --before-date Date bounds, RFC 3339 (e.g. 2026-06-01T00:00:00Z)
--limit Max results (default 50)

Returns the same headers-only shape as list. Searches the whole folder, regardless of new/acked state. Filtered (whitelisted-out) mail never appears.

ack — mark messages processed

emcli ack --account gmail --folder INBOX --uid-list 70314,70315,70320

Marks one or more UIDs as processed; they stop appearing under list --new. Safe to call more than once, and the order doesn't matter. You can't ack a message you aren't allowed to see.

send — send a message (RW accounts only)

emcli send --account gmail \
  --to alice@example.com --cc bob@example.com \
  --subject "Hello" --body "Plain-text body here."

# multiple recipients (repeat the flag or comma-separate)
emcli send --account gmail --to a@x.com --to b@x.com --subject Hi --body Yo
emcli send --account gmail --to 'a@x.com,b@x.com' --subject Hi --body Yo

# attachments
emcli send --account gmail --to a@x.com --subject Report --body "see attached" \
  --attach ./report.pdf --attach ./data.csv

# reply (threads correctly off an existing message you can see)
emcli send --account gmail --to a@x.com --subject "Re: Hi" --body "thanks" \
  --reply-to 70314 --folder INBOX
Flag Meaning
--to / --cc / --bcc Recipients (repeatable, or comma-separated)
--subject Subject
--body Plain-text body
--attach File to attach (repeatable)
--reply-to <uid> Thread the reply onto this source message
--folder Folder of the --reply-to source (default INBOX)

--reply-to reads the source message's identifiers so the reply threads in the recipient's mail client. The source is subject to the inbound whitelist — you can't reply to mail you aren't allowed to see.

The message's From: is the account's send-as address (--from, set on account add/edit); if none is configured it falls back to the login username. A display-name address like Steve Cliff <me@example.com> shows the name in the recipient's client while the bare address is used for the SMTP envelope.


8. The JSON envelope

Every agent command prints exactly one JSON object:

{ "error": false, "error_detail": {}, "data": { } }
  • errortrue or false.
  • error_detail — empty {} on success; on failure { "code": "...", "message": "..." }.
  • data — the result; shape depends on the command.

The process exit code mirrors error (0 on success, non-zero on failure), so scripts can check either.

Examples.

list / search success:

{ "error": false, "error_detail": {},
  "data": { "messages": [
    { "uid": 70314, "from": "\"Boss\" <boss@example.com>", "to": "<you@example.com>",
      "subject": "Hello", "date": "Mon, 22 Jun 2026 17:00:30 +0000",
      "message_id": "abc@example.com", "has_attachments": false } ] } }

send success:

{ "error": false, "error_detail": {}, "data": { "sent": true, "recipients": ["alice@example.com"] } }

A blocked send (read-only account):

{ "error": true, "error_detail": { "code": "policy", "message": "send blocked: ro_mode" }, "data": {} }

Error codes you may see in error_detail.code:

Code Meaning
config Missing/invalid EMCLI_KEY or configuration problem
db Database error
network Connection problem reaching the mail server
auth Authentication failed
policy Blocked by a rule (ro_mode, whitelist_out)
not_found Message/account not found — also returned for filtered (invisible) mail
usage A required flag was missing or invalid

A message hidden by the inbound whitelist or subject filter returns not_found — the same as a message that doesn't exist. This is deliberate: the agent can't tell the difference, so it can't learn that filtered mail exists.


9. Diagnostics: doctor

doctor checks that each account can actually connect and authenticate — it logs in and out but sends and reads nothing.

emcli doctor                    # check every account
emcli doctor --account gmail    # check just one

Example output:

gmail (RW)
  IMAP  ok
  SMTP  ok
work (RO)
  IMAP  ok
  SMTP  n/a (read-only)
badpass (RO)
  IMAP  FAIL: Invalid credentials
  SMTP  n/a (read-only)

doctor exits non-zero if any check fails. SMTP is only checked for RW accounts with an SMTP host configured. Run doctor after adding or editing an account to confirm the credentials and server settings are right.


10. Audit log and settings

Audit log

Every agent action (list, get, search, ack, send) — allowed or blocked — is recorded.

emcli audit list                       # most recent 50
emcli audit list --account gmail        # filter to one account
emcli audit list --limit 200

Each row shows the time, account, action, result (allowed/blocked), target, and (for blocks) the reason. Secrets never appear here.

Settings

emcli config set audit_retention_days 90
emcli config get audit_retention_days
  • audit_retention_days — how long to keep audit rows. On each run, entries older than this are purged. Must be a whole number ≥ 0. 0 or unset means no automatic purging.

11. Troubleshooting

"EMCLI_KEY is not set" / "must be base64 of exactly 32 bytes". Set EMCLI_KEY to a valid base64-encoded 32-byte key (section 2). Agent commands (list, get, search, ack, send, doctor) need this key.

"this command requires EMCLI_ADMIN_KEY (admin privilege)". Set EMCLI_ADMIN_KEY (section 2). Admin commands (account add/edit/remove, whitelist, config, audit, init) need this key; EMCLI_KEY alone is not enough for them. (account list is the exception — an agent can run it.)

A command fails to decrypt / wrong key. The key doesn't match the one used when the database was initialised. Restore the original key, or re-run emcli init (idempotent — it won't regenerate the DEK if one already exists) with both correct keys, then re-add any accounts if needed.

doctor shows IMAP FAIL / SMTP FAIL.

  • Invalid credentials / authentication failed — wrong username or password. For Gmail, make sure you're using an app password (section 4) and that IMAP is enabled in Gmail settings.
  • Connection errors — wrong host/port/security. Try the provider's documented settings; for SMTP, 587/starttls is a common alternative to 465/tls.

The agent can't see a message you know exists. It's probably filtered: check the account's inbound whitelist (emcli whitelist in list --account NAME) and subject filter. Filtered mail is invisible by design.

send is blocked.

  • ro_mode — the account is read-only. Change it: emcli account edit --name NAME --mode RW (and set SMTP details).
  • whitelist_out — a recipient isn't on the outbound whitelist. Add it, or review the rule.

list --new is empty but there's mail. By default, mail that existed before you added the account is treated as already handled. Add the account with --process-backlog to treat existing mail as new, or just use list/search without --new.

The interactive form won't open ("could not open a new TTY"). Interactive commands (init, account add/edit without flags) need a real terminal. Use the flag-based forms instead when running non-interactively.


12. Command cheat sheet

# Help
emcli                  # or: emcli help / emcli --help — list all commands
emcli <command> --help # usage and flags for one command

# Admin (requires EMCLI_ADMIN_KEY)
emcli init                                            # create DB + add first account (form)
emcli account add [flags | none for form]            # add an account
emcli account list                                    # full table (admin) / name+from+can_send JSON (agent)
emcli account edit --name N [flags | none for form]  # change an account
emcli account remove --name N --yes                   # delete an account
emcli whitelist in|out add|remove|list --account N [--address A]
emcli config set|get <key> [value]                    # e.g. audit_retention_days
emcli audit list [--account N] [--limit K]
emcli version

# Agent (requires EMCLI_KEY or EMCLI_ADMIN_KEY; one line of JSON each)
emcli doctor [--account N]                             # connectivity/auth check
emcli list   --account N [--folder F] [--new] [--limit K] [--before U] [--since U]
emcli get    --account N [--folder F] --uid U
emcli search --account N [--folder F] [--from A] [--subject-contains S] [--text S] [--since-date D] [--before-date D] [--limit K]
emcli ack    --account N [--folder F] --uid-list U1,U2,U3
emcli send   --account N --to A [--cc A] [--bcc A] --subject S --body B [--attach P]… [--reply-to U [--folder F]]

Environment: EMCLI_ADMIN_KEY (required for admin commands, base64 32-byte key), EMCLI_KEY (required for agent commands, base64 32-byte key), EMCLI_DB (optional DB path).