Giter Site home page Giter Site logo

beam-community / bamboo Goto Github PK

View Code? Open in Web Editor NEW
1.9K 25.0 339.0 794 KB

Testable, composable, and adapter based Elixir email library for devs that love piping.

Home Page: https://hex.pm/packages/bamboo

License: MIT License

Elixir 95.49% HTML 4.38% Shell 0.13%
elixir email elixir-phoenix bamboo

bamboo's Introduction

bamboo

Bamboo Circle CI Coverage Status


This README follows master, which may not be the currently published version! Use the docs for the published version of Bamboo.


Bamboo is part of the thoughtbot Elixir family of projects.

Flexible and easy to use email for Elixir.

  • Built-in support for popular mail delivery services. Bamboo ships with adapters for several popular mail delivery services, including Mandrill, Mailgun, and SendGrid. It's also quite easy to write your own delivery adapter if your platform isn't yet supported.
  • Deliver emails in the background. Most of the time you don't want or need to wait for the email to send. Bamboo makes it easy with Mailer.deliver_later.
  • A functional approach to mail delivery. Emails are created, manipulated, and sent using plain functions. This makes composition a breeze and fits naturally into your existing Elixir app.
  • Unit test with ease. Bamboo separates email creation and email delivery allowing you to test by asserting against email fields without the need for special functions.
  • Dead-simple integration tests. Bamboo provides helper functions to make integration testing easy and robust.
  • View sent emails during development. Bamboo provides a plug you can use in your router to view sent emails.
  • Integrate with Phoenix out of the box. Use Phoenix views and layouts to make rendering email easy.

See the docs for the most up to date information.

We designed Bamboo to be simple and powerful. If you run into anything that is less than exceptional, or you just need some help, please open an issue.

Installation

To install Bamboo, add it to your list of dependencies in mix.exs.

def deps do
  [{:bamboo, "~> 2.3.0"}]
end

You may also use the latest code available from master instead of a published version in hex:

def deps do
  [{:bamboo, github: "thoughtbot/bamboo"}]
end

Once you've added Bamboo to your list, update your dependencies by running:

$ mix deps.get

If you are using Elixir < 1.4, also ensure Bamboo is started alongside your application:

def application do
  [applications: [:bamboo]]
end

Getting Started

Bamboo separates the tasks of email creation and email sending. To use Bamboo, you'll need to define one or more email modules (email creation), define a mailer module (email sending), and provide some configuration.

To create emails, define an email module within your application.

# some/path/within/your/app/email.ex
defmodule MyApp.Email do
  import Bamboo.Email

  def welcome_email do
    new_email(
      to: "[email protected]",
      from: "[email protected]",
      subject: "Welcome to the app.",
      html_body: "<strong>Thanks for joining!</strong>",
      text_body: "Thanks for joining!"
    )
  end
end

In addition to the keyword syntax above you can also compose emails using pipes.

To send emails, define a mailer module for your application that uses Bamboo's mailer.

# some/path/within/your/app/mailer.ex
defmodule MyApp.Mailer do
  use Bamboo.Mailer, otp_app: :my_app
end

Your configuration will need to know your OTP application, your mailer module, the adapter you are using, and any additional configuration required by the adapter itself.

# config/config.exs
config :my_app, MyApp.Mailer,
  adapter: Bamboo.MandrillAdapter,
  api_key: "my_api_key"

Bamboo uses Hackney for making requests. If you want to pass options to Hackney directly, such as controlling timeouts, you can use the hackney_opts key:

# config/config.exs
config :my_app, MyApp.Mailer,
  adapter: Bamboo.MandrillAdapter,
  api_key: "my_api_key",
  hackney_opts: [
    recv_timeout: :timer.minutes(1),
    connect_timeout: :timer.minutes(1)
  ]

Other adapter-specific configuration may be required. Be sure to check the adapter's docs.

Now that you have configured Bamboo and defined your modules, you can deliver email in fitting places within your application.

defmodule MyApp.SomeControllerPerhaps do
  def send_welcome_email do
    Email.welcome_email()   # Create your email
    |> Mailer.deliver_now!() # Send your email
  end
end

Your application is now set up to send email with Bamboo! ๐ŸŽ‰

Using Adapters

An adapter is a set of instructions for how to communicate with a specific email delivery service. Bamboo ships with support for several popular services, there are others made available by the community, or you can use other services by writing a custom adapter.

