defmodule BerrypodWeb.Admin.EmailSettings do use BerrypodWeb, :live_view 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 {: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(:provider_options, provider_options()) |> assign(:email_configured, Mailer.email_configured?()) |> assign( :from_address, Settings.get_setting("email_from_address") || socket.assigns.current_scope.user.email ) |> assign(:sending_test, false) |> assign(:form, to_form(%{}, as: :email))} end defp provider_options do Enum.map(Adapters.all(), fn adapter -> %{ value: adapter.key, name: adapter.name, description: adapter.description, tags: adapter.tags, url: adapter.url } end) end @impl true def handle_event("change_adapter", %{"email" => %{"adapter" => key}}, socket) do {:noreply, assign(socket, :adapter_key, key)} 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("disconnect", _params, socket) do if socket.assigns.env_locked do {:noreply, put_flash(socket, :error, "Email config is controlled by environment variables")} else # Clear all email settings Settings.delete_setting("email_adapter") for key <- Adapters.all_field_keys() do Settings.delete_setting(key) end # Reset to Local adapter Application.put_env(:berrypod, Mailer, adapter: Swoosh.Adapters.Local) {:noreply, socket |> assign(:adapter_key, nil) |> assign(:current_values, %{}) |> assign(:email_configured, false) |> put_flash(:info, "Email provider disconnected")} 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, socket.assigns.from_address) do {:ok, _} -> {:noreply, socket |> assign(:sending_test, false) |> put_flash(:info, "Test email sent to #{user.email}")} {:error, reason} -> {:noreply, socket |> assign(:sending_test, false) |> put_flash(:error, "Failed to send test email: #{inspect(reason)}")} end end defp save_adapter_config(socket, adapter_info, params) do # 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 == "" # Secret fields can be left blank to keep existing value empty and not (field.type == :secret and Settings.get_secret("email_#{field.key}") != nil) end) if missing != [] do labels = Enum.map_join(missing, ", ", & &1.label) {:noreply, put_flash(socket, :error, "Missing required fields: #{labels}")} else # Save adapter type Settings.put_setting("email_adapter", adapter_info.key) # Clear fields from other adapters current_keys = MapSet.new(Enum.map(adapter_info.fields, &"email_#{&1.key}")) for key <- Adapters.all_field_keys(), key not in current_keys do Settings.delete_setting(key) end # Save current adapter fields (blank secrets keep existing value) for field <- adapter_info.fields do value = params[field.key] settings_key = "email_#{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 # Save from address from_address = params["from_address"] || "" if from_address != "" do Settings.put_setting("email_from_address", from_address) end # 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(:current_values, current_values) |> assign(:from_address, from_address) |> assign(:email_configured, Mailer.email_configured?()) |> put_flash(:info, "Email settings saved")} end end @impl true def render(assigns) do ~H"""
<.header> Email settings <:subtitle> Configure how your shop sends email. Transactional providers only handle order confirmations and password resets. All email providers also support newsletters and marketing campaigns. <%= if @env_locked do %>
<.icon name="hero-lock-closed" class="size-5 text-amber-600 shrink-0 mt-0.5" />

Controlled by environment variables

Email is configured via SMTP_HOST and related env vars. Remove them to configure email from this page instead.

<% end %>
<.form for={@form} phx-change="change_adapter" phx-submit="save">
<.card_radio_group name="email[adapter]" value={@adapter_key} legend="Email provider" options={@provider_options} disabled={@env_locked} display={:tags} />
<%= for adapter <- @all_adapters do %> <% selected = @adapter_key == adapter.key %> <% end %>
<%= if @email_configured do %>

Test email

Send a test email to {@current_scope.user.email} to verify delivery works.

<% end %>
""" end attr :field_def, :map, required: true attr :value, :any, default: nil attr :disabled, :boolean, default: false defp adapter_field_static(%{field_def: %{type: :secret}} = assigns) do ~H"""
<.input name={"email[#{@field_def.key}]"} value="" type="password" label={@field_def.label} autocomplete="off" placeholder={if @value, do: @value, else: ""} required={@field_def.required && !@value} disabled={@disabled} /> <%= if @value && !@disabled do %>

Current: {@value} — leave blank to keep existing value

<% end %>
""" end defp adapter_field_static(%{field_def: %{type: :integer}} = assigns) do ~H""" <.input name={"email[#{@field_def.key}]"} value={@value || @field_def.default || ""} type="number" label={@field_def.label} required={@field_def.required} disabled={@disabled} /> """ end defp adapter_field_static(assigns) do ~H""" <.input name={"email[#{@field_def.key}]"} value={@value || @field_def.default || ""} type="text" label={@field_def.label} required={@field_def.required} disabled={@disabled} /> """ end end