feat: add smtp settings
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon 2026-03-12 13:39:48 +01:00
parent c4135308e6
commit a4f3aa5d6f
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
23 changed files with 2424 additions and 152 deletions

View file

@ -451,40 +451,191 @@ defmodule Mv.Config do
def oidc_only_env_set?, do: env_set?("OIDC_ONLY")
# ---------------------------------------------------------------------------
# SMTP configuration (stubs for TDD ENV overrides Settings; see docs/smtp-configuration-concept.md)
# SMTP configuration ENV overrides Settings; see docs/smtp-configuration-concept.md
# ---------------------------------------------------------------------------
@doc "Returns SMTP host. ENV SMTP_HOST overrides Settings. Stub: always nil until implemented."
@doc """
Returns SMTP host. ENV `SMTP_HOST` overrides Settings.
"""
@spec smtp_host() :: String.t() | nil
def smtp_host, do: nil
def smtp_host do
smtp_env_or_setting("SMTP_HOST", :smtp_host)
end
@doc "Returns SMTP port (e.g. 587). ENV SMTP_PORT overrides Settings. Stub: always nil until implemented."
@doc """
Returns SMTP port as integer. ENV `SMTP_PORT` (parsed) overrides Settings.
Returns nil when neither ENV nor Settings provide a valid port.
"""
@spec smtp_port() :: non_neg_integer() | nil
def smtp_port, do: nil
def smtp_port do
case System.get_env("SMTP_PORT") do
nil ->
get_from_settings_integer(:smtp_port)
@doc "Returns SMTP username. ENV SMTP_USERNAME overrides Settings. Stub: always nil until implemented."
value when is_binary(value) ->
case Integer.parse(String.trim(value)) do
{port, _} when port > 0 -> port
_ -> nil
end
end
end
@doc """
Returns SMTP username. ENV `SMTP_USERNAME` overrides Settings.
"""
@spec smtp_username() :: String.t() | nil
def smtp_username, do: nil
def smtp_username do
smtp_env_or_setting("SMTP_USERNAME", :smtp_username)
end
@doc "Returns SMTP password. ENV SMTP_PASSWORD overrides SMTP_PASSWORD_FILE overrides Settings. Stub: always nil until implemented."
@doc """
Returns SMTP password.
Priority: `SMTP_PASSWORD` ENV > `SMTP_PASSWORD_FILE` (file contents) > Settings.
Strips trailing whitespace/newlines from file contents.
"""
@spec smtp_password() :: String.t() | nil
def smtp_password, do: nil
def smtp_password do
case System.get_env("SMTP_PASSWORD") do
nil -> smtp_password_from_file_or_settings()
value -> trim_nil(value)
end
end
@doc "Returns SMTP TLS/SSL mode (e.g. 'tls', 'ssl', 'none'). Stub: always nil until implemented."
defp smtp_password_from_file_or_settings do
case System.get_env("SMTP_PASSWORD_FILE") do
nil -> get_smtp_password_from_settings()
path -> read_smtp_password_file(path)
end
end
defp read_smtp_password_file(path) do
case File.read(String.trim(path)) do
{:ok, content} -> trim_nil(content)
{:error, _} -> nil
end
end
@doc """
Returns SMTP TLS/SSL mode string (e.g. 'tls', 'ssl', 'none').
ENV `SMTP_SSL` overrides Settings.
"""
@spec smtp_ssl() :: String.t() | nil
def smtp_ssl, do: nil
def smtp_ssl do
smtp_env_or_setting("SMTP_SSL", :smtp_ssl)
end
@doc "Returns true when SMTP is configured (e.g. host present). Stub: always false until implemented."
@doc """
Returns true when SMTP is configured (host present from ENV or Settings).
"""
@spec smtp_configured?() :: boolean()
def smtp_configured?, do: false
def smtp_configured? do
present?(smtp_host())
end
@doc "Returns true when any SMTP ENV variable is set (for Settings UI hint). Stub: always false until implemented."
@doc """
Returns true when any SMTP ENV variable is set (used in Settings UI for hints).
"""
@spec smtp_env_configured?() :: boolean()
def smtp_env_configured?, do: false
def smtp_env_configured? do
smtp_host_env_set?() or smtp_port_env_set?() or smtp_username_env_set?() or
smtp_password_env_set?() or smtp_ssl_env_set?()
end
@doc "Returns true if SMTP_HOST ENV is set."
@spec smtp_host_env_set?() :: boolean()
def smtp_host_env_set?, do: env_set?("SMTP_HOST")
@doc "Returns true if SMTP_PORT ENV is set."
@spec smtp_port_env_set?() :: boolean()
def smtp_port_env_set?, do: env_set?("SMTP_PORT")
@doc "Returns true if SMTP_USERNAME ENV is set."
@spec smtp_username_env_set?() :: boolean()
def smtp_username_env_set?, do: env_set?("SMTP_USERNAME")
@doc "Returns true if SMTP_PASSWORD or SMTP_PASSWORD_FILE ENV is set."
@spec smtp_password_env_set?() :: boolean()
def smtp_password_env_set?, do: env_set?("SMTP_PASSWORD") or env_set?("SMTP_PASSWORD_FILE")
@doc "Returns true if SMTP_SSL ENV is set."
@spec smtp_ssl_env_set?() :: boolean()
def smtp_ssl_env_set?, do: env_set?("SMTP_SSL")
# ---------------------------------------------------------------------------
# Transactional email sender identity (mail_from)
# ENV variables MAIL_FROM_NAME / MAIL_FROM_EMAIL take priority; fallback to
# Settings smtp_from_name / smtp_from_email; final fallback: hardcoded defaults.
# ---------------------------------------------------------------------------
@doc """
Returns the display name for the transactional email sender.
Priority: `MAIL_FROM_NAME` ENV > Settings `smtp_from_name` > `"Mila"`.
"""
@spec mail_from_name() :: String.t()
def mail_from_name do
case System.get_env("MAIL_FROM_NAME") do
nil -> get_from_settings(:smtp_from_name) || "Mila"
value -> trim_nil(value) || "Mila"
end
end
@doc """
Returns the email address for the transactional email sender.
Priority: `MAIL_FROM_EMAIL` ENV > Settings `smtp_from_email` > `nil`.
Returns `nil` when not configured (caller should fall back to a safe default).
"""
@spec mail_from_email() :: String.t() | nil
def mail_from_email do
case System.get_env("MAIL_FROM_EMAIL") do
nil -> get_from_settings(:smtp_from_email)
value -> trim_nil(value)
end
end
@doc "Returns true if MAIL_FROM_NAME ENV is set."
@spec mail_from_name_env_set?() :: boolean()
def mail_from_name_env_set?, do: env_set?("MAIL_FROM_NAME")
@doc "Returns true if MAIL_FROM_EMAIL ENV is set."
@spec mail_from_email_env_set?() :: boolean()
def mail_from_email_env_set?, do: env_set?("MAIL_FROM_EMAIL")
# Reads a plain string SMTP setting: ENV first, then Settings.
defp smtp_env_or_setting(env_key, setting_key) do
case System.get_env(env_key) do
nil -> get_from_settings(setting_key)
value -> trim_nil(value)
end
end
# Reads an integer setting attribute from Settings.
defp get_from_settings_integer(key) do
case Mv.Membership.get_settings() do
{:ok, settings} ->
case Map.get(settings, key) do
v when is_integer(v) and v > 0 -> v
_ -> nil
end
{:error, _} ->
nil
end
end
# Reads the SMTP password directly from the DB via an explicit select,
# bypassing the standard read action which excludes smtp_password for security.
defp get_smtp_password_from_settings do
query = Ash.Query.select(Mv.Membership.Setting, [:id, :smtp_password])
case Ash.read_one(query, authorize?: false, domain: Mv.Membership) do
{:ok, settings} when not is_nil(settings) ->
settings |> Map.get(:smtp_password) |> trim_nil()
_ ->
nil
end
end
end