How to use Bamboo with custom layouts in Phoenix

May 21, 2020

In this post I will show you just how easy it is to set up Bamboo correctly from the start so that you can get off the ground running with sending emails with fully customizable layouts and templates.

Bamboo is a simple to use mailer and email builder for your Elixir App.

Here’s the break down of what we will do in this post:

  • New up a phoenix 1.5.0 app from scratch
  • Add bamboo to the project
  • Configure bamboo for local development
  • Scaffold a basic page action to later create and send an email
  • Define our email module for building emails
  • Setup our Views for both the layout and email
  • Add Templates for both the layout and email
  • Add mailer_view macro to our app.
  • Build an email and view it in our console logs
  • Send the newly created email with our mailer and view it in the browser

I will try my best to show you step by step while also keeping it quick. You should be able to complete all of this in around a half-hour.

lets get started

New up phoenix app

Let get started by creating a basic phoenix app.

mix basic_mailer_app --no-ecto

Using the --no-ecto we forgo the need to set up our db since this example won’t need any ecto models.

Hit enter when presented with Fetch and install dependencies? [Yn] and wait as we install the deps and npm stuff.

Now we cd into our directory. $ cd basic_mailer_app

Add bamboo to mix and pull deps

From here we open our code editor and add the bamboo dep to our mix.exs file.

# mix.exs

defp deps do
  {:phoenix, "~> 1.5.1"},
  {:bamboo, "~> 1.5"}

Next, run fetch the deps via mix deps.get to install bamboo.

Configure bamboo’s LocalAdapter

One of the great things about Bamboo is support for 3rd party APIs via adapters.
See for more info about available adapters.

Bamboo has many adapters, the two I find the most useful are the Bamboo.LocalAdapter and the Bamboo.TestAdapter

We are going to use Bamboo.LocalAdapter so that we can view the emails sent in our console logs and via our browser.

Open up your config/config.exs and add the following just above the
import_config "#{Mix.env()}.exs" and make it look like:

# config/config.exs

config :basic_mailer_app, BasicMailerAppWeb.Mailer,
  adapter: Bamboo.LocalAdapter,
  # optional
  open_email_in_browser_url: "http://localhost:4000/sent_emails"

# 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"

In this config, we configure our (yet to be created mailer BasicMailerAppWeb.Mailer) to use the Bamboo.LocalAdapter when sending emails and to open_email_in_browser_url with a given endpoint after sending emails.

Next we set up the sent_emails endpoint so we can view emails in our browser after they have been sent with the Bamboo.LocalAdapter.

Open the router and add this little bit of code just below your :api pipeline.

# lib/basic_mailer_app_web/router.ex

pipeline :api do
  plug :accepts, ["json"]

if Mix.env() == :dev do
    forward "/sent_emails", Bamboo.SentEmailViewerPlug

This condition checks Mix to see what our current env is, given that its :dev we forward our sent_emails request to Bamboo.SentEmailViewerPlug. Basically, this only works on the development environment

At this point, we should be able to spin up the app mix phx.server and navigate to http://localhost:4000/sent_emails and we should see a message No emails sent.

Setup basic page action

Now that we have a place to see emails once they have been sent and we have configured our Bamboo.LocalAdapter, let’s scaffold the controller actions where we will later create and send an email. For now its just going to act as a placeholder.

Open up your page controller and add this new action.

# lib/basic_mailer_app_web/controllers/page_controller.ex

def send_email(conn, _params) do
  render(conn, "send_email.html")

Next lets add the route. Add get "/send_email", PageController, :send_email like it’s shown below.

# lib/basic_mailer_app_web/router.ex

scope "/", BasicMailerAppWeb do
  pipe_through :browser

  get "/send_email", PageController, :send_email

  get "/", PageController, :index

After that lets clean up the templates real quick.

Replace everything in /lib/basic_mailer_app_web/templates/page/index.html.eex with the contents below.

# lib/basic_mailer_app_web/templates/page/index.html.eex
<section class="phx-hero">
  <%= link "Send Email", to: "/send_email" %>

Next create the template for our new send_email action. Create the following file under lib/basic_mailer_app_web/templates/page/send_email.html.eex

# lib/basic_mailer_app_web/templates/page/send_email.html.eex

<section class="phx-hero">
  <h1>Email Sent</h1>

