# SMTP Configuration – Concept

**Status:** Implemented  
**Last updated:** 2026-03-12

---

## 1. Goal

Enable configurable SMTP for sending transactional emails (join confirmation, user confirmation, password reset). Configuration via **environment variables** and **Admin Settings** (database), with the same precedence pattern as OIDC and Vereinfacht: **ENV overrides Settings**. Include a **test email** action in Settings (button + recipient field) with clear success/error feedback.

---

## 2. Scope

- **In scope:** SMTP server configuration (host, port, credentials, TLS/SSL), sender identity (from-name, from-email), test email from Settings UI, warning when SMTP is not configured in production, specific error messages per failure category, graceful delivery errors in AshAuthentication senders.
- **Out of scope:** Separate adapters per email type; retry queues.

---

## 3. Configuration Sources

| Source   | Priority | Use case                          |
|----------|----------|-----------------------------------|
| ENV      | 1        | Production, Docker, 12-factor     |
| Settings | 2        | Admin UI, dev without ENV          |

When `SMTP_HOST` is set, SMTP runs in **ENV-only mode**:
- all SMTP fields in Settings are read-only,
- saving SMTP settings in the UI is disabled,
- and the UI shows a warning block if required SMTP ENV values are missing.
- the UI displays the effective ENV-driven SMTP values in disabled fields so admins can verify what is active.

---

## 4. SMTP Parameters

| Parameter       | ENV                    | Settings attribute  | Notes                                       |
|----------------|------------------------|---------------------|---------------------------------------------|
| Host           | `SMTP_HOST`            | `smtp_host`         | e.g. `smtp.example.com`                     |
| Port           | `SMTP_PORT`            | `smtp_port`         | Default 587 (TLS), 465 (SSL), 25 (plain)    |
| Username       | `SMTP_USERNAME`        | `smtp_username`     | Optional if no auth                         |
| Password       | `SMTP_PASSWORD`        | `smtp_password`     | Sensitive, not shown when set               |
| Password file  | `SMTP_PASSWORD_FILE`   | —                   | Docker/Secrets: path to file with password  |
| TLS/SSL        | `SMTP_SSL`             | `smtp_ssl`          | `tls` / `ssl` / `none` (default: tls)       |
| Sender name    | `MAIL_FROM_NAME`       | `smtp_from_name`    | Display name in "From" header (default: Mila)|
| Sender email   | `MAIL_FROM_EMAIL`      | `smtp_from_email`   | Address in "From" header; must match SMTP user on most servers |

**Boot-time ENV handling:** In `config/runtime.exs`, if `SMTP_PORT` is set but empty or invalid, it is treated as unset and default 587 is used. This avoids startup crashes (e.g. `ArgumentError` from `String.to_integer("")`) when variables are misconfigured in deployment.

**Important:** On most SMTP servers (e.g. Postfix with strict relay policies) the sender email (`smtp_from_email`) must be the same address as `smtp_username` or an alias that is owned by that account.

**Settings UI:** The form uses three rows on wide viewports: host, port, TLS/SSL | username, password | sender email, sender name. Content width is limited by the global settings wrapper (see `DESIGN_GUIDELINES.md` §6.4).

---

## 5. Password from File

Support **SMTP_PASSWORD_FILE** (path to file containing the password), same pattern as `OIDC_CLIENT_SECRET_FILE` in `runtime.exs`. Read once at runtime; `SMTP_PASSWORD` ENV overrides file if both are set.

---

## 6. Behaviour When SMTP Is Not Configured

- **Dev/Test:** Keep current adapters (`Swoosh.Adapters.Local`, `Swoosh.Adapters.Test`). No change.
- **Production:** If neither ENV nor Settings provide SMTP (no host):
  - Show a warning in the Settings UI.
  - Delivery attempts silently fall back to the Local adapter (no crash).

### 6.1 Behaviour in ENV-only mode (`SMTP_HOST` set)

- The SMTP source of truth is environment variables only.
- The UI does not allow editing SMTP fields in this mode.
- The Settings page shows a warning block when required values are missing:
  - `SMTP_USERNAME`
  - `SMTP_PASSWORD` or `SMTP_PASSWORD_FILE`

---

## 7. Test Email (Settings UI)

- **Location:** SMTP / E-Mail section in Global Settings.
- **Elements:** Input for recipient, submit button inside a `phx-submit` form.
- **Behaviour:** Sends one email using current SMTP config and `mail_from/0`. Returns `{:ok, _}` or `{:error, classified_reason}`.
- **Error categories:** `:sender_rejected`, `:auth_failed`, `:recipient_rejected`, `:tls_failed`, `:connection_failed`, `{:smtp_error, message}` — each shows a specific human-readable message in the UI.
- **Permission:** Reuses existing Settings page authorization (admin).

---

## 8. Sender Identity (`mail_from`)

