Sending Email with Mailgun
Sending email from a Phoenix application is really easy. Phoenix does not ship with a library to send emails, but there are various packages available that can help with that.
The options include Phoenix Swoosh and Bamboo.
If you are only going to use Mailgun, you can use the mailgun package instead:
Using the Mailgun package
Before we begin, we’ll need an account with Mailgun - we won’t actually be able to send mail without it. Once we have an account, though, the rest will be straightforward.
First, sign up at Mailgun. They have a generous number of free emails per month, so we can get going with a free account.
Once we have an account, we’ll get a sandbox through which we can send mail. The url of that sandbox will be our domain unless we choose to create a custom one through Mailgun.
Now that we have an account, we’ll need to add mailgun
as a dependency to our project. We’ll do that in the deps/0
function in mix.exs
.
defp deps do
[{:phoenix, "~> 1.2.0"},
{:phoenix_ecto, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.3"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:cowboy, "~> 1.0"},
{:mailgun, "~> 0.1.2"}]
end
Next, we’ll need to run mix deps.get
to bring the mailgun
package into our application. In the case of a dependency conflict error with Poison, add the following line as well:
{:poison, "~> 2.1", override: true}
Configuration
We’ll also need to configure our :mailgun_domain
and :mailgun_key
in config/config.ex
.
The :mailgun_domain
will be a full url, something like this https://api.mailgun.net/v3/sandbox-our-domain.mailgun.org
. The :mailgun_key
will be a long string - “key-another-long-string”.
For security reasons, it’s important to not commit these values to a public source code repository. There are a couple of ways we can accomplish this.
One way is quick, but it requires us to set environment variables for our :mailgun_domain
and :mailgun_key
in all of our environments - development, production, and whichever other environments we might define. With the environment variables set, we can reference them in our config/config.exs
file.
config :hello,
mailgun_domain: System.get_env("MAILGUN_DOMAIN"),
mailgun_key: System.get_env("MAILGUN_API_KEY")
There’s another way which doesn’t require environment variables for all environments, but is a little more complex to set up. This method mimics the way that config/prod.secret.exs
works by creating a config/config.secret.exs
file which will apply to all environments. We won’t be using prod.secret.exs
itself, because we will need these configuration values in development as well as production. Here goes.
The first thing we will do is add a line to the .gitignore
file for a new config/config.secret.exs
file. This will keep config.secret.exs
out of our git repository.
. . .
# The config/prod.secret.exs file by default contains sensitive
# data and you should not commit it into version control.
#
# Alternatively, you may comment the line below and commit the
# secrets file as long as you replace its contents by environment
# variables.
/config/prod.secret.exs
/config/config.secret.exs
The next step is to create the config/config.secret.exs
file with our mailgun
configuration in it.
use Mix.Config
config :hello,
mailgun_domain: "https://api.mailgun.net/v3/sandbox-our-domain.mailgun.org",
mailgun_key: "key-another-long-string"
Finally, we’ll need to import config.secret.exs
into our regular config/config.exs
file.
. . .
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"
import_config "config.secret.exs"
Since our config/config.secret.exs
file won’t be in our repository, we’ll need to take some extra steps when we deploy our application. Please see the Deployment Introduction Guide for more information.
The Client Module
In order for our application to interact with Mailgun, we’ll need a client module. Let’s define one here lib/hello/mailer.ex
. When we use
the Mailgun.Client
module in the second line, we pass our configuration to the mailgun
package, and we import mailgun
’s send_email/1
function into our mailer.
defmodule Hello.Mailer do
use Mailgun.Client,
domain: Application.get_env(:hello, :mailgun_domain),
key: Application.get_env(:hello, :mailgun_key)
end
Note The filesystem watcher does not monitor files in the
lib
directory for changes in order to recompile them. This means that if we update the mailer client, we’ll need to restart the server in order for those changes to take effect.
With this in place, we can start creating our custom email functions. Web applications may send any number of different types of emails - welcome emails after signup, password confirmations, activity notifications - the list goes on. For each type of email, we’ll define a new function which will call send_email/1
, passing in a keyword list of arguments.
Let’s say we want to send a welcome email to new users formatted as plain text. We’ll need to know who to send the email to, as well as the “from” address, subject, and body of the email. This will be sent as a plain text email because we’ve specified the :text
option.
def send_welcome_text_email(email_address) do
send_email to: email_address,
from: "us@example.com",
subject: "Welcome!",
text: "Welcome to Hello!"
end
Sending this email is as easy as invoking the function with an email address, from wherever we want to in our application.
Hello.Mailer.send_welcome_text_email("us@example.com")
Since we’re just getting started, it would be great to test this out locally without hitting Mailgun. The mailgun
package gives us a very easy way to do this. In the client module, we set the mode to :test
and provide a path to a file for mailgun
to write out the JSON representation of our emails.
Let’s add those to our client module at lib/hello/mailer.ex
.
defmodule Hello.Mailer do
use Mailgun.Client, domain: Application.get_env(:my_app, :mailgun_domain),
key: Application.get_env(:my_app, :mailgun_key),
mode: :test, # Alternatively use Mix.env while in the test environment.
test_file_path: "/tmp/mailgun.json"
. . .
end
Let’s try this out from iex
. We’ll use iex -S mix phoenix.server
in order to interact with a running Phoenix application. Once we’re in an iex
session, we can call our welcome email function, passing in the address we want to send the email to.
$ iex -S mix phoenix.server
. . .
iex> Hello.Mailer.send_welcome_text_email("us@example.com")
{:ok, "OK"}
In test mode, the send_mail/1
function will always return {:ok, "OK"}
.
Now, we can see the results in the output file.
$ more /tmp/mailgun.json
{"to":"us@example.com","text":"Welcome to Hello!","subject":"Welcome!","from":"Mailgun Sandbox <postmaster@sandbox-our-domain.mailgun.org>"}
We can send HTML emails as well. To do this, we can define a new function which uses an :html
key instead of :text
. The HTML value we use will need to be a string.
def send_welcome_html_email(email_address) do
send_email to: email_address,
from: "us@example.com",
subject: "Welcome!",
html: "<strong>Welcome to Hello</strong>"
end
Notice that we have some duplication here in the value of the “from” lines in both functions. We can fix that with a module attribute.
defmodule Hello.Mailer do
. . .
@from "us@example.com"
. . .
If we substitute our module attribute for the string in the :from
lines, our two functions will look like this.
def send_welcome_text_email(email_address) do
send_email to: email_address,
from: @from,
subject: "Welcome!",
text: "Welcome to Hello!"
end
def send_welcome_html_email(email_address) do
send_email to: email_address,
from: @from,
subject: "Welcome!",
html: "<strong>Welcome to Hello</strong>"
end
When we call the send_welcome_html_email/1
function, we get almost the same output, with the HTML content instead of the text content.
$ iex -S mix phoenix.server
iex> Hello.Mailer.send_welcome_html_email("us@example.com")
{:ok, "OK"}
Here’s the output in /tmp/mailgun.json
.
$ more /tmp/mailgun.json
{"to":"them@example.com","subject":"Welcome!","html":"<strong>Welcome to Hello Test</strong>","from":"Mailgun Sandbox <postmaster@sandbox-our-domain.mailgun.org>"}
For many email uses, it’s good to have clients try to render an HTML version first, then fall back to plain text if they are unable to do so. Let’s write a new send_welcome_email/1
function which will supersede the other two welcome email functions. In it, we’ll simply use both :text
and :html
options. This will produce a multi-part email with the text section separated from the HTML section. Each will appear in the order it is defined in the function.
def send_welcome_email(email_address) do
send_email to: email_address,
from: @from,
subject: "Welcome!",
text: "Welcome to Hello!",
html: "<strong>Welcome to Hello</strong>"
end
When we call our new function, this is what we get.
$ more /tmp/mailgun.json
{"to":"us@example.com","text":"Welcome to Hello!","subject":"Welcome!","html":"<strong>Welcome to Hello Test</strong>","from":"Mailgun Sandbox <postmaster@sandbox-our-domain.mailgun.org>"}
Let’s take our client out of test mode by removing the :mode
and :test_file_path
options.
defmodule Hello.Mailer do
use Mailgun.Client,
domain: Application.get_env(:hello, :mailgun_domain),
key: Application.get_env(:hello, :mailgun_key)
. . .
When we restart the application and call our send_welcome_email/1
function, we actually get a response back from Mailgun telling us our email has been queued.
iex> Hello.Mailer.send_welcome_email("us@example.com")
{:ok,
"{\n \"id\": \"<20150820050046.numbers.more_numbers@sandbox-our-domain.mailgun.org>\",\n \"message\": \"Queued. Thank you.\"\n}"}
Great! Time to check our inbox.
Looking at the original source of our email, we can see that it is indeed a multipart email with two parts. The first is our text email, with a Content-Type of “text/plain”. The second is our HTML email with a Content-Type of “text/html”.
To: them@example.com
From: Mailgun Sandbox
<postmaster@sandbox-our-domain.mailgun.org>
Subject: Welcome!
Mime-Version: 1.0
Content-Type: multipart/alternative; boundary="ab2eaf529cf8442b93154d6e3d98896e"
--ab2eaf529cf8442b93154d6e3d98896e
Content-Type: text/plain; charset="ascii"
Mime-Version: 1.0
Content-Transfer-Encoding: 7bit
Welcome to Hello!
--ab2eaf529cf8442b93154d6e3d98896e
Content-Type: text/html; charset="ascii"
Mime-Version: 1.0
Content-Transfer-Encoding: 7bit
<strong>Welcome to Hello Test</strong>
--ab2eaf529cf8442b93154d6e3d98896e--
Tidying Up
What we’ve written so far is fine, but for a real-world welcome email, we’re going to need more than a few words of text or a single HTML tag. With more text or HTML, though, our send_welcome_email/1
will become messy quite quickly. The solution is private functions which cordon off the complexity behind a descriptive name.
In our Hello.Mailer
module, we can define a private welcome_text/0
function which uses a heredoc to define a string literal for the text that makes up the body of our email.
. . .
defp welcome_text do
"""
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
"""
end
. . .
Now we can use it in our send_welcome_email/1
function.
def send_welcome_email(email_address) do
send_email to: email_address,
from: @from,
subject: "Welcome!",
text: welcome_text,
html: "<strong>Welcome to Hello</strong>"
end
If we’re going to render anything other than the simplest HTML while still having a readable send_welcome_email/1
function, using bare HTML strings is going to present problems as well. Rendering templates fixes that, but we need a string value for the :html
key. The Phoenix.View.render_to_string/3
function will do just what we need.
def send_welcome_email(email_address) do
send_email to: email_address,
from: @from,
subject: "Welcome!",
text: welcome_text,
html: Phoenix.View.render_to_string(Hello.EmailView, "welcome.html", %{})
end
To make this example work, we’ll need the same components that we would use to render any template in Phoenix.
First, we’ll need a basic Hello.EmailView
defined at lib/hello_web/views/email_view.ex
.
defmodule Hello.EmailView do
use Hello.Web, :view
end
We’ll also need a new email
directory in lib/hello_web/templates
with a welcome.html.eex
template in it.
<div class="jumbotron">
<h2>Welcome to Hello!</h2>
</div>
Note: If we need to use any path or url helpers in our template, we will need to pass the endpoint instead of a connection struct for the first argument. This is because we won’t be in the context of a request, so
@conn
won’t be available. For example, we will need to write this
alias Hello
Router.Helpers.page_url(Endpoint, :index)
instead of this.
Router.Helpers.page_path(@conn, :index)
If we have any other values we need to pass into the template, we can pass a map of them as the third argument to Phoenix.View.render_to_string/3
.
We can put the render call behind a private function as well, just as we did with welcome_text/0
.
. . .
defp welcome_html do
Phoenix.View.render_to_string(Hello.EmailView, "welcome.html", %{})
end
. . .
With that our send_welcome_email/1
function looks much nicer.
def send_welcome_email(email_address) do
send_email to: email_address,
from: @from,
subject: "Welcome!",
text: welcome_text,
html: welcome_html
end
Sending attachments
Mailgun also lets us send attachments with an email. We’ll use the :attachments
key to tell mailgun
that we want to include one or more of them. The value we give it needs to be a list of two element maps. One element of each map needs to be the path to a file we want to attach. The other needs to be the filename.
Sending new users a copy of the Phoenix framework logo with their welcome email would look like this.
def send_welcome_email(email_address) do
send_email to: email_address,
from: @from,
subject: "Welcome!",
text: welcome_text,
html: welcome_html,
attachments: [%{path: "priv/static/images/phoenix.png", filename: "phoenix.png"}]
end
If we put our mailer client back in test mode, restart our application, and call the send_welcome_email/1
function with our email address, we’ll see our attachment at the very end.
more mailgun.json
{"to":"us@example.com","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n","subject":"Welcome!","html":"<div class=\"jumbotron\">\n <h2>Welcome to Hello!</h2>\n</div>","from":"Mailgun Sandbox <postmaster@sandbox-our-domain.mailgun.org>","attachments":[{"path":"priv/static/images/phoenix.png","filename":"phoenix.png"}]}
Then we can take the mailer out of test mode and actually send it.