mitgliederverwaltung/docs/smtp-configuration-concept.md
Simon eb18209669
All checks were successful
continuous-integration/drone/push Build is passing
feat: rearrange smtp settings
2026-03-13 15:56:02 +01:00

7.8 KiB
Raw Blame History

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 an ENV variable is set, the corresponding Settings field is read-only in the UI (with hint "Set by environment").


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

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).

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.

Both tls_options (STARTTLS, port 587) and sockopts (direct SSL, port 465) use the same verify mode. The logic is duplicated in config/runtime.exs (boot) and Mv.Mailer.smtp_config/0 (Settings-only); keep in sync.


12. Summary Checklist

  • ENV: SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_PASSWORD_FILE, SMTP_SSL.
  • ENV: MAIL_FROM_NAME, MAIL_FROM_EMAIL for sender identity.
  • Settings: attributes and UI for host, port, username, password, TLS/SSL, from-name, from-email.
  • Password from file: SMTP_PASSWORD_FILE supported in runtime.exs.
  • Mailer: Swoosh SMTP adapter configured from merged ENV + Settings when SMTP is configured.
  • Per-request SMTP config via Mv.Mailer.smtp_config/0 for Settings-only scenarios.
  • TLS certificate validation relaxed for OTP 27 (tls_options + sockopts).
  • Prod warning: clear message in Settings when SMTP is not configured.
  • Test email: form with recipient field, translatable content, classified success/error messages.
  • 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.
  • AshAuthentication senders: graceful error handling (no crash on delivery failure).
  • Gettext for all new UI strings, translated to German.
  • 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) 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.