Files
emcli/specifications/PHASE2-STATUS.md
T
steve c99eaedafd feat(send): Phase 2 send path — SMTP, MIME, reply threading, outbound policy
Adds the `send` agent command and everything behind it:

- store: Account carries SMTP host/port/security (NULL-safe scan/insert/select);
  admin `account add` gains --smtp-* flags (applied for RW accounts).
- policy: OutboundRule.Check(recipients) → (ok, reason); RO ⇒ ro_mode,
  whitelist-out blocks the whole send if any recipient fails (no partial send).
- mail: Header.References; OutgoingMessage + BuildMIME (plain text + attachments,
  In-Reply-To/References threading, Bcc envelope-only); SendSMTP (tls/starttls,
  SASL PLAIN, envelope send) via emersion/go-smtp.
- cli: SendCmd gates outbound, resolves --reply-to under the inbound filter
  (filtered/absent source ⇒ not_found), reads attachments, audits, emits the
  JSON envelope; repeatable --to/--cc/--bcc/--attach flags wired into the router.

Implemented test-first; full suite passes incl -race. Validated live against
friday.mxlogin.com: real send to me@stevecliff.com, RO + whitelist-out blocks,
and --reply-to threading off a live INBOX message. test-creds.md gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:39:07 +01:00

86 lines
4.7 KiB
Markdown

# emcli — Phase 2 Status Report
**Date:** 2026-06-22
**Branch:** `main`
**Phase 2 scope:** Send path — SMTP, MIME building, reply threading, outbound policy
(RO rejection + whitelist-out), the `send` agent command, and admin SMTP account fields.
## TL;DR
**Phase 2 is complete and validated end-to-end against a live SMTP account.** All slices were
implemented test-first (TDD: failing test → minimal code → green). The binary builds as a single
static CGO-free executable, `go vet` is clean, and the full unit-test suite passes including under
`-race`. A real message was sent to `me@stevecliff.com` over the live provider, and outbound
enforcement (RO, whitelist-out) plus reply threading were exercised against the live mailbox.
## Live validation (real SMTP/IMAP account)
Against the live cPanel/MXlogin account over implicit TLS (`friday.mxlogin.com:465` for SMTP,
`:993` for IMAP):
- **`send`:** delivered a plain-text message to `me@stevecliff.com` — `{"sent":true,
"recipients":["me@stevecliff.com"]}`, exit 0. SASL PLAIN auth over implicit TLS succeeded.
- **RO rejection:** a send from an `RO` account was blocked with
`{"code":"policy","message":"send blocked: ro_mode"}`, exit 1 — sender never dialed.
- **whitelist-out:** with `@stevecliff.com` whitelisted, a send to `stranger@elsewhere.com` was
blocked (`whitelist_out`); a send to `me@stevecliff.com` succeeded.
- **`--reply-to` threading:** replying to a real INBOX UID read the source's
`Message-ID`/`References` over IMAP and set `In-Reply-To`/`References` on the outgoing message
(subject to the inbound filter — a filtered/absent source returns `not_found`).
- **Audit:** every send (allowed or blocked) wrote an `audit_log` row with the correct
`result`/`reason`. Password confirmed **encrypted at rest** (no plaintext in the DB).
## What was built
| Package | Change | Status |
|---|---|---|
| `internal/store` | `Account` now carries `SMTPHost/SMTPPort/SMTPSecurity`; threaded through insert/select/scan (NULL-safe). | ✅ |
| `internal/policy` | `OutboundRule.Check(recipients) (ok, reason)` — RO ⇒ `ro_mode`; whitelist-out, any recipient fails ⇒ `whitelist_out` (no partial send). Reuses `MatchAddress` (case-insensitive, `@domain`). | ✅ |
| `internal/mail` | `Header.References` (parsed Message-IDs); `OutgoingMessage`, `BuildMIME` (plain-text + attachments + threading headers, Bcc envelope-only), `SendSMTP` (tls/starttls dial, SASL PLAIN, envelope send). | ✅ |
| `internal/cli` | `Deps.Send`; `SendCmd` (outbound gating → reply-to resolution w/ inbound filter → attachment reads → send → audit); `send` wired into the router with repeatable `--to/--cc/--bcc/--attach` flags. | ✅ |
| `internal/cli` (admin) | `account add` gains `--smtp-host/--smtp-port/--smtp-security` (applied for `RW`). | ✅ |
### `send` command
```
emcli send --account <a> --to <addr>… [--cc <addr>…] [--bcc <addr>…]
--subject <s> --body <text> [--attach <path>]… [--reply-to <uid> [--folder <f>]]
```
- Emits the standard JSON envelope; exit code mirrors `error`.
- `--to/--cc/--bcc/--attach` are repeatable and also accept comma-separated values.
### Enforcement (SPEC §9 Outbound) — verified
- `RO` accounts cannot send (blocked before any network I/O).
- With whitelist-out enabled, **every** recipient (to+cc+bcc) must match or the whole send is
blocked — no partial send.
- A `--reply-to` source that fails the **inbound** filter returns `not_found`: the agent cannot
thread off, or confirm the existence of, mail it isn't allowed to see.
## Verification
```
CGO_ENABLED=0 go build ./... → OK, single static binary
go vet ./... → clean
go test ./... → all packages pass
go test -race ./... → all packages pass
```
New tests: `policy` outbound matrix (RO, all-must-pass, domain, case, one-bad-blocks-all);
`mail` `BuildMIME` round-trip (body + attachment survive, threading headers present) and
`References` parsing; `cli` send gating (RO block, whitelist-out block/allow, happy path,
reply-to threading, filtered-source `not_found`).
## Process
Test-first per slice, tracked via the task list. Plan:
`specifications/plans/2026-06-22-phase2-send-path.md`.
## Known limitations / deferred (not defects)
- **Phase 3 — OAuth2:** SMTP auth here is password/SASL-PLAIN only; XOAUTH2 follows in Phase 3
(schema columns already present).
- **Phase 4 — Admin TUI + `doctor`.**
- `From` is taken from the account `username` (email-as-username, as in Phase 1). A distinct
display-name/from-address is a future nicety.
- No attachment size cap (SPEC §13 open item) — unchanged from Phase 1.
- Carry-over Minor items from Phase 1 (audit-row completeness, CLI polish) remain open; none were
in the Phase 2 path.