# Send-as "From" address — design **Date:** 2026-06-23 **Status:** Approved (pending spec review) ## Problem An account's configuration has no field for the email address used as the `From:` when sending mail. Today the From is silently aliased to the login username (`internal/cli/send.go:26`, `From: acc.Username`), and neither `store.Account` nor the `accounts` table has any `from`/`address`/`email` column. This works only when the login username is exactly the desired send-as address. It breaks for: - providers where the login is an account ID rather than an email, - sending from an **alias** of the mailbox, - wanting a **display name** (`Steve Cliff `) rather than a bare address, - Gmail App Passwords where envelope-from and header-from may differ. ## Decisions - **Field shape:** a single freeform RFC 5322 From identity — bare (`me@stevecliff.com`) or with a display name (`Steve Cliff `). One field, not a split address/display-name pair. - **Fallback:** when the from-address is blank, fall back to `Account.Username` (current behaviour). No migration/backfill of existing accounts required; they keep working unchanged. ## Design ### 1. Data model Add `FromAddress string` to `store.Account` and a `from_address TEXT` column to the `accounts` table. **Not encrypted** — it is not a secret (it appears in every outgoing header), so it is stored as plaintext like `username`. ### 2. Fallback in one place Add a method so the fallback rule lives in exactly one spot: ```go // SendFrom returns the From identity for outgoing mail, falling back to the // login username when no explicit from-address is configured. func (a Account) SendFrom() string { if a.FromAddress != "" { return a.FromAddress } return a.Username } ``` `internal/cli/send.go` changes `From: acc.Username` → `From: acc.SendFrom()`. ### 3. Envelope sender vs header From A display-name From breaks the SMTP envelope: `c.SendMail("Steve ", …)` is invalid — the envelope sender must be the **bare** address. - `mail.BuildMIME` keeps using the full `m.From` for the `From:` header (go-message renders `Name ` correctly via `SetAddressList`). - `mail.SendSMTP` derives the envelope sender as the bare address: `gomail.ParseAddress(m.From).Address`. If parsing fails, fall back to the raw `m.From` (preserves today's behaviour for plain addresses). Header carries the display name; envelope carries the bare address. ### 4. Migration The schema is v1, applied via `CREATE TABLE … IF NOT EXISTS`, which will not add a column to an existing DB, and there is no migration runner yet. - Add the `from_address` column to the `CREATE TABLE accounts` statement (new DBs get it directly). - In `store.Open`, after applying the schema, run a version-gated migration: if the stored `schema_version` is `< 2`, execute `ALTER TABLE accounts ADD COLUMN from_address TEXT` and set `schema_version` to `2`. - Bump the `schemaVersion` constant to `2`. SQLite `ALTER TABLE … ADD COLUMN` is cheap and safe. The migration is idempotent under the version gate. ### 5. Inputs & validation - **CLI:** add a `--from` flag to `account add` and `account edit`. On `edit`, follow the existing `fs.Visit` overlay pattern (only set when the flag was passed). - **TUI:** add a `FromAddress` field to `tui.Fields`, a `fieldDef` (`{key: "from_address", label: "From address (optional)"}`), and wire it through `ToAccount`, `FieldsFromAccount`, `fieldValue`, and `collect`. - **Validation:** when the from-address is non-empty, reject it unless `gomail.ParseAddress` accepts it (covers bare and display-name forms). A blank from-address is always valid (→ fallback). Applied in `Fields.Validate` and on the `account add`/`edit` flag path. - **Persistence:** thread `from_address` through `AddAccount`, `UpdateAccount`, `scanAccount`, and the three `SELECT` column lists in `internal/store/account.go`. Stored via `nullStr` (blank → NULL). ### 6. Tests - `SendFrom()`: returns the from-address when set; returns username when blank. - `SendSMTP` envelope: when From is `Name `, the envelope sender passed to the server is the bare `addr` (table test on the extraction helper). - Migration: open a v1 DB with no `from_address` column → column is added, `schema_version` becomes 2, and an existing account still sends from its username. - `Fields.Validate`: rejects a malformed from-address; accepts bare and display-name forms; accepts blank. - Round-trip: `FieldsFromAccount` then `ToAccount` preserves `FromAddress`. ## Out of scope (YAGNI) - Separate envelope-from override field (derive it from From instead). - Per-message From override at send time. - Multiple aliases per account.