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!