How to send email from Phoenix in 4 easy steps

Nick Ciolpan
Jul 23 · 5 min read
Image for post
Image for post

Sooner or later your application will have to send out emails promoting its features or simply deliver notifications. Sending out emails is a core functionality of web applications nowadays.

In this article we’ll cover how to send e-mail using Phoenix and Elixir. We’ll assume you already have some experience with Phoenix and elixir. If this is not the case, we whole-heartedly recommend checking out the official Phoenix documentation and go through the Up and Running” tutorial before coming back here.

Usually, the Elixir community, refrains from recommending libraries, preferring low-level OTP approaches, such as gen_smtp. Our argument for using a high-level library, is the advantage of having many out-of-the-box solutions to a number of challenges such as queue management and templating.

This approach also has the advantage of implementing better error handling and a unified interface to interact with the rest of your application.

What do we need to consider before choosing one approach or the other? Well, our solution needs to:

  • Provide maintained drivers for the most popular email service providers like Mailgun, Mandrill or SendGrid.
  • Provide a way to send emails in the background and have the ability to schedule deliveries
  • Handle file attachments
  • Provide flexibility for email formatting

If you prefer to do the heavy lifting yourself, you can check out LibHunt. There are a couple of other viable options out there but for the scope of this tutorial we will use Thoughtbot’s Bamboo library.

Bamboo is probably the most popular solutions for Phoenix at the moment, and this is for good reasons:

  • It integrates nicely with Phoenix’s templating system

Let’s blade this.

1, Install the necessary dependencies

Add dependencies to mix.exs

def deps do
[{:bamboo, "~> 1.5"}]

And then, with the newly added dependencies, run:

Install dependencies via mix.exs

$ mix deps.get

Now let’s add the necessary lines in our configuration files:

Configure the adapter and api key — config/dev.exs

# config/dev.exs
config :my_app, MyApp.Mailer,
adapter: Bamboo.SendGridAdapter,
api_key: System.get_env("SENDGRID_API_KEY")

SENDGRID_API_KEY is a variable stored in our local .env file. Make sure you run source .env before starting up the application again.

The full list of adapters is available here.

2. Main files: emails and mailer

  1. We need to specify the contents of the emails we’d like to send. It allows us to define fields such as the sender, email subject, layouts, and template or attachments.
  2. We will need to define another module that will handle the sending of our previously crafted emails

Let’s first declare the mailer, as it’s the more compact of the two.

Creation of mailer.exs

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

Now let’s proceed with the definition of a base email function out of which all the following emails will be derived. We’re aiming to bulk up the base email with all recurring information or default settings.

Let’s start with a simple from field for now.

Creation of emails.exs

defp base_email() do
|> from("")

Now let’s use the base_email/0 function to define the actual email we want to send for our use case.

Define an email in the Bamboo way

defmodule MyApp.Email do
use Bamboo.Phoenix, view: MyApp.EmailView
def welcome_email(client) do
|> to(
|> subject("Welcome to MyApp. I'll be your guide")
|> put_html_layout({MyApp.LayoutView, "email.html"})
|> assign(:client, client)
|> render("welcome_new_client.html")
... end

One of my favorite features in Bamboo is the use of composable functions via pipes. You can still write your emails with the help of keyword lists but making use of Elixir’s pipe syntax is so much more fun.

Notice that we’ve added a put_html_layout function to the email. Here’s how the file looks like:


<head> </head>
<%= @inner_content %>

I’ve kept the layout basic on purpose. You can add your own CSS styles later.

We’ll also create a template for our email implementation. It will make use of the email layout above.


<p>Welcome, <%= %></p>

Again, our example is basic. You get the idea…

At some point, it will make more sense to move the layout function to the base email from which our particular email implementation is derived. Let’s do it.

Move put_html_layout/2 to base_email/0

# lib/my_app_web/templates/layout/email.html.eex
defp base_email() do
|> from("")
|> put_html_layout({MyApp.LayoutView, "email.html"})

We now have a working mailer in place.

3. Sending the email

Send emails — client_controller.ex

def create(conn, %{"client" => client_params}) do
case Agencies.create_client_for_agency(conn, client_params) do
{:ok, client} ->
# The important line is here
|> Email.welcome_email
|> Mailer.deliver_later
|> put_flash(:info, "Client created successfully.")
|> redirect(to: Routes.client_path(conn, :show, client))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)

As soon as a new record is created, we’ll proceed with sending the email and then return the conn like we would normally do.

Notice the deliver_later bit. In most scenarios, we don’t want the server to wait for the email to send. Bamboo smartly makes use of Eixir’s Task to handle this scenario. If for some reason you prefer to send emails synchronously, use deliver_now instead.

There you go, you’ve implemented a working mailer for your application. One that doesn’t block the server while it does its thing.

But don’t take our word for it. Let’s write a test to check whether or not our email has been sent. We can achieve this with the help of Bamboo.Test and Bamboo.TestAdapter.

4. Test

Test if the client receives email upon enrollment

defmodule MyApp.ClientEnrollmentTest do
use ExUnit.Case
use Bamboo.Test
@create_attrs %{email: "some email", name: "some name"}
test "after enrolling a new client, the client gets a welcome email" do
client = fixture(:client)
expected_email = MyApp.Email.welcome_email(client)
conn = post(conn, Routes.client_path(conn, :create), client: @create_attrs) assert_delivered_email expected_email
defp fixture(:client) do
{:ok, client} = Agencies.create_client(@create_attrs)

Great success! Make sure you check out the official Bamboo documentation, to learn about more advanced scenarios.

If we missed something, let’s start a conversation on Twitter.

Image for post
Image for post


We’re web development experts, partnering with startups and…

Nick Ciolpan

Written by

Co-founder of Graffino. I have extensive experience as a full stack developer, managing clients and building state of the art platforms.



We’re web development experts, partnering with startups and enterprises to create profitable digital products and ventures. 🚀

Nick Ciolpan

Written by

Co-founder of Graffino. I have extensive experience as a full stack developer, managing clients and building state of the art platforms.



We’re web development experts, partnering with startups and enterprises to create profitable digital products and ventures. 🚀

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store