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>
4.8 KiB
emcli — Phase 4 Status Report
Date: 2026-06-22
Branch: main
Phase 4 scope (SPEC §7.2): doctor connectivity/auth diagnostics; admin completeness
(account edit/remove, config set/get, audit list); and a bubbletea TUI for init
and interactive account add/edit.
TL;DR
Phase 4 is complete and validated live. Built test-first; the binary builds as a single static
CGO-free executable, go vet is clean, and the full suite passes including -race. doctor and
the admin commands were exercised against the two real accounts (mxlogin password auth + Gmail
app-password). A real bug — doctor running checks with stripped (empty) credentials — was caught
by live validation, reproduced with a regression test, and fixed.
What was built
| Area | Change | Status |
|---|---|---|
store |
UpdateAccount (partial edit; re-encrypts password/secrets only when non-empty, blank keeps existing); RecentAuditFor(account, limit). |
✅ |
mail |
CheckIMAP (connect+login+logout) and CheckSMTP (connect+SASL-PLAIN auth+quit) — no mail transferred. |
✅ |
cli |
doctor [--account] (per-account IMAP/SMTP ok/FAIL, exit non-zero on any failure, no secrets); config set/get (validates audit_retention_days); audit list [--account] [--limit]; account edit (flag partial-update) / account remove [--yes]. |
✅ |
tui (new pkg) |
AccountForm bubbletea model over bubbles/textinput, with pure, fully-tested Fields (validation + store.Account assembly + edit prefill). |
✅ |
cli wiring |
init (create/open DB, seed audit_retention_days=90, add first account via TUI); bare account add → TUI; account edit --name X (only --name) → TUI prefilled. |
✅ |
Commands added
emcli doctor [--account <name>]
emcli config set <key> <value> # e.g. audit_retention_days
emcli config get <key>
emcli audit list [--account <name>] [--limit N]
emcli account edit --name <n> [--mode|--imap-host|--smtp-host|…] # flag partial-update
emcli account edit --name <n> # interactive (TUI)
emcli account remove --name <n> --yes
emcli account add # interactive (TUI)
emcli init # interactive (TUI)
Live validation
Against the two real accounts (mxlogin password auth, gmail app-password) in an isolated DB:
doctor—mxloginandgmailboth reportIMAP ok/SMTP ok; a deliberately bad-password account reportsIMAP FAIL: Invalid credentials(clean error, no crash); SMTP shownn/afor RO. Exit1when any check fails,0when--accounttargets a passing one.config—set/getround-trip;audit_retention_days=-5rejected (exit 2).audit list— rendered a reallist/allowedrow.account edit(flag) — set a subject-regex onmxlogin; a follow-updoctorstill passed, provingUpdateAccountpreserved the encrypted password through the edit.account remove --yes— deleted an account; gone fromaccount list.- TUI (
init/interactive) requires a real terminal; without a TTY it fails cleanly (could not open a new TTY), no panic. Drive it interactively to use.
Bug found and fixed during live validation
doctor initially authenticated with empty passwords for every account — it iterated
ListAccounts(), which deliberately strips secrets, and passed those credential-less structs to the
live checks. Caught immediately against real servers ("Empty username or password"). Fixed by
re-fetching each account with GetAccount (which decrypts) before checking; locked in with
TestDoctorUsesDecryptedCredentials.
Verification
CGO_ENABLED=0 go build ./... → OK, single static binary
go vet ./... → clean
go test ./... → all packages pass (incl. new internal/tui)
go test -race ./... → all packages pass
New tests: store update/audit-filter; mail check-fails-cleanly; cli doctor (all-ok, failure,
RO-skip, account-filter, decrypted-creds regression), config/audit/edit/remove via Run(); tui
Fields validation/assembly/prefill and form submit/cancel.
Known limitations / deferred
- TUI is a minimal keyboard-driven form (bool fields entered as
y/n, enums as text); no mouse or theming. Sufficient for admin use. - OAuth consent in
initomitted (OAuth deferred in Phase 3). - Carry-over Minor items from Phase 1 (audit-row completeness, some CLI polish) remain open.
Project status
Phases 1–4 complete: read path, send path, Gmail (app password), and admin/TUI/doctor. The core
emcli surface from the SPEC is implemented and validated live, with OAuth2 (§10) the one
deliberately-deferred item.