Sending Email from a Phoenix App

iRonin IT Team - Experts in software development
back-end development, elixir, phoenix, web

A step-by-step guide to sending emails from a Phoenix web app (Elixir). If you don’t have any on hand - don’t worry, we will show you also how to create a Phoenix app, then how to write your first mailer to be sent from it and how to preview emails during development. Let’s go!

Phoenix is gaining traction as a highly useful web application framework, built on the Elixir language. Today we want to take you through a simple example where you can send email directly from a Phoenix app. It’s all part of learning to embrace new technologies designed to make your web applications easier and more efficient to build.

For sending emails from a Phoenix web app we will use the bamboo library. First, let’s look at the configuration process and walk through creating a simple Phoenix web app - in case you don’t have one you can follow our guide with code snippets.


Let’s first up create a Phoenix app:

mix local.hex
mix archive.install
mix email_sender
cd email_sender

If you are creating an app for the first time, make sure your database config is correct - check config/{dev.exs|test.exs} files and look for the config :email_sender, EmailSender.Repo line.

The next thing to do is to generate a mail resource for our app:

mix phx.gen.html Mailing Message messages to:string subject:string body:string

Let’s then add our new resource’s endpoint to the router so we can perform basic CRUD operations (Create, Read, Update, Delete) on it:

# lib/email_sender_web/router.ex

defmodule EmailSenderWeb.Router do

  scope "/", EmailSenderWeb do
    resources "/messages", MessageController

and migrate the database:

mix ecto.migrate

This way when you open /messages you will see a simple interface to manage your message records.

In order to send emails we need to add the bamboo library to our dependencies in the mix.exs file:

# mix.exs
def application do
    mod: {EmailSender.Application, []},
    extra_applications: [:logger, :bamboo, :bamboo_smtp, :runtime_tools]

def deps do
    {:bamboo, "~> 0.8"},
    {:bamboo_smtp, "~> 1.4.0"}

Now we can install the new dependencies:

mix deps.get

We also need to configure our application to use the Bamboo.LocalAdapter adapter for sending emails in our dev environment and the Bamboo.TestAdapter adapter in our test environment:

# config/dev.exs


config :email_sender, EmailSender.Mailer,
  adapter: Bamboo.LocalAdapter

# config/test.exs


config :email_sender, EmailSender.Mailer,
  adapter: Bamboo.TestAdapter

For our production config you can use the Bamboo.SMTPAdapter adapter, but remember about using environment variables for specifying SMTP credentials:

# config/prod.exs


config :email_sender, EmailExample.Mailer,
  adapter: Bamboo.SMTPAdapter,
  server: System.get_env("SMTP_SERVER") ,
  port: 1025,
  username: System.get_env("SMTP_USERNAME"),
  password: System.get_env("SMTP_PASSWORD"),
  tls: :if_available, # can be `:always` or `:never`
  ssl: false, # can be `true`
  retries: 1


Now we need to create our mailer module, that we previously specified in the config files:

# lib/email_sender/mailer.ex
defmodule EmailSender.Mailer do
  use Bamboo.Mailer, otp_app: :email_sender

First mailer

We now have a Phoenix web app ready, so we can move on to writing our first mailer. Let’s start by writing a test first so we can progress in the proven style of TDD (Test Driven Development):