Nothing special here, just something to render given you navigate to that endpoint.

If you navigated to the homepage via http://localhost:4000 you should now just see a link that states Send Email Given you click it, you see text that’s states Email Sent

But, we have yet to create an email let alone send one. That’s because we are only rendering our template in the controller action at the moment.

We still need to set up our email module to build an email to send in the first place. Let’s hold off on our controller action for the moment and move to build out the email module that we will use to create the shape of our data.

Add email module

Create our Email module here lib/basic_mailer_app_web/email.ex and copy in the content below.

# lib/basic_mailer_app_web/email.ex
defmodule BasicMailerAppWeb.Email do
  @moduledoc """
  This module builds emails and renderes their layouts

  use Bamboo.Phoenix, view: BasicMailerAppWeb.EmailView
  import Bamboo.Email

  def random_email() do
    |> put_layout({BasicMailerAppWeb.EmailLayoutView, :layout})
    |> from({"My Basic Mailer App", ""})
    |> to({"Some Random Recipient", ""})
    |> subject("Just a random email")
    |> render(:random_email)


One of the first things we do is use Bamboo.Phoenix macros and provide it a (yet to be made) custom EmailView. After that, we import our Bamboo.Email module to give us access to the new_email function.

From there we create just a plain old function. We can call this whatever you want. I decided on random_email

Going into that said function we start a simple pipeline for building our email.

  • We kick the whole thing off by creating an empty email via new_email
  • Then we put_layout to set our layout View module and layout View template.
  • Next we set the from and to using a tuple where the first key is the name, the last key is the email.
  • Moving on we set the subject
  • And last we render our :random_email template for our custom BasicMailerAppWeb.EmailView module.

Add Views for both the layout and email

Next: we create the Views for both our Layout and our template.

# lib/basic_mailer_app_web/views/email_layout_view.ex

defmodule BasicMailerAppWeb.EmailLayoutView do
  @moduledoc """
  used for the email layout

  use BasicMailerAppWeb, :mailer_view
# lib/basic_mailer_app_web/views/email_view.ex

defmodule BasicMailerAppWeb.EmailView do
  @moduledoc """
  used for the email template

  use BasicMailerAppWeb, :mailer_view

Add Templates for both the layout and email

First the contents of our emails in the templates

# lib/basic_mailer_app_web/templates/email/random_email.html.eex

  Just some random email in HTML
# lib/basic_mailer_app_web/templates/email/random_email.text.eex

Just some random email in Text 

Then our layouts for said emails.

# lib/basic_mailer_app_web/templates/email_layout/layout.html.eex

<h1> Some Email Layout for HTML</h1>
<%= @inner_content %> 
# lib/basic_mailer_app_web/templates/email_layout/layout.text.eex

Some Email Layout for Text
<%= @inner_content %> 

Note the use of <%= @inner_content %> this is a newer Phoenix 1.5+ thing.

Add mailer_view macro

Because both our Template and Layout use a macro mailer_view we need to define that. This allows us to specify our Namespace and path as well as use the Phoenix.HTML macro.

# lib/basic_mailer_app_web.ex

def mailer_view do
  quote do
    use Phoenix.View,
      root: "lib/basic_mailer_app_web/templates",
      namespace: BasicMailerAppWeb

    use Phoenix.HTML


Build email and view it

At this point, we are ready to see our emails.

First, we will build an email from the page controller then we will use IO.inspect() to print our email out to the console.

# lib/basic_mailer_app_web/controllers/page_controller.ex

def send_email(conn, _params) do

+  # Create and send the email
+  BasicMailerAppWeb.Email.random_email()
+  |> IO.inspect()

  render(conn, "send_email.html")

Now clicking our button should print our email to the console logs. After we confirm this is working in our logs then it’s time to send the email.

Create and use mailer

# lib/basic_mailer_app_web/controllers/page_controller.ex

   # Create and send the email
- |> IO.inspect()
+ |> BasicMailerAppWeb.Mailer.deliver_now()

   render(conn, "send_email.html")

Clicking the button now should cause the browser to open a new window and you should now be able to view both the html and text versions of your emails. At this point you have just completed setting up Bamboo’s mailer to use a custom layout and template and you are ready to send the email for real via another smtp / 3rd party adapter.

Best of luck and thanks for reading.
– Josh Chernoff.

Photo by: Jose Mella