# 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 portability** — `pkg/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/_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 : ```go 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 : ```yaml 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 ## Links * Auth migration that requires this : [ADR-0028](0028-passwordless-auth-migration.md) * BDD test strategy that consumes Mailpit : [ADR-0030](0030-bdd-email-parallel-strategy.md) * Mailpit homepage : https://mailpit.axllent.org/ * Mailpit API reference : https://mailpit.axllent.org/docs/api-v1/