# test/email_sender/email_test.exs
defmodule EmailSender.EmailTest do
  use ExUnit.Case
  use Bamboo.Test

  test "create" do
    email = EmailSender.Email.create("",
                                     "test subject",
    assert == ""
    assert email.subject == "test subject"
    assert email.html_body =~ "Hello!"

Since an email is just a struct it’s easy to test it. We can check the email’s content using the =~ operator, which compares if the html_body contains the text specified on the right of the operator.

If we run our tests with the mix test command, we should see an error similar to the one below:

** (UndefinedFunctionError) function EmailSender.Email.create/3 is undefined (module EmailSender.Email is not available)

Let’s make the test pass by implementing a simple mailer:

# lib/email_sender/email.ex
defmodule EmailSender.Email do
  import Bamboo.Email

  def create(to, subject, body) do
    |> to(to)
    |> from("")
    |> subject(subject)
    |> html_body(body)

Now, in order to send a message from the app, we need to call the new create method on the EmailSender.Email module and pass it to our Mailer:

email = EmailSender.Email.create("", "Hello mail", "<h1>Hi Joe</h1>")
EmailSender.Mailer.deliver_now(email) # or EmailSender.Mailer.deliver_later(email)

Let’s run our tests again to ensure we made our test pass. Did yours pass? Ours did - so far so good.

Now we need to connect our Email with the Message record in the controller, so an email will be sent whenever a new message is created in the system.

Like before, we will start with test-first approach.

Let’s add a new test to the EmailSenderWeb.MessageControllerTest module:

# test/email_sender_web/controllers/message_controller_test.exs

defmodule EmailSenderWeb.MessageControllerTest do
  use EmailSenderWeb.ConnCase
  use Bamboo.Test


  describe "create message" do

    test "email is sent when data is valid", %{conn: conn} do
      post conn, message_path(conn, :create), message: @create_attrs

      assert_delivered_email EmailSender.Email.create(@create_attrs[:to],



If we run our tests again, we will see a failing test:

There were 0 emails delivered to this process.

     If you expected an email to be sent, try these ideas:

     1) Make sure you call deliver_now/1 or deliver_later/1 to deliver the email
     2) Make sure you are using the Bamboo.TestAdapter
     3) Use shared mode with Bamboo.Test. This will allow Bamboo.Test
     to work across processes: use Bamboo.Test, shared: :true
     4) If you are writing an acceptance test through a headless browser, use
     shared mode as described in option 3.

Let’s make it green by updating the create action in the controller:

# lib/email_sender_web/controllers/message_controller.ex

defmodule EmailSenderWeb.MessageController do
  def create(conn, %{"message" => message_params}) do
    case Mailing.create_message(message_params) do
      {:ok, message} ->
        email = EmailSender.Email.create(, message.subject, message.body)

        |> put_flash(:info, "Message created successfully.")
        |> redirect(to: message_path(conn, :show, message))
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)

Now we should see all our tests pass again.

Email preview during development

Let’s not stop there, but instead broaden our options - by adding a feature that previews sent emails while developing the app, by adding an additional route to our router:

# lib/email_sender_web/router.ex

defmodule EmailSenderWeb.Router do

  if Mix.env == :dev do
    forward "/sent_emails", Bamboo.EmailPreviewPlug

When we create a new message, the new email should appear in the /sent_emails mailbox:

Note: Remember that Bamboo.LocalAdapter must be used to make the email appear in the mailbox.

And that’s it! Your first mailer from a Phoenix web app is ready to be sent. Pretty easy, right? If you need other IT solutions or help in creating mobile or web apps - contact us! We’d love to see Elixir and Phoenix in more projects and are keen to help out!

Author's Bio
iRonin IT Team

Experts in software development

We are a 100% remote team of software development experts, providing web & mobile application development and DevOps services for international clients.

Similar articles

Bulletproof your development with remote team augmentation

Read how
This page is best viewed in portrait mode
Our websites and web services use cookies. We use cookies and collected data to enhance your experience, provide additional communication channels, improve marketing materials and enhance our offer. IRONIN SP. Z O.O. SP. K. is committed to protecting all the data that we collect or process in any way, especially data of personal nature. By accepting these terms you agree to our usage of cookies and processing your data, according to our Privacy Policy, and you declare that your browser settings reflect your preferences. Read more You have the right to revoke this agreement at any time, based on the terms of our Privacy Policy. You can change cookies settings in your browser. If you do not agree with us using cookies and processing your data, please change your cookies settings in your web browser and reject these terms. You can find more information about cookies, your data privacy This site uses cookies. By continuing to browse the site, you are agreeing to our use of cookies. data processing, and your rights in our Privacy Policy.