To use an adapter, declare it in the configuration for your mailer:

# config/config.exs
config :my_app, MyApp.Mailer,
  adapter: Bamboo.MandrillAdapter

Bamboo provides adapters for use in development and testing. To use these adapters, declare them in the environment configuration.

The local adapter stores emails in memory that can be viewed during development. Declare its use in your dev environment.

# config/dev.exs
config :my_app, MyApp.Mailer,
  adapter: Bamboo.LocalAdapter

The test adapter sends emails to your running process allowing you to test mail delivery without emails being sent externally. Declare its use in your test environment.

# config/test.exs
config :my_app, MyApp.Mailer,
  adapter: Bamboo.TestAdapter

You can create new adapters for any environment by implementing the Bamboo.Adapter behaviour.

Delivering Emails in the Background

Often times you don't want to send an email right away because it can block process completion (e.g. a web request in Phoenix). Bamboo provides a deliver_later function on your mailers to send emails in the background. It also provides a Bamboo.DeliverLaterStrategy behaviour that you can implement to tailor your background email sending.

By default, deliver_later uses Bamboo.TaskSupervisorStrategy. This strategy sends the email right away, but it does so in the background without linking to the calling process, so errors in the mailer won't bring down your app.

You can also create custom strategies by implementing the Bamboo.DeliverLaterStrategy behaviour. For example, you could create strategies for adding emails to a background processing queue such as exq or toniq.

Composing with Pipes

In addition to creating emails with keyword lists you, can use pipe syntax to compose emails. This is particularly useful for providing defaults (e.g. from address, default layout, etc.)

defmodule MyApp.Email do
  import Bamboo.Email
  import Bamboo.Phoenix

  def welcome_email do
    base_email() # Build your default email then customize for welcome
    |> to("[email protected]")
    |> subject("Welcome!!!")
    |> put_header("Reply-To", "[email protected]")
    |> html_body("<strong>Welcome</strong>")
    |> text_body("Welcome")
  end

  defp base_email do
    new_email()
    |> from("[email protected]") # Set a default from
    |> put_html_layout({MyApp.LayoutView, "email.html"}) # Set default layout
    |> put_text_layout({MyApp.LayoutView, "email.text"}) # Set default text layout
  end
end

Handling Recipients

The from, to, cc, and bcc addresses can be a string or a 2 element tuple. What happens if you try to send to a list of MyApp.Users? Transforming your data structure each time you send an email would be a pain.

# This stinks. Do you want to do this every time you create a new email?
users = for user <- users do
  {user.name, user.email}
end

new_email(to: users)

Bamboo alleviates this pain by providing the Bamboo.Formatter protocol. By implementing the protocol for your data structure once, you can pass that struct directly to Bamboo anywhere it expects an address. See the Bamboo.Email and Bamboo.Formatter docs for more information and examples.

Interceptors

It's possible to configure per Mailer interceptors. Interceptors allow you to modify or block emails on the fly.

# config/config.exs
config :my_app, MyApp.Mailer,
  adapter: Bamboo.MandrillAdapter,
  interceptors: [MyApp.DenyListInterceptor]
end

An interceptor must implement the Bamboo.Interceptor behaviour. To prevent email being sent, you can block it with Bamboo.Email.block/1.

# some/path/within/your/app/deny_list_interceptor.ex
defmodule MyApp.DenyListInterceptor do
  @behaviour Bamboo.Interceptor
  @deny_list ["[email protected]"]

  def call(email) do
    if email.to in @deny_list do
      Bamboo.Email.block(email)
    else
      email
    end
  end
end

Using Phoenix Views and Layouts

Phoenix is not required to use Bamboo. But if you want to use Phoenix's views and layouts to render emails, see bamboo_phoenix and Bamboo.Phoenix.

Viewing Sent Emails

Bamboo comes with a handy plug for viewing emails sent in development. Now you don't have to look at the logs to get password resets, confirmation links, etc. Just open up the sent email viewer and click the link.

See Bamboo.SentEmailViewerPlug.

Here is what it looks like:

Screenshot of BambooSentEmailViewer

Mandrill Specific Functionality (tags, merge vars, templates, etc.)

Mandrill offers extra features on top of regular SMTP email like tagging, merge vars, templates, and scheduling emails to send in the future. See Bamboo.MandrillHelper.

SendGrid Specific Functionality (templates, substitution tags, scheduled delivery, etc.)

