Files
dance-lessons-coach/adr/0029-email-infrastructure-mailpit.md
Gabriel Radureau 873f449d17 📝 docs(adr): ADR-0028/0029/0030 — passwordless auth + Mailpit + BDD email strategy
Three coordinated ADRs Proposed for the auth-completion sprint, requested
by user: signup → magic link by email → OpenID Connect Authorization
Code with PKCE, all integrated with Mailpit (already locally available
as docker image) and BDD parallel testing strategy.

ADR-0028 — Passwordless auth migration (sequenced):
- Phase A: magic link by email (no password storage, JWT issued on consume)
- Phase B: OpenID Connect Code flow with PKCE (cross-product SSO, mkcert
  for local HTTPS callbacks)
- Phase C (separate ADR later): decommission password auth

ADR-0029 — Email infra:
- Mailpit (axllent/mailpit:latest) for local dev + BDD (image already
  pulled, 51 MB), defaults SMTP :1025 / HTTP API :8025
- pkg/email.Sender interface for provider portability
- Production sender choice DEFERRED (separate ADR when volume / SLA
  / compliance requirements known — likely AWS SES or Postmark)

ADR-0030 — BDD email parallel strategy:
- Per-test recipient scoping: each scenario generates a unique address
  <scenario-key>-<8hex>@bdd.local
- Mailpit HTTP API filters by recipient → no cross-scenario interference
- pkg/bdd/mailpit/ helper package + pkg/bdd/steps/email_steps.go
- Preserves the 2.85x parallel BDD speedup from PR #35

Implementation lands in subsequent PRs ; today only the design is shipped.
README index updated with 3 new entries (0028/0029/0030 all Proposed).
2026-05-05 10:42:19 +02:00

7.1 KiB

29. Email infrastructure: Mailpit local + production deferred

Date: 2026-05-05 Status: Proposed Authors: Gabriel Radureau, AI Agent

Context and Problem Statement

ADR-0028 (passwordless auth) requires the application to send emails — magic-link tokens specifically. Email is a substrate decision : the choice of SMTP provider, the abstraction in code, and the local development experience all depend on it.

Two separate concerns :

  1. Local development + BDD tests : we need a local SMTP receiver that captures emails and exposes them for inspection. Real email providers (Gmail, SES, SendGrid) are unsuitable for local dev — they cost money, leak test data, and rate-limit aggressively.
  2. Production : the application needs to actually deliver mail to user inboxes. This decision is deferred — see "Out of scope" below.

ARCODANGE already has the Mailpit docker image pulled locally (axllent/mailpit:latest, 51 MB). Mailpit captures SMTP submissions on a port, stores them in-memory, exposes them via HTTP UI (default :8025) and an HTTP API (/api/v1/messages). It's the de-facto choice for Go projects needing local SMTP capture.

The application code needs to be provider-agnostic : a pkg/email package with a Sender interface, a Mailpit-compatible SMTP implementation, and a contract that production can swap for a real provider's adapter without changing call sites.

Decision Drivers

  • Local dev and CI must work without internet — emails should never leave the docker network in tests
  • Test inspection must be programmatic — BDD tests assert on email content, not just "an email was sent"
  • Production decision deferred — we don't know the volume / SLA / compliance requirements yet ; over-committing now is premature
  • Provider portabilitypkg/email interface lets us swap implementations without touching auth code
  • Cost — Mailpit is free, runs in a container, no API quota concerns

Considered Options

Option 1 (Chosen): Mailpit for local + tests, production via a production-grade provider TBD

  • Add Mailpit to docker-compose.yml (SMTP :1025, HTTP API :8025)
  • pkg/email package with a Sender interface
  • Default implementation : SMTPSender configured against the local Mailpit in dev/CI
  • Tests query Mailpit's HTTP API to inspect captured messages
  • Production deployment will add a separate pkg/email/<provider>_sender.go implementing the same interface — that decision is its own ADR

Option 2: MailHog instead of Mailpit

MailHog is the older, well-known alternative. Mailpit is its modern successor, written in Go, with a richer API and active maintenance.

  • Bad — abandoned upstream (last commit 2020). Mailpit is the natural replacement.

