# Email settings: admin UI with multi-adapter support

Status: Complete

Depends on: [setup-auto-confirm.md](setup-auto-confirm.md) (provides `email_configured?/0` and warning banner)

## Context

After the setup wizard auto-confirm work, SMTP is configurable via env vars only. Self-hosted users shouldn't need to restart the server or touch env vars to configure email. This plan adds an admin settings page for email delivery with support for popular Swoosh adapters, stored in the Settings table with encrypted secrets.

## Design

### Precedence chain

```
Env vars (SMTP_HOST etc.) > DB settings > Local adapter (dev default)
```

If `SMTP_HOST` is set, env vars win — the admin UI shows the config as read-only with a note that it's controlled by environment variables. If no env vars, the admin can configure everything from the UI. If nothing is configured, falls back to `Swoosh.Adapters.Local`.

### Supported adapters

Start with the popular ones that cover 95%+ of use cases. Each has a simple config shape:

| Adapter | Module | Fields |
|---------|--------|--------|
| SMTP | `Swoosh.Adapters.SMTP` | relay (string), port (integer), username (string), password (secret) |
| Postmark | `Swoosh.Adapters.Postmark` | api_key (secret) |
| Resend | `Swoosh.Adapters.Resend` | api_key (secret) |
| SendGrid | `Swoosh.Adapters.Sendgrid` | api_key (secret) |
| Mailgun | `Swoosh.Adapters.Mailgun` | api_key (secret), domain (string) |
| Brevo | `Swoosh.Adapters.Brevo` | api_key (secret) |

Adding more later is just adding an entry to the adapter registry — no code changes needed.

### Adapter registry

A simple data structure defining each adapter's config shape:

```elixir
defmodule Berrypod.Mailer.Adapters do
  def all do
    [
      %{
        key: "smtp",
        name: "SMTP",
        module: Swoosh.Adapters.SMTP,
        fields: [
          %{key: "relay", label: "Server host", type: :string, required: true},
          %{key: "port", label: "Port", type: :integer, default: 587},
          %{key: "username", label: "Username", type: :string},
          %{key: "password", label: "Password", type: :secret}
        ]
      },
      %{
        key: "postmark",
        name: "Postmark",
        module: Swoosh.Adapters.Postmark,
        fields: [
          %{key: "api_key", label: "API key", type: :secret, required: true}
        ]
      },
      # ... etc
    ]
  end
end
```

The UI renders dynamically from this registry. Secret fields use `put_secret/2` and show masked hints via `secret_hint/1`. Non-secret fields use `put_setting/3`.

### Storage in Settings table

Uses the existing key-value Settings table:

| Key | Type | Example |
|-----|------|---------|
| `email_adapter` | string | `"postmark"` |
| `email_relay` | string | `"smtp.example.com"` |
| `email_port` | integer | `587` |
| `email_api_key` | encrypted | (encrypted via Vault) |
| `email_password` | encrypted | (encrypted via Vault) |
| `email_username` | string | `"user@example.com"` |
| `email_domain` | string | `"mg.example.com"` |

All keys are prefixed with `email_`. Only the fields relevant to the selected adapter are written; others are cleared on adapter change.

### Runtime config loading

On app startup and on settings save:
1. Read `email_adapter` from Settings
2. Look up adapter in registry
3. Read all config fields for that adapter from Settings
4. Build Swoosh config keyword list
5. Call `Application.put_env(:berrypod, Berrypod.Mailer, config)`

This is wrapped in a `Berrypod.Mailer.load_config/0` function, called from:
- `Application.start/2` (boot)
- The admin settings save handler (runtime update without restart)

### Updating `email_configured?/0`

The helper from setup-auto-confirm.md already checks if the adapter is not `Swoosh.Adapters.Local`. No changes needed — once the admin configures an adapter via the UI, `Application.get_env` returns the new adapter and the function returns `true`.

## Changes

### 1. Adapter registry (`lib/berrypod/mailer/adapters.ex` — new)

Module with `all/0`, `get/1`, `field_keys/1` functions. Pure data, no side effects.

### 2. Config loader (`lib/berrypod/mailer.ex`)

Add to existing module:
- `load_config/0` — reads from Settings, builds Swoosh config, calls `Application.put_env/3`
- `env_var_configured?/0` — returns true if `SMTP_HOST` is set (used by UI to show read-only state)
- `current_config/0` — returns the active adapter name + config for display in the UI

### 3. Load on boot (`lib/berrypod/application.ex`)

Call `Mailer.load_config/0` after Repo is started (needs DB access for Settings reads and secret decryption).

### 4. Admin email settings page (`lib/berrypod_web/live/admin/email_settings.ex` — new)

LiveView at `/admin/settings/email`:

- Adapter dropdown (from registry)
- Dynamic form fields based on selected adapter
- Secret fields show masked hints when a value exists, clear button, new value input
- "Send test email" button — sends a test email to the admin's address
- Read-only mode when env vars are active (show values from env, disable form)
- Save persists to Settings table, calls `Mailer.load_config/0` to apply immediately

### 5. Route (`lib/berrypod_web/router.ex`)

Add in the authenticated admin live_session:
```elixir
live "/admin/settings/email", Admin.EmailSettings
```

### 6. Link from warning banner

Update the warning banner from setup-auto-confirm to link to `/admin/settings/email`:
"Email delivery not configured. [Configure email ->](/admin/settings/email)"

### 7. Admin nav

Add "Email" under settings in the admin sidebar, or as a tab on the existing settings page.

### 8. Test email function (`lib/berrypod/mailer.ex`)

- `send_test_email/1` — sends a simple test email to the given address using current config. Returns `{:ok, _}` or `{:error, reason}`.

### 9. Tests

**`test/berrypod/mailer_test.exs` (new):**
- `load_config/0` reads from Settings and applies to Application env
- `load_config/0` with no settings keeps Local adapter
- `env_var_configured?/0` checks SMTP_HOST

**`test/berrypod/mailer/adapters_test.exs` (new):**
- Registry returns all adapters
- Each adapter has required fields

**`test/berrypod_web/live/admin/email_settings_test.exs` (new):**
- Page renders adapter dropdown
- Changing adapter shows correct fields
- Saving config persists to Settings
- Secret fields show masked hints
- Read-only when env vars are set

## Files to modify

| File | Change |
|------|--------|
| `lib/berrypod/mailer/adapters.ex` | New — adapter registry |
| `lib/berrypod/mailer.ex` | Add `load_config/0`, `env_var_configured?/0`, `current_config/0`, `send_test_email/1` |
| `lib/berrypod/application.ex` | Call `Mailer.load_config/0` on boot |
| `lib/berrypod_web/live/admin/email_settings.ex` | New — admin settings page |
| `lib/berrypod_web/router.ex` | Add email settings route |
| `lib/berrypod_web/components/layouts/admin.html.heex` | Link banner to settings page, add nav entry |
| `assets/css/admin/components.css` | Styles for email settings form |
| Tests (3 new files) | Mailer, adapters, LiveView |

## Verification

1. `mix precommit` passes
2. No env vars set: admin UI at `/admin/settings/email` shows adapter dropdown, can configure Postmark with API key, save, `email_configured?()` returns true, warning banner disappears
3. With `SMTP_HOST` set: admin UI shows SMTP config read-only with "controlled by environment variables" note
4. "Send test email" delivers to admin's address
5. Server restart: config reloaded from DB, email still works
6. Adapter change: old config cleared, new adapter's fields shown, save applies immediately