SendGrid offers extra features on top of regular SMTP email like transactional templates with substitution tags. See Bamboo.SendGridHelper.

JSON support

Bamboo comes with JSON support out of the box via the Jason library. To use it, add :jason to your dependencies:

{:jason, "~> 1.0"}

You can customize it to use another library via the :json_library configuration:

config :bamboo, :json_library, SomeOtherLib

Testing

Bamboo separates email creation and email sending. Test email creation by asserting against the email struct created by your functions. For example, assuming your welcome email accepts a user recipient, provides the correct from address, and provides specific text, you might test like this:

defmodule MyApp.EmailTest do
  use ExUnit.Case

  test "welcome email" do
    user = {"Ralph", "[email protected]"}

    email = MyApp.Email.welcome_email(user)

    assert email.to == user
    assert email.from == "[email protected]"
    assert email.html_body =~ "<p>Thanks for joining</p>"
    assert email.text_body =~ "Thanks for joining"
  end
end

Test email sending in integration tests by using the Bamboo.TestAdapter along with Bamboo.Test. For example, assuming during the registration process of your app an email is sent to the user welcoming them to the application, you might test this feature like this:

defmodule MyApp.RegistrationTest do
  use ExUnit.Case
  use Bamboo.Test

  # Remember to use the `Bamboo.TestAdapter` in your test config

  test "after registering, the user gets a welcome email" do
    user = new_user()
    expected_email = MyApp.Email.welcome_email(user.email)

    MyApp.Registration.create(user)

    assert_delivered_email expected_email
  end

  defp new_user do
    # Build a user appropriate to your application
  end
end

See the documentation for Bamboo.Test for more examples and additional helper functions.

Available Adapters

Here is a list of adapters that either ship with Bamboo or have been made available by the community. Feel free to open an issue or a PR if you'd like to add a new adapter to the list.

Contributing

Before opening a pull request, please open an issue first.

Once we've decided how to move forward with a pull request:

$ git clone https://github.com/thoughtbot/bamboo.git
$ cd bamboo
$ mix deps.get
$ mix test
$ mix format

Once you've made your additions and mix test passes, go ahead and open a PR!

We run the test suite as well as formatter checks on CI. Make sure you are using the Elixir version defined in the .tool-versions file to have consistent formatting with what's being run on CI.

About thoughtbot

thoughtbot

Bamboo is maintained and funded by thoughtbot, inc. The names and logos for thoughtbot are trademarks of thoughtbot, inc.

We love open-source software, Elixir, and Phoenix. See our other Elixir projects, or hire our Elixir Phoenix development team to design, develop, and grow your product.

Thanks!

Thanks to @mtwilliams for an early version of the SendGridAdapter.

bamboo's People

Contributors

aellispierce avatar bartoszgorka avatar bgentry avatar bratsche avatar brian-penguin avatar dependabot-support avatar dependabot[bot] avatar drapergeek avatar germsvel avatar jbernardo95 avatar jerodsanto avatar jonrowe avatar jsteiner avatar kalys avatar lancejjohnson avatar leaexplores avatar linjunpop avatar maartenvanvliet avatar maymillerricci avatar mtarnovan avatar paulcsmith avatar princemaple avatar randycoulman avatar rawkode avatar ruudk avatar smdern avatar stevedomin avatar tomtaylor avatar tsubery avatar ymtszw avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bamboo's Issues

assert_delivered_email with options