Option 3: In-process mock email sender

Write a MockSender that captures emails in a Go slice. No SMTP at all.

  • Good — fastest tests, zero infra
  • Bad — doesn't validate the actual SMTP wire format, the From/To/Subject headers, the encoding of multi-byte content, or the DKIM/Reply-To setup
  • Bad — doesn't double as a manual-inspection tool for the developer (no UI to look at the email)

Option 4: Send to a real but throwaway provider (Mailtrap, Mailosaur)

External services that capture-and-display emails.

  • Good — production-similar paths
  • Bad — costs money, requires an account, leaks test data, doesn't work offline

Decision Outcome

Chosen option : Option 1 — Mailpit for local + tests, production deferred.

Rationale :

  • Mailpit is the modern, maintained successor to MailHog ; image is already on the dev machine
  • The interface-first design (pkg/email.Sender) means production swap is a future ADR, not a refactor
  • BDD tests have a real wire-format path to assert on (cf. ADR-0030)
  • Zero monthly cost in dev/CI

Implementation Plan

  1. pkg/email/sender.go — define the Sender interface :
    type Sender interface {
        Send(ctx context.Context, msg Message) error
    }
    type Message struct {
        To      string
        From    string
        Subject string
        BodyText string
        BodyHTML string
        Headers  map[string]string // for trace correlation, e.g. X-Test-Scenario-ID
    }
    
  2. pkg/email/smtp_sender.go — implementation using net/smtp (stdlib) configured by auth.email.smtp_host, smtp_port, smtp_username, smtp_password, smtp_use_tls. For Mailpit defaults : smtp_host=localhost smtp_port=1025 smtp_use_tls=false.
  3. pkg/email/sender_test.go — unit tests using httptest-style fake SMTP, plus a *_integration_test.go (build tag integration) hitting the live Mailpit.
  4. docker-compose.yml — add the mailpit service :
    mailpit:
      image: axllent/mailpit:latest
      ports:
        - "1025:1025"  # SMTP
        - "8025:8025"  # HTTP UI / API
      environment:
        MP_MAX_MESSAGES: 5000
    
  5. pkg/config/config.go — add the auth.email.* config keys with defaults pointing at local Mailpit.
  6. Documentation : documentation/EMAIL.md covering local setup, message inspection via UI (http://localhost:8025), API queries.

Pros and Cons of the Options

Option 1 (Chosen — Mailpit)

  • Good — already locally available, free, modern, maintained
  • Good — provider-agnostic interface decouples from prod choice
  • Good — full SMTP wire format = realistic test path
  • Good — UI for manual inspection during dev
  • Bad — requires Mailpit running (one more docker-compose service)
  • Bad — production decision still pending

Option 2 (MailHog)

  • Bad — unmaintained, choosing it would create immediate tech debt

Option 3 (Mock only)

  • Bad — too much abstraction loss, can't catch wire-level bugs

Option 4 (Mailtrap / Mailosaur)

  • Bad — cost, network dependency, account management

Consequences

  • New service in docker-compose.yml — developers run docker compose up -d once and Mailpit is on
  • New pkg/email package — auth code (ADR-0028 magic link) calls Sender.Send() rather than direct SMTP
  • New auth.email.* config keys, new env vars (DLC_AUTH_EMAIL_SMTP_HOST etc.)
  • Mailpit's HTTP API becomes part of the BDD test contract — tests use it to assert messages were sent (cf. ADR-0030)
  • Production sender ADR (TBD) will be a separate decision — this ADR explicitly does NOT pick a vendor for prod

Out of scope

  • Production email provider selection — separate ADR when we know volume / SLA / compliance constraints. Likely candidates: AWS SES, Postmark, SendGrid, Mailjet. Magic-link emails are transactional + low-volume — most providers handle that easily.
  • DKIM/SPF/DMARC setup — production deliverability concern, not a local-dev concern
  • HTML email templating — we'll start with plain-text emails ; HTML can be added with a template package (e.g. html/template) when ARCODANGE branding requires it