Fixes from testing email search (docs/enhancements-2026-07-07.md): - New `folders` agent command lists the account's mailboxes (name, delimiter, selectable), INBOX first, so agents can discover archived mail outside INBOX. - `search --all-folders` sweeps every selectable mailbox; each hit carries a `folder` field, `skipped_folders` reports mailboxes the server refused, and --limit caps visible results across the sweep. The sweep deliberately skips EnsureFolderBaseline so a read-only search never mutates list --new state. - Empty search results include a generic `data.hint` with next steps. The hint is a fixed constant per mode, so the invisibility invariant holds: absent and policy-filtered mail produce byte-identical envelopes (codified in TestSearchEmptyHintIndistinguishableFromFiltered). - Skill and user docs: document `--text` full-text search as best-effort (server-dependent); recommend --subject-contains/--from. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.4 KiB
emcli agent command reference
The six agent commands you may use. Each prints one JSON object to stdout and sets a matching
exit code (0 success, non-zero error). All take --account <name>; most take --folder (default
INBOX).
The JSON envelope
{ "error": false, "error_detail": {}, "data": { } }
error— boolean. Check it first.error_detail—{}on success;{ "code": "...", "message": "..." }on failure.data— command-specific payload (below).
Error codes: config (key/config), db, network, auth, policy (blocked by a rule),
not_found (missing or filtered/invisible mail), usage (bad/missing flag).
list — headers, newest first
emcli list --account A [--folder F] [--new] [--limit N] [--before U] [--since U]
| Flag | Default | Meaning |
|---|---|---|
--folder |
INBOX |
Mailbox |
--new |
off | Only messages not yet acked |
--limit |
50 |
Max results (capped at 500) |
--before <uid> |
— | Only UIDs lower than this (page to older mail) |
--since <uid> |
— | Only UIDs higher than this (page to newer mail) |
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 } ] }
Headers only — no body is downloaded. data.messages is [] when nothing matches.
get — one full message
emcli get --account A [--folder F] --uid U
data:
{ "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",
"body_text": "the decoded plain-text body…",
"attachments": [
{ "name": "report.pdf", "size": 20480, "mime": "application/pdf", "content_b64": "JVBERi0…" } ] }
body_textis the decoded plain-text part.- Each attachment's bytes are base64 in
content_b64; decode to recover the file (echo "$b64" | base64 -d > report.pdf).sizeis the decoded byte length. getdoes not acknowledge the message.- A filtered/invisible or missing UID returns
error: true, codenot_found.
folders — list mailboxes
emcli folders --account A
data:
{ "folders": [
{ "name": "INBOX", "delimiter": ".", "selectable": true },
{ "name": "INBOX.Archive", "delimiter": ".", "selectable": true } ] }
- INBOX first, then case-insensitive name order.
selectable: falsemarks hierarchy-only entries — don't pass those to--folder.- Use this before searching outside INBOX: archived mail often lives in folders like
INBOX.Archivethatlist/searchwon't touch by default.
search — server-side search (one folder, or all)
emcli search --account A [--folder F | --all-folders] [--from X] [--subject-contains X] [--text X] \
[--since-date D] [--before-date D] [--limit N]
| Flag | Meaning |
|---|---|
--from |
Sender contains |
--subject-contains |
Subject contains |
--text |
Full-text (best-effort — see below) |
--all-folders |
Sweep every selectable mailbox instead of one folder |
--since-date / --before-date |
RFC 3339 bounds, e.g. 2026-06-01T00:00:00Z |
--limit |
Max results (default 50) |
data shape is identical to list ({ "messages": [ … ] }). Searches the whole folder regardless
of new/acked state. Filtered mail never appears.
--text is unreliable on some servers. IMAP full-text search depends on the server having a
body index; some servers (observed in the wild) return zero results for mail that
--subject-contains finds fine. Prefer --subject-contains and --from as your primary search
methods and treat --text as best-effort — a --text miss does not mean the mail is absent.
--all-folders sweeps every selectable mailbox (mutually exclusive with --folder):
- Each message in the result gains a
"folder"field. UIDs are only unique per folder, so pass that folder back via--folderwhen youget/acka hit. data.skipped_folderslists mailboxes the server refused to search (always present,[]when none) — transport errors, not policy.--limitcaps total visible results across the sweep.- On Gmail-style servers the same message can appear under several folders with different UIDs;
dedupe by
message_idif that matters.
Empty results include a hint. When messages is [], data.hint suggests next steps
(run folders, retry with --all-folders, or adjust criteria). The hint text is fixed per mode
and identical whether matching mail is absent, in another folder, or hidden by policy — it tells
you what to try, not why the result is empty.
ack — mark message(s) processed
emcli ack --account A [--folder F] --uid-list U1,U2,U3
data:
{ "acked": [70314, 70315, 70320] }
- The only command that changes state. Call it after you've actually handled a message.
- Idempotent and order-independent; batch multiple UIDs comma-separated.
- After ack, those UIDs no longer appear under
list --new. - You cannot ack a message you aren't allowed to see — returns
not_found.
send — send or reply (RW accounts only)
emcli send --account A --to X [--cc X] [--bcc X] --subject S --body B \
[--attach P]… [--reply-to U [--folder F]]
| Flag | Meaning |
|---|---|
--to / --cc / --bcc |
Recipients — repeat the flag or comma-separate (--to a@x,b@x) |
--subject |
Subject |
--body |
Plain-text body |
--attach |
File path to attach (repeatable) |
--reply-to <uid> |
Thread the reply onto this source message |
--folder |
Folder of the --reply-to source (default INBOX) |
data:
{ "sent": true, "recipients": ["alice@example.com", "boss@example.com"] }
Blocked sends return error: true, code policy:
ro_mode— the account is read-only; it cannot send.whitelist_out— a recipient isn't on the outbound whitelist; the whole send is blocked and nothing was sent. Don't silently drop recipients — tell the user.
--reply-to reads the source message's Message-ID/References so the reply threads. The source
is subject to the inbound filter: a filtered/missing source returns not_found.
Enforcement rules (set by the user; you can't change them)
- Mode:
ROaccounts rejectsend.RWcan read and send. - Inbound whitelist / subject filter: disallowed mail is invisible everywhere (
list/searchomit it;get/ackreturnnot_found). You can't tell a filtered message from a non-existent one — by design. The empty-searchhintis deliberately generic for the same reason: it never reveals whether anything was filtered. - Outbound whitelist: every recipient (to+cc+bcc) must match, or the send is blocked whole.
- Address matching: case-insensitive; an entry
@domain.commatches any address at that domain; otherwise an exact-address match.
Parsing tips
# Guard on success, then read data:
out=$(emcli list --account gmail --new --limit 10)
if echo "$out" | jq -e '.error == false' >/dev/null; then
echo "$out" | jq -r '.data.messages[] | "\(.uid)\t\(.subject)"'
else
echo "$out" | jq -r '.error_detail | "\(.code): \(.message)"' >&2
fi
# Save an attachment from get:
emcli get --account gmail --uid 70314 \
| jq -r '.data.attachments[0].content_b64' | base64 -d > report.pdf