defmodule BerrypodWeb.Admin.EmailSettings do use BerrypodWeb, :live_view alias Berrypod.KeyValidation alias Berrypod.Mailer alias Berrypod.Mailer.Adapters alias Berrypod.Settings @impl true def mount(_params, _session, socket) do env_locked = Mailer.env_var_configured?() {current_adapter, current_values} = Mailer.current_config() saved_adapter = Settings.get_setting("email_adapter") adapter_key = current_adapter || saved_adapter grouped = Adapters.grouped() {:ok, socket |> assign(:page_title, "Email settings") |> assign(:env_locked, env_locked) |> assign(:adapter_key, adapter_key) |> assign(:current_values, current_values) |> assign(:all_adapters, Adapters.all()) |> assign(:recommended_adapters, grouped[:all_email] || []) |> assign(:advanced_adapters, grouped[:advanced] || []) |> assign(:email_configured, Mailer.email_configured?()) |> assign(:selected_adapter, adapter_key && Adapters.get(adapter_key)) |> assign(:sending_test, false) |> assign(:test_result, if(Mailer.email_verified?(), do: :ok)) |> assign(:test_error, nil) |> assign(:test_retryable, false) |> assign(:from_checklist, false) |> assign(:field_errors, %{}) |> assign(:form, to_form(%{}, as: :email))} end @impl true def handle_params(params, _uri, socket) do {:noreply, assign(socket, :from_checklist, params["from"] == "checklist")} end defp load_adapter_values(adapter_key) do case Adapters.get(adapter_key) do nil -> %{} adapter_info -> for field <- adapter_info.fields, into: %{} do settings_key = Adapters.settings_key(adapter_key, field.key) value = case field.type do :secret -> Settings.secret_hint(settings_key) _ -> Settings.get_setting(settings_key) end {field.key, value} end end end @impl true def handle_event("change_adapter", %{"email" => %{"adapter" => key}}, socket) do values = load_adapter_values(key) {:noreply, socket |> assign(:adapter_key, key) |> assign(:selected_adapter, Adapters.get(key)) |> assign(:current_values, values) |> assign(:field_errors, %{}) |> assign(:test_result, nil) |> assign(:test_error, nil)} end def handle_event("save", %{"email" => params}, socket) do if socket.assigns.env_locked do {:noreply, put_flash(socket, :error, "Email config is controlled by environment variables")} else adapter_key = params["adapter"] adapter_info = Adapters.get(adapter_key) if adapter_info do save_adapter_config(socket, adapter_info, params) else {:noreply, put_flash(socket, :error, "Please select an email provider")} end end end def handle_event("send_test", _params, socket) do user = socket.assigns.current_scope.user socket = assign(socket, :sending_test, true) case Mailer.send_test_email(user.email, Mailer.from_address()) do {:ok, _} -> Mailer.mark_email_verified() {:noreply, socket |> assign(:sending_test, false) |> assign(:test_result, :ok) |> assign(:test_error, nil)} {:error, reason} -> {:noreply, socket |> assign(:sending_test, false) |> assign(:test_result, :error) |> assign(:test_error, Mailer.friendly_error(reason)) |> assign(:test_retryable, Mailer.retryable_error?(reason))} end end defp save_adapter_config(socket, adapter_info, params) do # Trim all values params = Map.new(params, fn {k, v} -> {k, if(is_binary(v), do: String.trim(v), else: v)} end) # Validate required fields missing = adapter_info.fields |> Enum.filter(& &1.required) |> Enum.filter(fn field -> val = params[field.key] empty = is_nil(val) or val == "" settings_key = Adapters.settings_key(adapter_info.key, field.key) # Secret fields can be left blank to keep existing value empty and not (field.type == :secret and Settings.get_secret(settings_key) != nil) end) # Build per-field errors for missing required fields missing_errors = for field <- missing, into: %{} do {field.key, "#{field.label} is required"} end # Validate secret field formats for known providers 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 field_errors = Map.merge(missing_errors, format_errors) if field_errors != %{} do {:noreply, assign(socket, :field_errors, field_errors)} else # 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 current adapter fields (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 already configured if Settings.get_setting("email_from_address") in [nil, ""] do Settings.put_setting("email_from_address", socket.assigns.current_scope.user.email) end # Config changed — require re-verification Mailer.clear_email_verified() # Apply config immediately Mailer.load_config() # Re-read current state {current_adapter, current_values} = Mailer.current_config() {:noreply, socket |> assign(:adapter_key, current_adapter) |> assign(:selected_adapter, Adapters.get(current_adapter)) |> assign(:current_values, current_values) |> assign(:email_configured, Mailer.email_configured?()) |> assign(:field_errors, %{}) |> assign(:test_result, nil) |> assign(:test_error, nil) |> put_flash(:info, "Settings saved — send a test email to check it works")} end end @impl true def render(assigns) do ~H"""
Controlled by environment variables
Email is configured via SMTP_HOST and related env vars.
Remove them to configure email from this page instead.
All of these have a free tier. Pick whichever you like.
<.external_link href={adapter.url} class="admin-link"> Sign up at {adapter.name} ↗ if you don't already have an account. It's free.
{adapter_fields_instruction(adapter)}
<%= for field <- adapter.fields do %> <.adapter_field_static field_def={field} value={if selected, do: @current_values[field.key]} disabled={!selected || @env_locked} error={if selected, do: @field_errors[field.key]} /> <% end %> <%= unless @env_locked do %>Test email sent to {@current_scope.user.email}. Check your inbox to confirm it arrived.
{@test_error}
Fix your settings above and reconnect, then try the test again.
<% end %>Send a test to {@current_scope.user.email} to check everything works.