All checks were successful
deploy / deploy (push) Successful in 1m2s
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>
174 lines
4.9 KiB
Elixir
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
|