---
title: "Email"
---

# Email

Configure the SMTP server Govern sends verification, password reset, magic link and email change notices through, and know at once when delivery fails.

Govern sends verification, password reset, magic link and email change notices through one SMTP server. Without one (`NI_AUTH_SMTP_HOST` empty), nothing is sent and the request that needs mail answers `503 email_unavailable`.

## Settings

| Variable | Meaning |
|---|---|
| `NI_AUTH_SMTP_HOST` | Mail server host name. Empty: delivery is off (logged at start). `localhost` works only when a mail server listens on the Govern host or container |
| `NI_AUTH_SMTP_PORT` | Port, default `587` |
| `NI_AUTH_SMTP_STARTTLS` | `true` (the server's default): require STARTTLS before authenticating; a server without it is refused |
| `NI_AUTH_SMTP_USERNAME`, `NI_AUTH_SMTP_PASSWORD` | SMTP credentials (PLAIN, only over TLS or to localhost). Keep them in your secret store |
| `NI_AUTH_SMTP_FROM` | The `From` address; its domain must be one your provider lets you send from |
| `NI_AUTH_SMTP_INSECURE_SKIP_VERIFY` | Never in production |
| `NI_AUTH_SIGN_IN_URL` | Where the "Sign in" button of the email link pages points (https); empty: no button |

The Docker Compose stack points at its Mailpit container by default (`NI_AUTH_SMTP_HOST=mailpit`, port `1025`, STARTTLS off), which captures mail for local development at `http://localhost:8025`. Set real values for any other use.

## Example: Amazon SES

1. In the SES console of the region you send from, verify the domain of `NI_AUTH_SMTP_FROM` (DKIM records; add SPF and DMARC for deliverability).
2. Request production access: in the SES sandbox only verified addresses receive mail, and others are refused (`reason=rejected`).
3. Create SMTP credentials in the SES console. They are an SMTP user name and password, not IAM access keys.
4. Configure Govern:

   ```bash
   NI_AUTH_SMTP_HOST=email-smtp.eu-west-1.amazonaws.com
   NI_AUTH_SMTP_PORT=587
   NI_AUTH_SMTP_STARTTLS=true
   NI_AUTH_SMTP_USERNAME=<SES SMTP user name>
   NI_AUTH_SMTP_PASSWORD=<SES SMTP password>
   NI_AUTH_SMTP_FROM=no-reply@<verified domain>
   ```

5. Restart, then probe as a super admin: `POST /api/v1/admin/email/probe` must answer `status: ok`. Request a verification email for a test account.

Outbound TCP 587 to the SES endpoint must be allowed. In an air-gapped installation, point `NI_AUTH_SMTP_HOST` at your internal relay.

## Knowing whether mail works

- **Logs.** Every failed send is logged at error level with `event=email_delivery_failed`, a `reason` and a hint. Reasons: `not_configured`, `connect`, `timeout`, `tls`, `auth`, `rejected` (a `5xx`, for example an unverified sender), `temporary` (a `4xx`), `circuit_open` (sends paused after repeated failures), `other`. The recipient is logged by domain only.
- **Probe.** The mail server is probed at start and every 5 minutes: connect, STARTTLS, authenticate, quit. Nothing is sent.
- **Metrics** on `/metrics`: `keystone_email_deliveries_total{kind="send|probe",result="sent|failed",reason}` and `keystone_email_delivery_healthy` (1 or 0). Alert on `keystone_email_delivery_healthy == 0` for 10 minutes.
- **Readiness.** `GET /readyz` includes `"email": "ok" | "failing" | "unconfigured" | "unknown"`. It never makes the node unready.
- **Admin view.** `GET /api/v1/admin/email/health` (super admins) returns the status, host, port, STARTTLS, From, whether credentials are set, the last success and failure times, the last reason and the number of consecutive failures. `POST /api/v1/admin/email/probe` probes now and returns the same object; use it after a configuration change.
- **Circuit breaker.** Selfguard guards the mail server with the `smtp` breaker: an outage fails fast instead of holding requests, and a rejected recipient does not count as an outage. An open breaker raises a `dependency.breaker_open` finding; it closes by itself once a trial send succeeds.
