diff --git a/README.md b/README.md index bb6f2bec..84f18e05 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,37 @@ 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 +to modify / intercept (block) email on the fly. + +```elixir +# 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`. + +```elixir +# some/path/within/your/app/deny_list_interceptor.ex +defmodule MyApp.DenyListInterceptor do + @behaviour Bamboo.Interceptor + @deny_list ["bar@foo.com"] + + 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 diff --git a/lib/bamboo/email.ex b/lib/bamboo/email.ex index d36b7a3c..b880faf5 100644 --- a/lib/bamboo/email.ex +++ b/lib/bamboo/email.ex @@ -83,7 +83,8 @@ defmodule Bamboo.Email do text_body: nil | String.t(), headers: %{String.t() => String.t()}, assigns: %{atom => any}, - private: %{atom => any} + private: %{atom => any}, + blocked: boolean() } defstruct from: nil, @@ -96,7 +97,8 @@ defmodule Bamboo.Email do headers: %{}, attachments: [], assigns: %{}, - private: %{} + private: %{}, + blocked: false alias Bamboo.{Email, Attachment} @@ -263,4 +265,8 @@ defmodule Bamboo.Email do def put_attachment(%__MODULE__{attachments: attachments} = email, path, opts \\ []) do %{email | attachments: [Bamboo.Attachment.new(path, opts) | attachments]} end + + def block(email) do + %{email | blocked: true} + end end diff --git a/lib/bamboo/interceptor.ex b/lib/bamboo/interceptor.ex new file mode 100644 index 00000000..0f1ca737 --- /dev/null +++ b/lib/bamboo/interceptor.ex @@ -0,0 +1,24 @@ +defmodule Bamboo.Interceptor do + @moduledoc ~S""" + Behaviour for creating an Interceptor. + + An interceptor allow to modify / block an email before it is sent. To block an email, it must be marked as intercepted with `Bamboo.Email.intercept/1`. + + ## Example + + defmodule Bamboo.DenyListInterceptor do + @behaviour Bamboo.Interceptor + @deny_list ["bar@foo.com"] + + def call(email) do + if email.to in @deny_list do + Bamboo.Email.intercept(email) + else + email + end + end + end + """ + + @callback call(email :: Bamboo.Email.t()) :: Bamboo.Email.t() +end diff --git a/lib/bamboo/mailer.ex b/lib/bamboo/mailer.ex index 9083585f..ef14074d 100644 --- a/lib/bamboo/mailer.ex +++ b/lib/bamboo/mailer.ex @@ -67,6 +67,7 @@ defmodule Bamboo.Mailer do {:ok, Bamboo.Email.t()} | {:ok, Bamboo.Email.t(), any} | {:error, Exception.t() | String.t()} + def deliver_now(email, opts \\ []) do {config, opts} = Keyword.split(opts, [:config]) config = build_config(config) @@ -195,7 +196,8 @@ defmodule Bamboo.Mailer do @doc false def deliver_now(adapter, email, config, opts) do - with {:ok, email} <- validate_and_normalize(email, adapter) do + with {:ok, email} <- validate_and_normalize(email, adapter), + %Bamboo.Email{blocked: false} = email <- apply_interceptors(email, config) do if empty_recipients?(email) do debug_unsent(email) @@ -208,6 +210,9 @@ defmodule Bamboo.Mailer do {:error, _} = error -> error end end + else + %Bamboo.Email{blocked: true} = email -> {:ok, email} + response -> response end end @@ -232,7 +237,8 @@ defmodule Bamboo.Mailer do @doc false def deliver_later(adapter, email, config) do - with {:ok, email} <- validate_and_normalize(email, adapter) do + with {:ok, email} <- validate_and_normalize(email, adapter), + %Bamboo.Email{blocked: false} = email <- apply_interceptors(email, config) do if empty_recipients?(email) do debug_unsent(email) else @@ -241,6 +247,9 @@ defmodule Bamboo.Mailer do end {:ok, email} + else + %Bamboo.Email{blocked: true} = email -> {:ok, email} + response -> response end end @@ -333,6 +342,14 @@ defmodule Bamboo.Mailer do defp is_nil_recipient?(_), do: false + defp apply_interceptors(email, config) do + interceptors = config[:interceptors] || [] + + Enum.reduce(interceptors, email, fn interceptor, email -> + apply(interceptor, :call, [email]) + end) + end + @doc """ Wraps to, cc and bcc addresses in a list and normalizes email addresses. diff --git a/test/lib/bamboo/email_test.exs b/test/lib/bamboo/email_test.exs index fbdfdbd6..d13965d0 100644 --- a/test/lib/bamboo/email_test.exs +++ b/test/lib/bamboo/email_test.exs @@ -116,4 +116,10 @@ defmodule Bamboo.EmailTest do assert [%Bamboo.Attachment{filename: "attachment.docx"}] = email.attachments end + + test "block/1 mark email as blocked" do + email = new_email() + refute email.blocked + assert %Bamboo.Email{blocked: true} = block(email) + end end diff --git a/test/lib/bamboo/mailer_test.exs b/test/lib/bamboo/mailer_test.exs index cb7412f5..f7c4b01a 100644 --- a/test/lib/bamboo/mailer_test.exs +++ b/test/lib/bamboo/mailer_test.exs @@ -2,14 +2,18 @@ defmodule Bamboo.MailerTest do use ExUnit.Case alias Bamboo.Email - @mailer_config adapter: __MODULE__.DefaultAdapter, foo: :bar + @mailer_config adapter: __MODULE__.DefaultAdapter, foo: :bar, interceptors: nil setup context do config = - Keyword.merge(@mailer_config, [adapter: context[:adapter]], fn - _key, default, nil -> default - _key, _default, override -> override - end) + Keyword.merge( + @mailer_config, + [adapter: context[:adapter], interceptors: context[:interceptors]], + fn + _key, default, nil -> default + _key, _default, override -> override + end + ) Application.put_env(:bamboo, __MODULE__.Mailer, config) Process.register(self(), :mailer_test) @@ -447,6 +451,76 @@ defmodule Bamboo.MailerTest do end end + describe "interceptors" do + @tag interceptors: [Bamboo.DenyListInterceptor, Bamboo.EnvInterceptor] + test "deliver_now/1 must apply interceptors and send email if not intercepted" do + email = new_email(to: "foo@bar.com") + assert {:ok, %Bamboo.Email{blocked: false}} = Mailer.deliver_now(email) + + assert_receive {:deliver, %Bamboo.Email{to: [{nil, "foo@bar.com"}], subject: "test - "}, + _config} + end + + @tag interceptors: [Bamboo.DenyListInterceptor, Bamboo.EnvInterceptor] + test "deliver_now/1 must apply interceptors and block email if intercepted" do + email = new_email(to: "blocked@blocked.com") + assert {:ok, %Bamboo.Email{blocked: true}} = Mailer.deliver_now(email) + refute_receive {:deliver, %Bamboo.Email{to: [{nil, "blocked@blocked.com"}]}, _config} + end + + @tag interceptors: [Bamboo.DenyListInterceptor, Bamboo.EnvInterceptor] + test "deliver_now!/1 must apply interceptors and send email if not intercepted" do + email = new_email(to: "foo@bar.com") + assert %Bamboo.Email{blocked: false} = Mailer.deliver_now!(email) + + assert_receive {:deliver, %Bamboo.Email{to: [{nil, "foo@bar.com"}], subject: "test - "}, + _config} + end + + @tag interceptors: [Bamboo.DenyListInterceptor, Bamboo.EnvInterceptor] + test "deliver_now!/1 must apply interceptors and block email if intercepted" do + email = new_email(to: "blocked@blocked.com") + + assert %Bamboo.Email{blocked: true} = Mailer.deliver_now!(email) + + refute_receive {:deliver, %Bamboo.Email{to: [{nil, "blocked@blocked.com"}]}, _config} + end + + @tag interceptors: [Bamboo.DenyListInterceptor, Bamboo.EnvInterceptor] + test "deliver_later/1 must apply interceptors and send email if not intercepted" do + email = new_email(to: "foo@bar.com") + assert {:ok, %Bamboo.Email{blocked: false}} = Mailer.deliver_later(email) + + assert_receive {:deliver, %Bamboo.Email{to: [{nil, "foo@bar.com"}], subject: "test - "}, + _config} + end + + @tag interceptors: [Bamboo.DenyListInterceptor, Bamboo.EnvInterceptor] + test "deliver_later/1 must apply interceptors and block email if intercepted" do + email = new_email(to: "blocked@blocked.com") + + assert {:ok, %Bamboo.Email{blocked: true}} = Mailer.deliver_later(email) + + refute_receive {:deliver, %Bamboo.Email{to: [{nil, "blocked@blocked.com"}]}, _config} + end + + @tag interceptors: [Bamboo.DenyListInterceptor, Bamboo.EnvInterceptor] + test "deliver_later!/1 must apply interceptors and send email if not intercepted" do + email = new_email(to: "foo@bar.com") + assert %Bamboo.Email{blocked: false} = Mailer.deliver_later!(email) + + assert_receive {:deliver, %Bamboo.Email{to: [{nil, "foo@bar.com"}], subject: "test - "}, + _config} + end + + @tag interceptors: [Bamboo.DenyListInterceptor, Bamboo.EnvInterceptor] + test "deliver_later!/1 must apply interceptors and block email if intercepted" do + email = new_email(to: "blocked@blocked.com") + assert %Bamboo.Email{blocked: true} = Mailer.deliver_later!(email) + refute_receive {:deliver, %Bamboo.Email{to: [{nil, "blocked@blocked.com"}]}, _config} + end + end + defp new_email(attrs \\ []) do attrs = Keyword.merge([from: "foo@bar.com", to: "foo@bar.com"], attrs) Email.new_email(attrs) diff --git a/test/support/deny_list_interceptor.ex b/test/support/deny_list_interceptor.ex new file mode 100644 index 00000000..9750657e --- /dev/null +++ b/test/support/deny_list_interceptor.ex @@ -0,0 +1,13 @@ +defmodule Bamboo.DenyListInterceptor do + @behaviour Bamboo.Interceptor + + @deny_list ["blocked@blocked.com"] + + def call(email) do + if Enum.any?(email.to, &(elem(&1, 1) in @deny_list)) do + Bamboo.Email.block(email) + else + email + end + end +end diff --git a/test/support/env_interceptor.ex b/test/support/env_interceptor.ex new file mode 100644 index 00000000..632933d9 --- /dev/null +++ b/test/support/env_interceptor.ex @@ -0,0 +1,9 @@ +defmodule Bamboo.EnvInterceptor do + @behaviour Bamboo.Interceptor + + @env Mix.env() + + def call(email) do + %{email | subject: "#{@env} - #{email.subject}"} + end +end