Announcing Bamboo, Email with a Functional Twist

Paul Smith

Bamboo is a testable, composable and adapter based email library for Elixir.

You might be thinking, what could possibly be so cool about sending email in Elixir? Can it be any better than what I’ve used in the past?

Hopefully we can show you what the problems were with existing solutions and how we solved them with features unique to Elixir.

Those pesky email addresses

Sometimes you only send to one person and all you need is the address. That’s pretty simple to do

Bamboo.Email.new_email(to: user.email)

But often times you want to also give the person’s name, or send to a list of people. Let’s take an example where users can subscribe to a post. When someone comments on a post, each subscribed user gets an email.

Typically you would need to do something like this:

def new_comment_on_post(comment, recipients) do
  recipients = for user <- recipients do
    # Emails can either be a string, or like in this example, a 2 item tuple.
    {user.name, user.email}
  end

  new_email
  |> to(recipients)
  |> subject("New comment on a post you are subscribed to")
  |> text_body("There is a new comment")
  |> html_body("There is a <strong>new comment</strong>")
end

You would have to do this every time you send an email to recipients. Instead, Bamboo lets you define a protocol for Bamboo.Formatter that will let Bamboo know how to format your recipients. It would look something like this.

defimpl Bamboo.Formatter, for: MyApp.User do
  def format_email_address(user, _opts) do
    {user.name, user.email}
  end
end

Now when you send an email you can just pass a single user, or list of users and Bamboo will format them correctly.

new_email
|> to(recipients) # Will format the users with the Bamboo.Formatter protocol

Defaults come for free

In a lot of libraries it can be hard to use things together, but with Bamboo and Elixir’s pipe operator, you get defaults for free. Set a default layout, default from address or whatever you need by using functions and Elixir’s much loved pipe operator.

defmodule MyApp.Email do
  import Bamboo.Email

  def welcome_email(recipient) do
    base_email
    |> to(recipient)
    |> subject("Welcome!")
    |> text_body("Welcome to the app")
  end

  defp base_email do
    new_email
    |> from("myapp@thoughtbot.com")
    |> put_header("Reply-To", "support@thoughtbot.com")
  end
end

Testing used to be a pain

Bamboo was designed to make unit and integration testing simple. Because composing emails is split from actually delivering the emails, unit testing is very straightforward.

defmodule MyApp.EmailTest do
  use ExUnit.Case

  test "welcome email" do
    user = %User{name: "Paul", email: "paul@gmail.com"}

    email = MyApp.Email.welcome_email(user)

    assert email.to == user
    assert email.subject == "This is your welcome email"
    # The =~ asserts that the left hand side contains the text on the right
    assert email.html_body =~ "Welcome to the app"
  end
end

Then when you want to integration test, you can assert that the welcome email was sent. Here’s an example of a controller for handling new registrations in Phoenix.

defmodule MyApp.RegistrationControllerTest do
  use MyApp.ConnCase
  use Bamboo.Test

  test "sends welcome email" do
    user_params = [name: "Paul", email: "paul@gmail.com"]

    post conn, registration_path(conn, :post), user_params

    newly_registered_user = Repo.get_by!(User, user_params)
    assert_delivered_email MyApp.Email.welcome_email(newly_registered_user)
  end
end

defmodule MyApp.RegistrationController do
  use MyApp.Web, :controller

  def create(conn, %{"user" => user_params}) do
    user = insert_user(user_params)

    MyApp.Email.welcome_email(user) |> MyApp.Mailer.deliver_later

    redirect(conn, to: "/")
  end
end

Bamboo comes with a EmailPreviewPlug for using in Phoenix or other Plug based frameworks. This lets you see emails that were sent when using Bamboo.LocalAdapter. This is great for trying out password reset features, or seeing how an email will look.

Flexibility is at the core

Adapters make it much easier to switch providers if something goes wrong, they shut down, or you can get a better price elsewhere.

Bamboo ships with adapters for Mandrill and Sendgrid. There are also third party adapters available, and building your own is straightforward for most services.

Keeps things fast

Studies show that speed directly correlates to customer satisfaction and spending. Let’s keep things fast.

Bamboo lets you easily send emails in the background without any dependencies outside of what comes with Elixir. Just add the Bamboo.TaskSupervisorStrategy to your app, and send with Mailer.deliver_later.

If you need something more robust you can easily create your own strategy for delivering later with Bamboo.DeliverLaterStrategy.

Give it a try

For more examples and in-depth documentation, see the docs on hex.pm.

Get started with Bamboo today, we think you’ll love it.