From 8ae8d92df0c7e6da1d3ec9b6e4ad632565872060 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 16 Jun 2026 15:10:03 +0200 Subject: [PATCH] refactor(email): share build/deliver skeleton across join emails --- .../emails/join_already_member_email.ex | 24 +-------- .../emails/join_already_pending_email.ex | 24 +-------- lib/mv_web/emails/join_confirmation_email.ex | 24 ++------- lib/mv_web/emails/join_email.ex | 54 +++++++++++++++++++ 4 files changed, 61 insertions(+), 65 deletions(-) create mode 100644 lib/mv_web/emails/join_email.ex diff --git a/lib/mv_web/emails/join_already_member_email.ex b/lib/mv_web/emails/join_already_member_email.ex index fa309d8..a7ea54f 100644 --- a/lib/mv_web/emails/join_already_member_email.ex +++ b/lib/mv_web/emails/join_already_member_email.ex @@ -5,15 +5,9 @@ defmodule MvWeb.Emails.JoinAlreadyMemberEmail do Used for anti-enumeration: the UI shows the same success message; only the email informs the recipient. Uses the unified email layout. """ - use Phoenix.Swoosh, - view: MvWeb.EmailsView, - layout: {MvWeb.EmailLayoutView, "layout.html"} - - use MvWeb, :verified_routes - import Swoosh.Email use Gettext, backend: MvWeb.Gettext, otp_app: :mv - alias Mv.Mailer + alias MvWeb.Emails.JoinEmail @doc """ Sends the "already a member" notice to the given address. @@ -23,20 +17,6 @@ defmodule MvWeb.Emails.JoinAlreadyMemberEmail do def send(email_address) when is_binary(email_address) do subject = gettext("Membership application – already a member") - assigns = %{ - subject: subject, - app_name: Mailer.mail_from() |> elem(0), - locale: Gettext.get_locale(MvWeb.Gettext) - } - - email = - new() - |> from(Mailer.mail_from()) - |> to(email_address) - |> subject(subject) - |> put_view(MvWeb.EmailsView) - |> render_body("join_already_member.html", assigns) - - Mailer.deliver(email, Mailer.smtp_config()) + JoinEmail.deliver(email_address, "join_already_member.html", subject) end end diff --git a/lib/mv_web/emails/join_already_pending_email.ex b/lib/mv_web/emails/join_already_pending_email.ex index 17dc487..8d0c680 100644 --- a/lib/mv_web/emails/join_already_pending_email.ex +++ b/lib/mv_web/emails/join_already_pending_email.ex @@ -6,15 +6,9 @@ defmodule MvWeb.Emails.JoinAlreadyPendingEmail do Used for anti-enumeration: the UI shows the same success message; only the email informs the recipient. Uses the unified email layout. """ - use Phoenix.Swoosh, - view: MvWeb.EmailsView, - layout: {MvWeb.EmailLayoutView, "layout.html"} - - use MvWeb, :verified_routes - import Swoosh.Email use Gettext, backend: MvWeb.Gettext, otp_app: :mv - alias Mv.Mailer + alias MvWeb.Emails.JoinEmail @doc """ Sends the "application already under review" notice to the given address. @@ -24,20 +18,6 @@ defmodule MvWeb.Emails.JoinAlreadyPendingEmail do def send(email_address) when is_binary(email_address) do subject = gettext("Membership application – already under review") - assigns = %{ - subject: subject, - app_name: Mailer.mail_from() |> elem(0), - locale: Gettext.get_locale(MvWeb.Gettext) - } - - email = - new() - |> from(Mailer.mail_from()) - |> to(email_address) - |> subject(subject) - |> put_view(MvWeb.EmailsView) - |> render_body("join_already_pending.html", assigns) - - Mailer.deliver(email, Mailer.smtp_config()) + JoinEmail.deliver(email_address, "join_already_pending.html", subject) end end diff --git a/lib/mv_web/emails/join_confirmation_email.ex b/lib/mv_web/emails/join_confirmation_email.ex index 08f4ad3..5578d16 100644 --- a/lib/mv_web/emails/join_confirmation_email.ex +++ b/lib/mv_web/emails/join_confirmation_email.ex @@ -2,15 +2,10 @@ defmodule MvWeb.Emails.JoinConfirmationEmail do @moduledoc """ Sends the join request confirmation email (double opt-in) using the unified email layout. """ - use Phoenix.Swoosh, - view: MvWeb.EmailsView, - layout: {MvWeb.EmailLayoutView, "layout.html"} - use MvWeb, :verified_routes - import Swoosh.Email use Gettext, backend: MvWeb.Gettext, otp_app: :mv - alias Mv.Mailer + alias MvWeb.Emails.JoinEmail @doc """ Sends the join confirmation email to the given address with the confirmation link. @@ -31,22 +26,9 @@ defmodule MvWeb.Emails.JoinConfirmationEmail do confirm_url = url(~p"/confirm_join/#{token}") subject = gettext("Confirm your membership request") - assigns = %{ + JoinEmail.deliver(email_address, "join_confirmation.html", subject, %{ confirm_url: confirm_url, - subject: subject, - app_name: Mailer.mail_from() |> elem(0), - locale: Gettext.get_locale(MvWeb.Gettext), resend: Keyword.get(opts, :resend, false) - } - - email = - new() - |> from(Mailer.mail_from()) - |> to(email_address) - |> subject(subject) - |> put_view(MvWeb.EmailsView) - |> render_body("join_confirmation.html", assigns) - - Mailer.deliver(email, Mailer.smtp_config()) + }) end end diff --git a/lib/mv_web/emails/join_email.ex b/lib/mv_web/emails/join_email.ex new file mode 100644 index 0000000..c837be5 --- /dev/null +++ b/lib/mv_web/emails/join_email.ex @@ -0,0 +1,54 @@ +defmodule MvWeb.Emails.JoinEmail do + @moduledoc """ + Shared build/deliver skeleton for the join-flow emails (confirmation, + already-a-member, already-pending). + + Each concrete join email supplies only its subject, template, and any + email-specific assigns; this module builds the Swoosh email with the unified + layout and delivers it via `Mailer.deliver/2` with `Mailer.smtp_config/0`, + preserving the `{:ok, email}` / `{:error, reason}` contract. + """ + use Phoenix.Swoosh, + view: MvWeb.EmailsView, + layout: {MvWeb.EmailLayoutView, "layout.html"} + + import Swoosh.Email + + alias Mv.Mailer + + @doc """ + Builds and delivers a join-flow email. + + - `email_address` - recipient address + - `template` - the EmailsView template to render (e.g. `"join_confirmation.html"`) + - `subject` - already-translated subject line + - `extra_assigns` - email-specific assigns merged on top of the common ones + (`subject`, `app_name`, `locale`) + + Returns `{:ok, email}` on success, `{:error, reason}` on delivery failure. + """ + @spec deliver(String.t(), String.t(), String.t(), map()) :: + {:ok, Swoosh.Email.t()} | {:error, term()} + def deliver(email_address, template, subject, extra_assigns \\ %{}) + when is_binary(email_address) and is_binary(template) and is_binary(subject) do + assigns = + Map.merge( + %{ + subject: subject, + app_name: Mailer.mail_from() |> elem(0), + locale: Gettext.get_locale(MvWeb.Gettext) + }, + extra_assigns + ) + + email = + new() + |> from(Mailer.mail_from()) + |> to(email_address) + |> subject(subject) + |> put_view(MvWeb.EmailsView) + |> render_body(template, assigns) + + Mailer.deliver(email, Mailer.smtp_config()) + end +end