SuperAPI AI Directory

swoosh-emails

Coding & development

View repository

Updated 10-15 22:17

About this skill

Swoosh Emails

RULES — Follow these with no exceptions

1. Define emails in separate modules — MyApp.Emails.UserEmail, not inline in contexts 2. Use Phoenix components for email templates — reuse UI components in emails 3. Configure delivery per environment — Local adapter in dev/test, real adapter in prod 4. Test emails with Swoosh.TestAssertions — assert emails were sent with correct content 5. Never send emails synchronously in web requests — use Oban for async delivery; Task.start only for simple cases

Setup

# mix.exs
defp deps do
  [
    {:swoosh, "~> 1.14"},
    {:finch, "~> 0.18"},
    {:gen_smtp, "~> 1.0"}
  ]
end

# application.ex
def start(_type, _args) do
  children = [
    # ...
    {Finch, name: MyApp.Finch}
  ]
  # ...
end

Validate Setup

After adding deps and configuring the supervision tree, confirm everything works before writing email modules:

# In iex -S mix (dev environment with Local adapter)
MyApp.Mailer.deliver(Swoosh.Email.new(to: "test@example.com", from: "noreply@myapp.com", subject: "Test"))
# => {:ok, %{}} — mailer configured correctly
# => {:error, ...} — Finch missing from supervision tree or adapter misconfigured

Defining Emails

See assets/mailer_template.ex for a copy-paste template with the Mailer module, an email-builder module, and an Oban delivery worker.

With Phoenix Components

# lib/my_app/emails/user_email.ex
defmodule MyApp.Emails.UserEmail do
  import Swoosh.Email
  import Phoenix.Component, only: [sigil_H: 2]
  alias MyAppWeb.EmailComponents

  def welcome(user) do
    assigns = %{user: user}

    html =
      ~H"""
      <EmailComponents.layout>
        <h1>Welcome, <%= @user.name %>!</h1>
        <p>Thanks for signing up.</p>
        <EmailComponents.button href={url(~p"/dashboard")}>Get Started</EmailComponents.button>
      </EmailComponents.layout>
      """
      |> Phoenix.HTML.Safe.to_iodata()
      |> IO.iodata_to_binary()

    new()
    |> to({user.name, user.email})
    |> from({"MyApp", "noreply@myapp.com"})
    |> subject("Welcome to MyApp!")
    |> html_body(html)
    |> text_body("Welcome, #{user.name}! Thanks for signing up.")
  end
end

Email Layout and Button Components

# lib/my_app_web/components/email_components.ex
defmodule MyAppWeb.EmailComponents do
  use Phoenix.Component

  def layout(assigns) do
    ~H"""
    <html>
      <body style="font-family: sans-serif; max-width: 600px; margin: auto;">
        <%= render_slot(@inner_block) %>
      </body>
    </html>
    """
  end

  def button(assigns) do
    ~H"""
    <a href={@href} style="background: #4F46E5; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none;">
      <%= render_slot(@inner_block) %>
    </a>
    """
  end
end

Mailer Module

# lib/my_app/mailer.ex
defmodule MyApp.Mailer do
  use Swoosh.Mailer, otp_app: :my_app
end

Configuration

# config/dev.exs
config :my_app, MyApp.Mailer,
  adapter: Swoosh.Adapters.Local

config :swoosh, serve: true

# config/test.exs
config :my_app, MyApp.Mailer,
  adapter: Swoosh.Adapters.Test

# config/runtime.exs (production)
config :my_app, MyApp.Mailer,
  adapter: Swoosh.Adapters.Sendgrid,
  api_key: System.get_env("SENDGRID_API_KEY")

Sending Emails

With Oban

# lib/my_app/workers/send_welcome_email.ex
defmodule MyApp.Workers.SendWelcomeEmail do
  use Oban.Worker, queue: :mailers, max_attempts: 3

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
    case Accounts.get_user(user_id) do
      nil ->
        {:cancel, "user #{user_id} not found"}

      user ->
        email = MyApp.Emails.UserEmail.welcome(user)

        case MyApp.Mailer.deliver(email) do
          {:ok, _} -> {:ok, :sent}
          {:error, reason} -> {:error, reason}
        end
    end
  end
end

# Enqueue from context
def register_user(attrs) do
  with {:ok, user} <- create_user(attrs) do
    %{user_id: user.id}
    |> MyApp.Workers.SendWelcomeEmail.new()
    |> Oban.insert()

    {:ok, user}
  end
end

With Task (Simple Cases Only)

def register_user(attrs) do
  with {:ok, user} <- create_user(attrs) do
    Task.start(fn ->
      user |> UserEmail.welcome() |> Mailer.deliver()
    end)

    {:ok, user}
  end
end

Testing Emails

defmodule MyApp.AccountsTest do
  use MyApp.DataCase, async: true

  import Swoosh.TestAssertions

  test "sends welcome email on registration" do
    attrs = %{email: "test@example.com", password: "password123"}

    assert {:ok, user} = Accounts.register_user(attrs)

    assert_email_sent(fn email ->
      assert email.to == [{user.name, user.email}]
      assert email.subject =~ "Welcome"
    end)
  end

  test "no email sent on failed registration" do
    attrs = %{email: "", password: ""}

    assert {:error, _changeset} = Accounts.register_user(attrs)

    assert_no_email_sent()
  end
end

Email Preview in Development

# config/dev.exs
config :swoosh, serve: true

# Access preview at http://localhost:4000/dev/mailbox

Common Pitfalls

❌ Don't ✅ Do
Build emails inline inside a context Define them in dedicated modules (MyApp.Emails.UserEmail)
Call Mailer.deliver/1 inside a web request Enqueue an Oban job (or Task.start for simple cases) so the request never blocks on SMTP
Ship an HTML-only email Always set both html_body/2 and text_body/2
Use a real adapter in dev/test Swoosh.Adapters.Local in dev, Swoosh.Adapters.Test in test, real adapter only in prod
Hardcode the API key in config Read it at runtime: System.get_env("SENDGRID_API_KEY") in runtime.exs
Assert delivery by inspecting logs Use Swoosh.TestAssertions — assert_email_sent/1 and assert_no_email_sent/0
Forget {Finch, name: MyApp.Finch} in the supervision tree Start Finch so API-based adapters have an HTTP client

Integration

Predecessor This Skill Successor
phoenix-auth-customization swoosh-emails oban-essentials
ecto-essentials swoosh-emails testing-essentials

Companion skills:

  • oban-essentials — deliver emails asynchronously with retries
  • testing-essentials — assert delivery with Swoosh.TestAssertions