`whitelist <in|out> <add|remove|list>` has two positional slots; omitting
either let a --flag slide into the slot and produced a misleading
"--account is required". Validate the direction and the subcommand up
front, before flag parsing, so the real mistake is reported.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A literal "~/..." in EMCLI_DB has no shell to expand it, so SQLite opened
it relative to the cwd and silently created a stray "~" directory tree.
Expand a leading "~" or "~/" to the user's home dir; "~user", mid-path
tildes, and absolute/relative paths are left untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
account list now routes to the agent role; an agent (EMCLI_KEY only) gets a
JSON envelope of name/from/can_send, while the admin keeps the full text
table. account add/edit/remove stay admin-only.
Also emit the agent path's missing-key/open failure as a JSON Failure
envelope (per spec), and update the stale run_test case that asserted the
old admin-only behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Let an agent holding only EMCLI_KEY discover accounts via `account list`,
exposing name/from/can_send (not host/username); admin keeps the full
text table. account add/edit/remove stay admin-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- tui.ValidFromAddress: exported validator; blank passes, malformed rejects
- Fields.FromAddress: new field, round-trips through ToAccount/FieldsFromAccount
- Fields.Validate: calls ValidFromAddress before returning nil
- TUI form: from_address fieldDef between username and password
- send.go: From set via acc.SendFrom() instead of acc.Username
- admin.go account add: --from flag with pre-parse validation
- admin.go account edit: --from flag; validate before Visit, apply in Visit
- USER-MANUAL.md: --from flag added to account add flags table
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve final-review findings: commandRole is now the single source of
truth (Run resolves role once and threads it to handlers, replacing
hardcoded openStore roles). Tighten crypto/SKILL/SPEC/USER-MANUAL wording
and document init's agent-key-on-first-init-only semantics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review: fail fast on store.Open/key-loader errors in test setup;
use t.Errorf+continue so every admin command is checked, not just the first.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Initialize a DB, drop EMCLI_ADMIN_KEY, attempt every admin command with
only EMCLI_KEY: each is refused and the DB is byte-for-byte unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openStore(role) selects the DEK wrap slot; admin commands require
EMCLI_ADMIN_KEY (admin slot only, no agent fallback); init writes both
slots from both keys. Test helpers seed the wrap slots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Open() now opens LOCKED; InitKeys generates a DEK sealed under both KEKs;
Unlock loads it from the role's slot (admin slot has no agent fallback).
s.key becomes the DEK, so account/mail crypto is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace KeyFromEnv with AgentKeyFromEnv/AdminKeyFromEnv reading EMCLI_KEY
and EMCLI_ADMIN_KEY; add NewDEK for envelope encryption. Seal/Open double
as DEK wrap/unwrap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enforce the agent/admin trust boundary with two env keys (EMCLI_ADMIN_KEY,
EMCLI_KEY) via envelope encryption: one DEK wrapped per role. Admin commands
unwrap the admin slot only (no agent fallback), so a forced agent holding
EMCLI_KEY cannot authorize config changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump EMCLI_VERSION default (install.sh + AGENTIC-MANUAL.md + RELEASING.md) so
agents install the v0.4.1 binary (help for all commands, SMTP-port form default,
skill split). Drop the stale "placeholder until first release" note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NewAccountForm prefilled defaults for mode, IMAP port, and both securities but
left SMTP port blank. Default it to 465 to match `account add --smtp-port`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
emcli had only raw flag usage and no command listing; `--help` on agent commands
even emitted a JSON error envelope and exited 2. Add real help:
- Top-level `emcli` / `help` / `-h` / `--help` prints a grouped command catalogue
(agent vs admin) with one-line summaries and the EMCLI_KEY/EMCLI_DB env vars.
- `emcli help <command>` prints that command's synopsis + summary.
- `emcli <command> --help` prints synopsis + summary + flags and exits 0. Agent
commands keep stdout JSON-free (usage goes to stderr); admin commands print to
stdout. Help works without EMCLI_KEY (no DB access).
- help.go holds the command catalogue; flag.ErrHelp is handled as success, and
admin handlers short-circuit help before opening the store.
Unknown commands still error (exit 2). Full suite passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SKILL.md body loads into context on every activation, so one-time install/
setup prose was wasted context once emcli is running. Move it out:
- New AGENTIC-MANUAL.md: get-the-files bootstrap, binary install (incl. options
and build-from-source, folding in the old references/install.md), EMCLI_KEY,
account discovery. Fetched only during first-time setup.
- SKILL.md trimmed (182→~145 lines) to the recurring path: security model, a short
"Files & first run" pointer + per-session preflight, the list/get/ack/send
workflow, JSON envelope, command table, enforcement, do/don't.
- Remove references/install.md (folded in); fix RELEASING.md pointer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An agent pointed at the repo may load only SKILL.md and then guess a wrong path
for the installer (it fetched /scripts/install.sh at repo root → 404; the file is
under skills/emcli/). Fix:
- Add a "First: get this skill's files" section: the supporting scripts/ and
references/ files, the absolute raw base URL to fetch them, and the Gitea
contents API to enumerate the directory.
- Install step now gives an absolute-URL fetch-then-run for the only-SKILL.md case,
keeping `bash scripts/install.sh` for the bundled case.
- State that every scripts/… and references/… path is relative to the skill dir and
resolvable against the raw base URL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Gitea Actions workflow published v0.4.0 successfully, so drop the "untested"
caveat. Document that release assets download anonymously — the repo/releases must
be public or install.sh gets a 404 (private repos 404 unauthenticated downloads).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Makefile `release`: cross-compiles CGO-free static binaries for linux/amd64,
linux/arm64, darwin/amd64, darwin/arm64, windows/amd64 into dist/, named
emcli_<version>_<os>_<arch>[.exe] (matching skills/emcli/scripts/install.sh),
plus a sha256 checksums.txt. VERSION is injected into internal/version.String.
- Makefile `publish`: creates the Gitea release and uploads all dist/ assets via tea.
- .gitea/workflows/release.yml: on a v* tag, build + publish via the Gitea API.
- RELEASING.md: the local (make) and CI flows.
Verified end-to-end: `make release VERSION=v0.4.0` builds all five assets with the
version baked in; serving them locally, skills/emcli/scripts/install.sh downloads,
passes checksum verification, and the installed binary reports v0.4.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Adds the admin/diagnostics surface from SPEC §7.2:
- doctor [--account]: per-account IMAP + (RW) SMTP connectivity/auth checks via
new mail.CheckIMAP/CheckSMTP (connect+auth only, no mail). Exit non-zero on any
failure; secrets never printed.
- store.UpdateAccount: partial edit, re-encrypts password/secrets only when a
non-empty value is supplied (blank keeps existing). RecentAuditFor(account).
- config set/get (validates audit_retention_days), audit list [--account][--limit],
account edit (flag partial-update) / remove [--yes].
- internal/tui: bubbletea AccountForm with pure, fully-tested Fields (validation +
store.Account assembly + edit prefill). init / bare `account add` / `account edit
--name X` drop into the TUI; flag forms remain for scripting.
Built test-first; full suite green incl -race. Validated live against the mxlogin
(password) and Gmail (app-password) accounts. Live validation caught a real bug:
doctor authenticated with empty passwords because it iterated ListAccounts (which
strips secrets) — fixed to re-fetch via GetAccount, locked in by a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Evaluated OAuth2 (SPEC §10) and chose not to build it this phase. A self-built,
unverified OAuth app suffers Google's 7-day refresh-token expiry in Testing
status (or the unverified-warning + restricted-scope verification cost in
Production). For a single-user personal tool, a Gmail App Password (2FA) is
strictly simpler and reuses the IMAP/SMTP password auth from Phases 1–2.
Validated live against a real Gmail account over app-password auth: list/get/
search, send, and a full SMTP-out → IMAP-in round-trip. No code changes were
required; the speculative OAuth store fields started mid-session were reverted.
OAuth2 remains a clean future addition (schema columns already present).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
If header/body parsing errored mid-fetch we returned without draining the
message channel, so the UidFetch goroutine could block on a full channel.
Both fetch paths now break, drain remaining messages, then read the done
error. Verified with the race detector.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
list and search now fetch BODY.PEEK[HEADER] + BODYSTRUCTURE instead of the
whole RFC822 message, so listing a large mailbox no longer downloads every
message body and attachment. Header parsing reuses the same go-message path
(RFC2047 decoding/formatting preserved); has_attachments is derived from the
BODYSTRUCTURE tree. FetchFull keeps fetching the full message for get.
Validated end-to-end against a live IMAP account: list/search/get output
identical to the prior full-fetch behaviour, has_attachments correct.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pass 0 (unlimited) to m.Search so the mail layer returns all matching
headers; the existing post-filter loop already caps at the caller's
limit, mirroring ListCmd. Add TestSearchLimitCountsVisibleOnly to prove
filtering happens before the cap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thread uidv through setup's return value (new uint32 before the cleanup
func) so AckCmd no longer makes a redundant SelectFolder round-trip that
silently returned 0 on failure and recorded acks under the wrong
UID-validity epoch. All four callers updated; read-only callers ignore
the value with _.
- Cap search results to limit (keep most-recent UIDs)
- Propagate io.ReadAll errors from body reads in fetchByUIDSet
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>