# emcli — Phase 2 Plan: Send Path **Date:** 2026-06-22 **Depends on:** Phase 1 (read path, encrypted store, policy package, JSON envelope) — complete. **Scope (SPEC §7.1 `send`, §9 Outbound, §10 threading headers):** SMTP send, MIME building, reply threading, outbound policy (RO rejection + whitelist-out), the `send` agent command, and admin support for SMTP account fields. ## Approach Test-first (TDD), one slice at a time, mirroring Phase 1's package boundaries. Pure logic (`policy`, MIME building) is unit-tested directly; the live SMTP path is validated against the real provider at the end (as Phase 1 did for IMAP), since a unit SMTP server is out of scope. ## Tasks ### 1. `store`: load/save SMTP fields on `Account` The schema already has `smtp_host/smtp_port/smtp_security`, but `Account` and its scan/insert/select don't carry them. Add `SMTPHost string`, `SMTPPort int`, `SMTPSecurity string`; thread through `AddAccount`, `GetAccount`, `ListAccounts`, `scanAccount`. Admin `account add` gains `--smtp-host/--smtp-port/--smtp-security`. **Test:** add an account with SMTP fields, read it back, assert equality. ### 2. `policy`: outbound rule `OutboundRule{ Mode string; WhitelistOutEnabled bool; WhitelistOut []string }` with `Check(recipients []string) (ok bool, reason string)`: - `Mode == "RO"` → `(false, "ro_mode")`. - `WhitelistOutEnabled` and any recipient fails `MatchAddress` → `(false, "whitelist_out")` (whole send blocked — no partial). - else `(true, "")`. **Tests:** table-driven — RW allows; RO blocks; all-recipients-must-pass; domain `@x.com` match; case-insensitivity; one bad recipient among good ones blocks all. ### 3. `mail`: SMTP send + MIME building - Add `References` to `Header`; populate in `readHeader` from the `References` header so a reply can chain correctly. - `SMTPConfig{Host,Port,Security,Username,Password}`. - `OutgoingMessage{From,To,Cc,Bcc,Subject,BodyText,Attachments,InReplyTo,References,Date}`. - `BuildMIME(OutgoingMessage) ([]byte, error)` via `go-message/mail` writer: a plain-text inline part plus any attachments; sets From/To/Cc/Subject/Date and, when present, `In-Reply-To`/`References`. (Bcc recipients go in the SMTP envelope, not the headers.) - `SendSMTP(SMTPConfig, OutgoingMessage) error`: dial (`tls` → `DialTLS`, `starttls` → `DialStartTLS`), `Auth` with SASL PLAIN, then `SendMail(from, allRecipients, mime)`. **Tests:** `BuildMIME` round-trips through `ParseMessage` (body + attachment survive); threading headers present when set; recipients assembled correctly. Live send deferred to task 6. ### 4. `cli`: `send` command + dispatch wiring - Add `Send func(store.Account, mail.OutgoingMessage) error` to `Deps` (live impl wraps `mail.SendSMTP`). - `SendCmd(d, account, to, cc, bcc, subject, body, attachPaths, replyToUID, replyFolder)`: 1. Load account. Build `OutboundRule`; `Check(to+cc+bcc)` → on block, audit `send`/`blocked`/reason and emit policy error. 2. If `replyToUID > 0`: dial IMAP, apply **inbound** filter (a filtered/invisible source UID → `not_found`), fetch its `Message-ID`+`References`, set `InReplyTo`/`References`. 3. Read `--attach` files from disk into attachments. 4. `d.Send(acc, msg)`; on success audit `send`/`allowed`, emit `{ "sent": true, "recipients": [...] }`. - `run.go`: route `send`; repeatable `--to/--cc/--bcc/--attach` (custom `stringSlice` flag.Value, also comma-splitting), `--subject/--body/--reply-to/--folder`. **Tests (fake Sender + fake Mailer):** RO blocks (sender never called); whitelist-out blocks; happy path calls sender with right recipients and emits success; reply-to on a filtered source returns `not_found`. ### 5. Build / vet / full test suite (incl `-race`) `CGO_ENABLED=0 go build`, `go vet ./...`, `go test ./...` and `-race`. Pristine output. ### 6. Live send validation Configure an RW account with the test SMTP creds (`emcli@stevecliff.com` @ `friday.mxlogin.com`), send a real message to `me@stevecliff.com`, confirm the success envelope. Spot-check enforcement live: RO account rejects send; whitelist-out blocks a non-listed recipient. ## Out of scope (later phases) - OAuth2 send auth (Phase 3) — password/SASL-PLAIN only here. - Admin TUI / `doctor` (Phase 4). - Carry-over Minor items from Phase 1 (audit-row completeness, CLI polish) — touch only if directly in the way.