The email interface, wired in
Six files as tabs: email.go → console.go → resend.go → config.go → handlers.go → main.go (the interface + two implementations, then the config/struct/wiring that install them). The last three are modifications.
1. internal/email/email.go (new) — the Message, the Sender interface, and New (the picker: "resend" → Resend, anything else → console).
2. internal/email/console.go (new) — prints a boxed email to os.Stdout. Note the mu sync.Mutex (concurrent sends don't interleave) and the deliberate choice to write with fmt.Fprintf, not slog:
// It writes directly rather than through slog on purpose — slog would escape the
// newlines into \n literals and mangle the box.
3. internal/email/resend.go (new) — the prod sender: marshal JSON, POST https://api.resend.com/emails with a bearer token. One HTTP call.
4. internal/config/config.go (modify) — three fields: EmailSender (default "console"), ResendAPIKey, EmailFrom. Pure additions.
5. internal/handlers/handlers.go (modify) — the Handlers struct gains Mailer email.Sender; New takes it.
6. cmd/signflow/main.go (modify) — construct the sender from config and pass it in:
mailer := email.New(cfg.EmailSender, cfg.ResendAPIKey, cfg.EmailFrom, log)
h := handlers.New(cfg, queries, sessions, mailer, log)
Verify. go run ./cmd/signflow and read the very first log lines:
level=INFO msg="email: using console sender (links print to stdout, no email is sent)"
That line is the whole promise: no EMAIL_SENDER set, so you got the console sender — every email in the app will land in this terminal, no API key anywhere. Nothing sends yet (the reset handler is the next step), but the plumbing is in and announcing itself.