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_sendermix phx.gen.html Mailing Message messages to:string subject:string body:string# lib/email_sender_web/router.ex
defmodule EmailSenderWeb.Router do
  ...
  scope "/", EmailSenderWeb do
    ...
    resources "/messages", MessageController
  end
endmix ecto.migrate# 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"}
  ]
endmix deps.get# config/dev.exs
...
config :email_sender, EmailSender.Mailer,
  adapter: Bamboo.LocalAdapter
# config/test.exs
...
config :email_sender, EmailSender.Mailer,
  adapter: Bamboo.TestAdapter# 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
...# lib/email_sender/mailer.ex
defmodule EmailSender.Mailer do
  use Bamboo.Mailer, otp_app: :email_sender
end# 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
endSince 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)# 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
endemail = 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
  ...
endThere 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.# 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# lib/email_sender_web/router.ex
defmodule EmailSenderWeb.Router do
  ...
  if Mix.env == :dev do
    forward "/sent_emails", Bamboo.EmailPreviewPlug
  end
end
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!