`Mv.Mailer.mail_from/0` returns `{name, email}`. Priority:
1. `MAIL_FROM_NAME` / `MAIL_FROM_EMAIL` ENV variables
2. `smtp_from_name` / `smtp_from_email` in Settings (DB)
3. Hardcoded defaults: `{"Mila", "noreply@example.com"}`

Provided by `Mv.Config.mail_from_name/0` and `Mv.Config.mail_from_email/0`.

---

## 9. Join Confirmation Email

`MvWeb.Emails.JoinConfirmationEmail` uses the same SMTP configuration as the test email: `Mailer.deliver(email, Mailer.smtp_config())`. This ensures Settings-based SMTP is used when not configured via ENV at boot. On delivery failure the domain returns `{:error, :email_delivery_failed}` (and logs via `Logger.error`); the JoinLive shows an error message and no success UI.

---

## 10. AshAuthentication Senders

Both `SendPasswordResetEmail` and `SendNewUserConfirmationEmail` use `Mv.Mailer.deliver/1` (not `deliver!/1`). Delivery failures are logged (`Logger.error`) and not re-raised, so they never crash the caller process. AshAuthentication ignores the return value of `send/3`.

---

## 11. TLS / SSL in OTP 27

OTP 26+ enforces `verify_peer` by default, which fails for self-signed or internal SMTP server certificates.

By default, TLS certificate verification is relaxed (`verify_none`) so self-signed or internal SMTP servers work. For public SMTP providers (Gmail, Mailgun, etc.) you can enable verification:

- **ENV (prod):** Set `SMTP_VERIFY_PEER=true` (or `1`/`yes`) when configuring SMTP via environment variables in `config/runtime.exs`. This sets `config :mv, :smtp_verify_peer` and is used for both boot-time and per-send config.
- **Default:** `false` (verify_none) for backward compatibility and internal/self-signed certs.

Verify mode is set in `tls_options` for port 587 (STARTTLS). For port 465 (implicit SSL), the initial connection is `ssl:connect`, so we also pass `sockopts: [verify: verify_mode]` so the SSL handshake uses the same mode. For 587 we must not pass `verify` in sockopts—gen_tcp is used first and rejects it (ArgumentError). The logic lives in `Mv.Smtp.ConfigBuilder.build_opts/1` (single source of truth), used by `config/runtime.exs` (boot) and `Mv.Mailer.smtp_config/0` (Settings-only).

**Tests:** `Mv.Smtp.ConfigBuilderTest` asserts sockopts/TLS shape. `Mv.Mailer.smtp_config/0` returns `[]` when the mailer adapter is `Swoosh.Adapters.Test`; `test/mv/mailer_smtp_config_test.exs` asserts that guard and, with the adapter temporarily set to `Swoosh.Adapters.Local`, wiring from ENV. Those mailer tests use `Mv.DataCase` so Settings fallbacks in `Mv.Config` (e.g. SMTP username/password when ENV is unset) stay under the SQL sandbox.

---

## 12. Summary Checklist

- [x] ENV: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_PASSWORD_FILE`, `SMTP_SSL`.
- [x] ENV: `MAIL_FROM_NAME`, `MAIL_FROM_EMAIL` for sender identity.
- [x] Settings: attributes and UI for host, port, username, password, TLS/SSL, from-name, from-email.
- [x] Password from file: `SMTP_PASSWORD_FILE` supported in `runtime.exs`.
- [x] Mailer: Swoosh SMTP adapter configured from merged ENV + Settings when SMTP is configured.
- [x] Per-request SMTP config via `Mv.Mailer.smtp_config/0` for Settings-only scenarios.
- [x] TLS certificate validation relaxed for OTP 27 (tls_options for 587; sockopts with verify only for 465).
- [x] Prod warning: clear message in Settings when SMTP is not configured.
- [x] Test email: form with recipient field, translatable content, classified success/error messages.
- [x] Join confirmation email: uses `Mailer.smtp_config/0` (same as test mail); on failure returns `{:error, :email_delivery_failed}`, error shown in JoinLive, logged for admin.
- [x] AshAuthentication senders: graceful error handling (no crash on delivery failure).
- [x] Gettext for all new UI strings, translated to German.
- [x] Docs and code guidelines updated.

---

## 13. Follow-up / Future Work

- **SMTP password at-rest encryption:** The `smtp_password` attribute is currently stored in plaintext in the `settings` table. It is excluded from default reads (same pattern as `oidc_client_secret`); both are read only via explicit select when needed. For production systems at-rest encryption (e.g. with [Cloak](https://hexdocs.pm/cloak)) should be considered and tracked as a follow-up issue.
- **Error classification:** SMTP error categorization currently uses substring matching on server messages (e.g. "535", "authentication"). A more robust approach would be to pattern-match on `gen_smtp` error tuples first where possible, and fall back to string analysis only when needed. Server wording varies; consider extending patterns as new providers are used.
