feat(cli): folders command, search --all-folders, empty-search hint
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>
This commit is contained in:
@@ -68,7 +68,7 @@ CGO_ENABLED=0 go build -o emcli ./cmd/emcli # then move ./emcli onto your PATH
|
||||
## 3. Confirm the agent key is present
|
||||
|
||||
emcli uses two keys; **you (the agent) are given only `EMCLI_KEY`** (the agent key). It authorises
|
||||
`list`, `get`, `search`, `ack`, `send`, and `doctor`. Admin commands require `EMCLI_ADMIN_KEY`,
|
||||
`list`, `get`, `search`, `folders`, `ack`, `send`, and `doctor`. Admin commands require `EMCLI_ADMIN_KEY`,
|
||||
which the human holds — attempting admin commands with only `EMCLI_KEY` is refused by `emcli`.
|
||||
|
||||
For agent use, **the orchestrator that launched you provides `EMCLI_KEY`** in the environment.
|
||||
|
||||
+17
-3
@@ -17,8 +17,8 @@ sets its exit code to match.
|
||||
|
||||
## Security model — read this first
|
||||
|
||||
- **You only run agent commands:** `list`, `get`, `search`, `ack`, `send`, `doctor`, and
|
||||
`account list` (to discover accounts). You are provided only `EMCLI_KEY` (the agent key), which
|
||||
- **You only run agent commands:** `list`, `get`, `search`, `folders`, `ack`, `send`, `doctor`,
|
||||
and `account list` (to discover accounts). You are provided only `EMCLI_KEY` (the agent key), which
|
||||
authorises these and nothing else. Account *setup* (`account add/edit/remove`), passwords,
|
||||
whitelists, and config are the **user's** job (admin commands that require `EMCLI_ADMIN_KEY`) —
|
||||
do not run or suggest running `account add/edit/remove`, `whitelist`, `config`, `audit`, or
|
||||
@@ -117,7 +117,8 @@ read-only — tell the user; do not attempt another account without their say-so
|
||||
|---|---|
|
||||
| `emcli list --account A [--folder F] [--new] [--limit N] [--before U] [--since U]` | Message headers, newest first |
|
||||
| `emcli get --account A [--folder F] --uid U` | One full message (body + attachments) |
|
||||
| `emcli search --account A [--folder F] [--from X] [--subject-contains X] [--text X] [--since-date D] [--before-date D]` | Server-side search |
|
||||
| `emcli search --account A [--folder F \| --all-folders] [--from X] [--subject-contains X] [--text X] [--since-date D] [--before-date D]` | Server-side search |
|
||||
| `emcli folders --account A` | List the account's mailboxes/folders |
|
||||
| `emcli ack --account A [--folder F] --uid-list U1,U2` | Mark message(s) processed |
|
||||
| `emcli send --account A --to X [--cc X] [--bcc X] --subject S --body B [--attach P]… [--reply-to U]` | Send / reply |
|
||||
| `emcli account list` | Discover accounts: JSON `name` / `from` / `can_send` per account |
|
||||
@@ -125,6 +126,19 @@ read-only — tell the user; do not attempt another account without their say-so
|
||||
Defaults: `--folder INBOX`, `--limit 50` (max 500). Dates are RFC 3339 (e.g.
|
||||
`2026-06-01T00:00:00Z`). UIDs come from `list`/`search` output.
|
||||
|
||||
### Searching effectively
|
||||
|
||||
- `list`/`search` cover **one folder** (INBOX by default). To find mail that may be archived or
|
||||
sorted elsewhere, run `emcli folders --account A` to see the mailboxes, or search with
|
||||
`--all-folders` to sweep them all (each hit then carries a `folder` field — pass it back via
|
||||
`--folder` when you `get`/`ack` that message).
|
||||
- Prefer `--subject-contains` and `--from`. `--text` (full-text) is **best-effort**: some IMAP
|
||||
servers have no body index and return zero results for mail that `--subject-contains` finds —
|
||||
a `--text` miss does not mean the mail is absent.
|
||||
- An empty search result includes a `data.hint` with next steps — follow it. Some mail may be
|
||||
hidden by the user's inbound policy; that's normal and indistinguishable from mail that doesn't
|
||||
exist. Don't probe for hidden mail.
|
||||
|
||||
**Full reference** (every flag, exact JSON shapes for each command, attachment encoding, error
|
||||
codes, and the enforcement rules): `references/commands.md` — read it from disk, or fetch it from
|
||||
the raw base URL in "Files & first run" above if you don't have it locally.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# emcli agent command reference
|
||||
|
||||
The five agent commands you may use. Each prints **one** JSON object to stdout and sets a matching
|
||||
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`).
|
||||
|
||||
@@ -76,10 +76,30 @@ emcli get --account A [--folder F] --uid U
|
||||
|
||||
---
|
||||
|
||||
## `search` — server-side search (whole folder)
|
||||
## `folders` — list mailboxes
|
||||
|
||||
```
|
||||
emcli search --account A [--folder F] [--from X] [--subject-contains X] [--text X] \
|
||||
emcli folders --account A
|
||||
```
|
||||
|
||||
`data`:
|
||||
```json
|
||||
{ "folders": [
|
||||
{ "name": "INBOX", "delimiter": ".", "selectable": true },
|
||||
{ "name": "INBOX.Archive", "delimiter": ".", "selectable": true } ] }
|
||||
```
|
||||
|
||||
- INBOX first, then case-insensitive name order.
|
||||
- `selectable: false` marks hierarchy-only entries — don't pass those to `--folder`.
|
||||
- Use this before searching outside INBOX: archived mail often lives in folders like
|
||||
`INBOX.Archive` that `list`/`search` won'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]
|
||||
```
|
||||
|
||||
@@ -87,13 +107,33 @@ emcli search --account A [--folder F] [--from X] [--subject-contains X] [--text
|
||||
|---|---|
|
||||
| `--from` | Sender contains |
|
||||
| `--subject-contains` | Subject contains |
|
||||
| `--text` | Full-text |
|
||||
| `--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 `--folder` when you `get`/`ack` a hit.
|
||||
- `data.skipped_folders` lists mailboxes the server refused to search (always present, `[]` when
|
||||
none) — transport errors, not policy.
|
||||
- `--limit` caps total visible results across the sweep.
|
||||
- On Gmail-style servers the same message can appear under several folders with different UIDs;
|
||||
dedupe by `message_id` if 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
|
||||
@@ -150,7 +190,8 @@ is subject to the inbound filter: a filtered/missing source returns `not_found`.
|
||||
- **Mode:** `RO` accounts reject `send`. `RW` can read and send.
|
||||
- **Inbound whitelist / subject filter:** disallowed mail is invisible everywhere (`list`/`search`
|
||||
omit it; `get`/`ack` return `not_found`). You can't tell a filtered message from a non-existent
|
||||
one — by design.
|
||||
one — by design. The empty-search `hint` is 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.com` matches any address at that
|
||||
domain; otherwise an exact-address match.
|
||||
|
||||
Reference in New Issue
Block a user