
Sending Email from a Phoenix App

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.
Configuration
Let’s first up create a Phoenix app:
mix local.hex
mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez
mix phx.new 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
end
end
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]
]
end
def deps do
[
...
{:bamboo, "~> 0.8"},
{:bamboo_smtp, "~> 1.4.0"}
]
end
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
end
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("user@test.com",
"test subject",
"<h1>Hello!</h1>")
assert email.to == "user@test.com"
assert email.subject == "test subject"
assert email.html_body =~ "Hello!"
end
end
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
new_email()
|> to(to)
|> from("me@example.com")
|> subject(subject)
|> html_body(body)
end
end
Now, in order to send a message from the app, we need to call the new create
method on the E
mailSender
.Email
module and pass it to our Mailer
:
email = EmailSender.Email.create("jdoe@mail.com", "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],
@create_attrs[:subject],
@create_attrs[:body])
end
...
end
...
end
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.to, message.subject, message.body)
EmailSender.Mailer.deliver_now(email)
conn
|> put_flash(:info, "Message created successfully.")
|> redirect(to: message_path(conn, :show, message))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
...
end
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
end
end
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!

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.

