fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional, advanced) with category headings. fix phx-change clobbering text fields, async test email sending state, integer parse crash on bad port. add keyboard focus on card radios, fieldset legend, WCAG-compliant badge contrast, responsive grid. extract shared save_config into Mailer, add no-JS controller fallback with configured_adapter hidden field for adapter change detection. remove CardRadioScroll JS hook (no longer needed). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
defmodule Berrypod.Mailer do
|
||||
use Swoosh.Mailer, otp_app: :berrypod
|
||||
|
||||
alias Berrypod.KeyValidation
|
||||
alias Berrypod.Mailer.Adapters
|
||||
alias Berrypod.Settings
|
||||
|
||||
@@ -132,6 +133,117 @@ defmodule Berrypod.Mailer do
|
||||
Settings.get_setting("email_from_address") || "noreply@#{default_from_domain()}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates and persists email adapter configuration.
|
||||
|
||||
Trims values, validates required fields and key formats, clears settings
|
||||
from other providers, and applies config immediately.
|
||||
|
||||
Returns `{:ok, adapter_info}` on success or `{:error, field_errors}`
|
||||
where field_errors is a map of field_key => error_message.
|
||||
"""
|
||||
def save_config(adapter_key, params, fallback_from_email) do
|
||||
case Adapters.get(adapter_key) do
|
||||
nil ->
|
||||
{:error, %{"_base" => "Please select an email provider"}}
|
||||
|
||||
adapter_info ->
|
||||
params = trim_params(params)
|
||||
field_errors = validate_adapter_fields(adapter_info, params)
|
||||
|
||||
if field_errors == %{} do
|
||||
persist_adapter_config(adapter_info, params, fallback_from_email)
|
||||
{:ok, adapter_info}
|
||||
else
|
||||
{:error, field_errors}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_params(params) do
|
||||
Map.new(params, fn {k, v} -> {k, if(is_binary(v), do: String.trim(v), else: v)} end)
|
||||
end
|
||||
|
||||
defp validate_adapter_fields(adapter_info, params) do
|
||||
missing_errors =
|
||||
for field <- adapter_info.fields,
|
||||
field.required,
|
||||
val = params[field.key],
|
||||
is_nil(val) or val == "",
|
||||
# Secret fields can be left blank to keep existing value
|
||||
not (field.type == :secret and
|
||||
Settings.get_secret(Adapters.settings_key(adapter_info.key, field.key)) != nil),
|
||||
into: %{} do
|
||||
{field.key, "#{field.label} is required"}
|
||||
end
|
||||
|
||||
format_errors =
|
||||
for field <- adapter_info.fields,
|
||||
field.type == :secret,
|
||||
value = params[field.key],
|
||||
value != nil and value != "",
|
||||
{:error, message} <- [
|
||||
KeyValidation.validate_email_key(value, adapter_info.key, field.key)
|
||||
],
|
||||
into: %{} do
|
||||
{field.key, message}
|
||||
end
|
||||
|
||||
integer_errors =
|
||||
for field <- adapter_info.fields,
|
||||
field.type == :integer,
|
||||
value = params[field.key],
|
||||
is_binary(value) and value != "",
|
||||
match?(:error, Integer.parse(value)),
|
||||
into: %{} do
|
||||
{field.key, "#{field.label} must be a number"}
|
||||
end
|
||||
|
||||
missing_errors |> Map.merge(format_errors) |> Map.merge(integer_errors)
|
||||
end
|
||||
|
||||
defp persist_adapter_config(adapter_info, params, fallback_from_email) do
|
||||
# Clear settings from other providers
|
||||
new_keys = MapSet.new(Adapters.field_keys(adapter_info))
|
||||
|
||||
for key <- Adapters.all_field_keys(), key not in new_keys do
|
||||
Settings.delete_setting(key)
|
||||
end
|
||||
|
||||
# Save adapter type
|
||||
Settings.put_setting("email_adapter", adapter_info.key)
|
||||
|
||||
# Save field values (blank secrets keep existing value)
|
||||
for field <- adapter_info.fields do
|
||||
value = params[field.key]
|
||||
settings_key = Adapters.settings_key(adapter_info.key, field.key)
|
||||
|
||||
cond do
|
||||
value && value != "" ->
|
||||
case field.type do
|
||||
:secret -> Settings.put_secret(settings_key, value)
|
||||
:integer -> Settings.put_setting(settings_key, String.to_integer(value), "integer")
|
||||
_ -> Settings.put_setting(settings_key, value)
|
||||
end
|
||||
|
||||
field.type == :secret ->
|
||||
:keep
|
||||
|
||||
true ->
|
||||
Settings.delete_setting(settings_key)
|
||||
end
|
||||
end
|
||||
|
||||
# Auto-set from address to admin email if not configured
|
||||
if Settings.get_setting("email_from_address") in [nil, ""] do
|
||||
Settings.put_setting("email_from_address", fallback_from_email)
|
||||
end
|
||||
|
||||
# Require re-verification and apply immediately
|
||||
clear_email_verified()
|
||||
load_config()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Turns a raw delivery error into a user-friendly message.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user