# Email infrastructure Outgoing email transport. Per [ADR-0029](../adr/0029-email-infrastructure-mailpit.md): Mailpit for local dev + BDD tests, production sender deferred. ## Local setup (one-time) Mailpit is part of `docker-compose.yml`: ```bash docker compose up -d # starts postgres + mailpit docker compose ps # confirm both running ``` Mailpit listens on: - **SMTP submission** — `localhost:1025` (the app sends here) - **HTTP UI / API** — http://localhost:8025 (you inspect captured messages here) No real emails leave the docker network. No internet required. ## Application configuration The application's outgoing transport is configured under `auth.email.*` in `config.yaml` (or via `DLC_AUTH_EMAIL_*` env vars). Defaults already match local Mailpit: ```yaml auth: email: from: noreply@dance-lessons-coach.local smtp_host: localhost smtp_port: 1025 smtp_use_tls: false timeout: 10s # smtp_username + smtp_password left empty for local Mailpit ``` For production, override these to point at the chosen provider (SES, Postmark, etc.). ## Inspecting messages ### Web UI http://localhost:8025 — list of all captured messages, search, raw view, HTML preview. ### HTTP API (for automation) ```bash # Latest 10 messages (no filter — /api/v1/messages is for pagination) curl -s 'http://localhost:8025/api/v1/messages?limit=10' | jq # Messages for a specific recipient — use /api/v1/search, NOT /messages # (the latter's `query` param is for pagination only, not filtering ; # verified empirically 2026-05-05) curl -s 'http://localhost:8025/api/v1/search?query=to:test-user@bdd.local' | jq # Get a specific message by ID (full content, headers, attachments) curl -s 'http://localhost:8025/api/v1/message/' | jq # Purge messages for a recipient (used in test cleanup) — also via /search curl -X DELETE 'http://localhost:8025/api/v1/search?query=to:test-user@bdd.local' ``` Full API: https://mailpit.axllent.org/docs/api-v1/ ## Sending email from Go code ```go import "dance-lessons-coach/pkg/email" sender := email.NewSMTPSender(email.SMTPConfig{ Host: cfg.GetEmailConfig().SMTPHost, Port: cfg.GetEmailConfig().SMTPPort, // username/password optional — empty means no AUTH (Mailpit local) }) err := sender.Send(ctx, email.Message{ To: "alice@example.com", From: cfg.GetEmailConfig().From, Subject: "Your magic link", BodyText: "Click: https://example.com/magic-link/consume?token=...", Headers: map[string]string{ // optional — useful for BDD test correlation "X-Trace-Id": "req-abc-123", }, }) ``` Or, when both text and HTML are needed (`multipart/alternative`): ```go err := sender.Send(ctx, email.Message{ To: "alice@example.com", From: "...", Subject: "...", BodyText: "Click: https://...", BodyHTML: `

Click your magic link

`, }) ``` ## Production sender (TBD) Not chosen yet. When ready, implement another `email.Sender` in `pkg/email/_sender.go` and wire it via the config. The `Sender` interface is the swap point — call sites don't change. ## Cross-references - [ADR-0028 — Passwordless auth migration](../adr/0028-passwordless-auth-migration.md) (consumes this infrastructure) - [ADR-0029 — Email infrastructure decision](../adr/0029-email-infrastructure-mailpit.md) - [ADR-0030 — BDD email parallel strategy](../adr/0030-bdd-email-parallel-strategy.md) - [Mailpit docs](https://mailpit.axllent.org/docs/)