Usually assert_delivered_email(Email.welcome_email(user) will work fine, but there are cases when it won't

I think I should add assert_delivered_email(subject: "Something") in case you can't/don't want to assert against a whole email.

This would be helpful if parts of the email have state that would make it difficult to test the whole email. For example, if a token is generated in the text/html_body from Phoenix.Token, then the body would be different every time since the token would change. So it might be better to assert just a few of the most important fields.

Don't parse all config opts at compile time

Instead grab the adapter and other info (API key, etc.) at runtime. This makes it easier to change during tests, or while using in iex. That way you can switch to a TestAdapter or LocalAdapter long enough to send some tests emails and then switch it back if you wanted to. Also would clean up the Bamboo tests

EDIT: I'm not totally sure about this. Probably going to hold off on it for a bit

Remove use Mandrill.Email

It only imports two modules and most people won't use this that much (usually will have just a couple email modules). So I think it's better to just let people import the modules they need.

This is almost done

Add Bamboo.Test

This will

  • alias Bamboo.SentEmail
  • import Bamboo.Formatter.format_recipient
  • Add a setup block that calls SentEmail.reset

Raise if there is no html or text unless you explicitly set them both to empty

This makes it easier to find out when you accidentally forget to render the body, but still allows you to leave it empty if you really want to.

The reason that you might want to set empty html and text bodies is that some adapters may be used to render emails from another service. For example, you can use Mandrill to render responsive templates that are rendered on their server, so you would not set a text or html body

Fix options with assert_delivered_email

It should normalize the email addresses, and should only match against the keys that are in the options. For asserting just what you pass in, do something like this

defmacro assert_delivered_email(email_params) do
    # Normalize the address as well
    quote do
      import ExUnit.Assertions
      assert_received({:delivered_email, unquote(email_params)})
    end
  end

More explicit phoenix template rendering

Right now view rendering uses Phoenix.View.render_existing which is ok except that it can make it easy to think you're sending an email with both formats, but in reality you had a typo in a template name and you only send a text or html version. Instead I think this should be more explicit so you get more immediate and helpful feedback.

Right now you do: render("template_name") which will render a text or html template if it exists

Instead I think it should be render("template_name.html") and render("template_name.text") if you have just one type of template to send.

If you want to send text and html then you would do render(:template_name) and it will render both and raise if either one is missing.

Make sure task worker is added

I wonder if there should be some way to test that the task worker is added so you don't deploy to prod and find out that you forgot to add the Bamboo.TaskSupervisor to your supervisor.

Rename Formatter.format_recipient and pass type of email

Instead have Bamboo.EmailAddress.format It should also have a second argument that is passed a keyword list (or maybe a map) that contains what type of address it is (from, cc, bcc, to)

That way you can do something like

defimpl Bamboo.EmailAddress, for: MyApp.User do
  # Add the app's name when sending the email from a user
  def format(user, %{type: :from}) do
    %{name: "#{user.name} (NameOfApp)", address: user.address}
  end

  def format(user, _opts) do
    %{name: user.name, address: user.address
  end
end

Add ability to add attachments

This is probably going to come a bit later since I have never needed to send an attachment in an email, it's always a link back to the app. I know this is useful to people though so it should be added.

Should deliver and deliver_async noop if all recipients are blank?

While using this in one of our applications we had to do something like this

users = Repo.all(User)

if length(users) > 0 do
  Emails.welcome_email(users) |> Mailer.deliver_async
end

Maybe deliver_async should throw a warning? Maybe an option to allow empty_recipients?

deliver_async(allow_empty_recipients: true)

Maybe this could be done at the mailer level via an option?

defmodule MyApp.Mailer do
  use Bamboo.Mailer, otp_app: :my_app

  @allow_empty_recipients true
end

Change nil on %Email{from} to %UnknowMailbox{}

nil doesn't really mean much so it can be hard to figure out where it's coming from and what it means. Instead the default should be a struct like %UnknownMailbox{} or %UnsetMailbox{}. Something along those lines

Make TestMailbox.one wait

If you use deliver_async TestMailbox.one will often fail because the email hasn't quite been sent yet. The Mailbox should wait a bit. Maybe using receive

Don't use Task.async in deliver_async

Task.async is linking the task process with the parent process so if the latter dies first it will kill the former.

For example, with Phoenix it would mean that a HTTP request could finish before we actually had a chance to send the email.

Elixir 1.2 introduced Task.async_no_link which could be used here.

Happy to submit a PR if you think that's the way to go.

Use more reliable async task

I think Task.Supervisor.start_child should do the trick. Task.start_link links to the caller process and if the caller process dies, so does the background task. This is probably not preferred.

Improve folder naming

@scrogson suggested improving the folder names for clarity

I would create lib/bamboo/adapters
move mandrill_adapter.ex to lib/bamboo/adapters/mandrill.ex
same for the test_adapter.ex
make lib/bamboo/adapters/mandrill/email.ex
and lib/bamboo/adapters/test/mailbox.ex
and update the module names accordingly

Change deliver to deliver_now for delivering right away

I think this makes the library a bit easier to use. That way deliver uses the strategy you want and if you want to deliver right away then you use deliver_sync. This essentially flips how the current mailer works.

Still not 100% sold on the name though. Function name ideas:

  • deliver_now
  • deliver_sync
  • deliver_blocking (though it's not really blocking so maybe a bad name)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.