berrypod/lib/berrypod/mailer.ex
jamey 3dca9ad9d0
All checks were successful
deploy / deploy (push) Successful in 1m2s
gate magic link login on verified email delivery
The login page now only shows the magic link form when a test email has
been sent successfully, not just when an adapter is configured. Saving
email settings or disconnecting clears the flag so the admin must
re-verify after config changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 22:25:27 +00:00

174 lines
4.9 KiB
Elixir

defmodule Berrypod.Mailer do
use Swoosh.Mailer, otp_app: :berrypod
alias Berrypod.Mailer.Adapters
alias Berrypod.Settings
@doc """
Returns whether a real email adapter is configured.
True when the adapter is anything other than `Swoosh.Adapters.Local`
(which just stores emails in memory for dev use).
"""
def email_configured? do
adapter = Application.get_env(:berrypod, __MODULE__)[:adapter]
adapter != nil and adapter != Swoosh.Adapters.Local
end
@doc """
Returns whether email delivery has been verified via a successful test email.
This is the flag the login page uses to decide whether to show the magic link
form. A configured adapter alone isn't enough — the admin must have sent a
test email that succeeded.
"""
def email_verified? do
email_configured?() and Settings.get_setting("email_verified", false) == true
end
@doc "Marks email delivery as verified (called after a successful test email)."
def mark_email_verified do
Settings.put_setting("email_verified", true, "boolean")
end
@doc "Clears the email verified flag (called when config changes)."
def clear_email_verified do
Settings.delete_setting("email_verified")
end
@doc """
Returns true if email is configured via environment variables (SMTP_HOST).
When env vars are active, the admin UI shows the config as read-only.
"""
def env_var_configured? do
System.get_env("SMTP_HOST") != nil
end
@doc """
Returns the current adapter key and config for display in the admin UI.
Returns `{adapter_key, config_map}` or `{nil, %{}}` if using the default.
"""
def current_config do
mailer_config = Application.get_env(:berrypod, __MODULE__, [])
adapter = mailer_config[:adapter]
case Enum.find(Adapters.all(), &(&1.module == adapter)) do
nil ->
{nil, %{}}
adapter_info ->
config =
for field <- adapter_info.fields, into: %{} do
settings_key = Adapters.settings_key(adapter_info.key, field.key)
value =
case field.type do
:secret -> Settings.secret_hint(settings_key)
_ -> Settings.get_setting(settings_key)
end
{field.key, value}
end
{adapter_info.key, config}
end
end
@doc """
Loads email config from the Settings table and applies it to Application env.
Env vars take precedence — if SMTP_HOST is set, this is a no-op since
runtime.exs already configured the adapter.
Called on boot (from Application.start) and after admin saves email settings.
"""
def load_config do
if env_var_configured?() do
:ok
else
case Settings.get_setting("email_adapter") do
nil ->
:ok
adapter_key ->
case Adapters.get(adapter_key) do
nil ->
:ok
adapter_info ->
config = build_config(adapter_info)
Application.put_env(:berrypod, __MODULE__, config)
# API-based adapters need a real HTTP client (dev defaults to false)
if adapter_info.module != Swoosh.Adapters.SMTP do
Application.put_env(:swoosh, :api_client, Swoosh.ApiClient.Req)
end
:ok
end
end
end
end
@doc """
Sends a test email to the given address using the current config.
"""
def send_test_email(to_address, from \\ nil) do
import Swoosh.Email
email =
new()
|> to(to_address)
|> from({"Berrypod", from || from_address()})
|> subject("Berrypod test email")
|> text_body("This is a test email from your Berrypod shop. Email delivery is working.")
deliver(email)
end
@doc "Returns the configured from address for outbound email."
def from_address do
Settings.get_setting("email_from_address") || "noreply@#{default_from_domain()}"
end
# Build Swoosh config keyword list from Settings for a given adapter
defp build_config(adapter_info) do
opts =
for field <- adapter_info.fields, reduce: [] do
acc ->
settings_key = Adapters.settings_key(adapter_info.key, field.key)
value =
case field.type do
:secret -> Settings.get_secret(settings_key)
_ -> Settings.get_setting(settings_key)
end
case {value, field} do
{nil, _} ->
acc
{val, %{type: :integer}} ->
[{String.to_atom(field.key), to_integer(val)} | acc]
{val, _} ->
[{String.to_atom(field.key), val} | acc]
end
end
# SMTP uses :relay, others use the native Swoosh key names
[{:adapter, adapter_info.module} | opts]
end
defp to_integer(val) when is_integer(val), do: val
defp to_integer(val) when is_binary(val), do: String.to_integer(val)
defp default_from_domain do
case Application.get_env(:berrypod, BerrypodWeb.Endpoint)[:url][:host] do
nil -> "example.com"
host -> host
end
end
end