Web Development

How to send emails from Phoenix in 4 easy steps

Nick Ciolpan

Nick Ciolpan

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:

  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
                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.

We partner closely with our clients

To create and support the strategic IT vision for their business.