See all articles

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.


Let’s first up create a Phoenix app:

1 2 3 4 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:

1 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:

1 2 3 4 5 6 7 8 # lib/email_sender_web/router.ex defmodule EmailSenderWeb.Router do ... scope "/", EmailSenderWeb do ... resources "/messages", MessageController end end

and migrate the database:

1 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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 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:

1 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:

1 2 3 4 5 6 7 8 # 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:

1 2 3 4 5 6 7 8 9 10 11 12 # 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:

1 2 3 4 # 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):

1 2 3 4 5 6 7 8 9 10 11 12 13 # 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", "<h1>Hello!</h1>") assert == "" 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:

1 ** (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:

1 2 3 4 5 6 7 8 9 10 11 # lib/email_sender/email.ex defmodule EmailSender.Email do import Bamboo.Email def create(to, subject, body) do new_email() |> to(to) |> from("") |> 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`:

1 2 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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 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:

1 2 3 4 5 6 7 8 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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 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) 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:

1 2 3 4 5 6 7 # 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!

Bulletproof your development with remote team augmentation

Build your product and prevent possible issues with development process.