Video

Want to see the full-length video right now for free?

Notes

Elixir & Phoenix, so hot right now! On this week's episode we'll take a look at some of the great features of both Elixir and Phoenix through the lens of Bamboo, a library for sending emails in Phoenix apps, recently released by thoughtbot developer Paul Smith.

Compositional building of emails

Elixir is a functional programming language, and Bamboo embraces and encourages this functional nature in its design. The default way to build an email using Bamboo is with the new_email function, to which you pass a struct defining how to build the email:

new_email(
  to: "john@gmail.com",
  from: "support@myapp.com",
  subject: "Welcome to the app.",
  html_body: "<strong>Thanks for joining!</strong>",
  text_body: "Thanks for joining!"
)

This certainly works, but with with Bamboo you can take advantage of Elixir's pipe operator to build the email in multiple steps, passing the resulting output of each previous step on to the next:

new_email
  |> to("foo@example.com")
  |> from("me@example.com")
  |> subject("Welcome!!!")
  |> html_body("<strong>Welcome</strong>")
  |> text_body("welcome")

This approach makes it both very easy and very clear to manage things like default mailer settings, without having to use more complex constructs like Rails' class macros. Everything behaves the same way, and any additional functionality is added by simply creating a new function that takes in an email as an input, and produces a new email based on the input.

Formatting with Protocols

Another Elixir feature that Bamboo takes advantage of is protocols. Briefly, an Elixir protocol is an interface that data can conform to, allowing for polymorphism in our Elixir code.

In Bamboo, the Bamboo.Formatter defines an interface for converting data, for instance a User struct, into an email address. The implementation would look something like:

defimpl Bamboo.Formatter, for: MyApp.User do
  # Used by `to`, `bcc`, `cc` and `from`
  def format_email_address(user, _opts) do
    fullname = "#{user.first_name} #{user.last_name}"
    {fullname, user.email}
  end
end

Here we've defined the format_email_address function implementation for our User struct, allow Bamboo to automatically format email address when sending messages. Internally, Bamboo will then call out to the format_email_address, allowing our custom implementation to take over. Check out the code for the full details.

Testing emails & adapters

Another feature that makes Bamboo interesting is the use of the Adapter Pattern to actually handle sending emails. This makes it very easy to swap in the Bamboo.TestAdapter. This adapter takes advantage of Elixir's processes system and the ability to send and receive messages.

With the TestAdapter enabled, Bamboo will capture the email to allow for test helpers like assert_delivered_email(email) to be used. This works by managing the messages sent from the test process, but for the most part Bamboo handles this and we don't need to dig too deep into the details.

deliver_later

One final feature of Elixir that Bamboo takes advantage of is the underlying Erlang platform and the associated Elixir supervisors. With supervisors, it's expected that your code will be running as a child process of some parent "supervisor" process, making it very easy spin off new processes.

Bamboo takes advantage of this to allow for delivering emails in the background, without the need to implement a job queue. While it's possible to go with a more complex implementation for sending later, it's really nice that the nature of Erlang and Elixir make it easy to simply take advantage of the process management and supervisor functionality to provide the deliver_later functionality out of the box.