skills/emcli/ — an Agent Skill teaching an agent to read and send mail through
emcli's JSON agent commands:
- SKILL.md: name/description (what + when + trigger keywords), compatibility,
metadata; body covers the security model (agent-only commands, never touch
EMCLI_KEY), setup, the list→get→ack workflow, sending, and enforcement
awareness. Frontmatter validated against the spec (name matches dir; desc
574/1024; compatibility 239/500); body 146 lines (<500).
- scripts/install.sh: detects OS/arch, downloads the release binary, verifies
the sha256 checksum when present, fails gracefully. Release tag/assets
(v0.4.0, emcli_<ver>_<os>_<arch>) are placeholders until the first release.
- references/{commands.md,install.md}: full agent command reference (flags, JSON
shapes, error codes, enforcement) and install options, loaded on demand.
README links to the skill.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.4 KiB
emcli agent command reference
The five 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.
search — server-side search (whole folder)
emcli search --account A [--folder F] [--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 |
--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.
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. - 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