igmarin/elixir-phoenix-skills

swoosh-emails

Use when sending emails from Phoenix applications. Invoke before implementing email functionality. Covers Swoosh setup, email templates, delivery configuration, testing, and production adapters. Trigger words: email, Swoosh, mailer, email templates, SMTP, SendGrid, email testing.

First seen Jun 20, 2026

Installation

$ npx skills add igmarin/elixir-phoenix-skills --skill swoosh-emails

Also in this package

Other skills from igmarin/elixir-phoenix-skills · top by installs.

npx skills add igmarin/elixir-phoenix-skills

Browse all from igmarin/elixir-phoenix-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 0
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.0.0
LicenseMIT
More metadata
version
1.0.0
user-invocable
true

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 7,177 B
  • docs SUMMARY.md 301 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 15 installs

SKILL.md

Swoosh Emails

Canonical FP bar: [docs/fcis-engineering-rules.md](../../docs/fcis-engineering-rules.md) — Functional Core, Imperative Shell: pure domain modules; side effects at edges. HTTP/email/i18n adapters are edges; keep request building and response mapping pure where possible.

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: "[email protected]", from: "[email protected]", subject: "Test"))
# => {:ok, %{}} — mailer configured correctly
# => {:error, ...} — Finch missing from supervision tree or adapter misconfigured

Defining Emails

See [assets/mailertemplate.ex](assets/mailertemplate.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", "[email protected]"})
    |> 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: "[email protected]", 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 htmlbody/2 and textbody/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.getenv("SENDGRIDAPI_KEY") in runtime.exs
Assert delivery by inspecting logs Use Swoosh.TestAssertions — assertemailsent/1 and assertnoemail_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