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

4.7 KiB

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.