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 for 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's easily testable, featuring both unit and integration tests
- It integrates nicely with Phoenix's templating system
Let's blade this.
#1 Install the necessary dependencies
First things first, we need to add Bamboo to the list of dependencies.
Add dependencies to mix.exs
def deps do
[{:bamboo, "~> 1.5"}]
end
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
Bamboo requires setting up at least two modules. These are in charge of email creation and email sending:
- 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.
- 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
new_email()
|> from("hello@myapp.io")
end
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
base_email()
|> to(client.email)
|> 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
...
end
One of our 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:
lib/my_app_web/templates/layout/email.html.eex
<html>
<head> </head>
<body>
<%= @inner_content %>
</body>
</html>
We'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.
lib/my_app_web/templates/email/welcome_client.html.eex
<p>Welcome, <%= @client.name %></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
new_email()
|> from("hello@myapp.io")
|> put_html_layout({MyApp.LayoutView, "email.html"})
end
We now have a working mailer in place.
#3 Sending the mail
We're now ready to send our email, as soon as a new client account has been created. We'll send it right from the client controller, the moment we have the confirmation that the record has successfully been inserted into the database.
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
client
|> Email.welcome_email
|> Mailer.deliver_later
conn
|> put_flash(:info, "Client created successfully.")
|> redirect(to: Routes.client_path(conn, :show, client))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
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
Let's first make sure we add Bamboo.TestAdapter to config/test.exs. Follow the instructions at step one.
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
end
defp fixture(:client) do
{:ok, client} = Agencies.create_client(@create_attrs)
client
end
end
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.