Skip to content

Commit

Permalink
Adds ability to define interceptors (#591)
Browse files Browse the repository at this point in the history
This commit adds the ability to define custom email interceptors. 

Interceptors are modules that abide by an interceptor behaviour 
and are configured alongside Bamboo's config. 

Why interceptors?
-------------------

Interceptors can be used to either modify emails or prevent them
from going out. 

For example, we may want to:

- rewrite subject for a given environment (e.g. prefix [environment] to subject)
- rewrite email `to` address so that it only sends to allowed addresses 
   in dev/staging
- block outbound emails for a given environment (staging, dev,...)
- block email in case the recipient is blocked (bounces, complaints, ...)
  • Loading branch information
StephaneRob committed Apr 5, 2021
1 parent 7af7bf3 commit 733b9e5
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 9 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions lib/bamboo/email.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -96,7 +97,8 @@ defmodule Bamboo.Email do
headers: %{},
attachments: [],
assigns: %{},
private: %{}
private: %{},
blocked: false

alias Bamboo.{Email, Attachment}

Expand Down Expand Up @@ -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
24 changes: 24 additions & 0 deletions lib/bamboo/interceptor.ex
Original file line number Diff line number Diff line change
@@ -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
21 changes: 19 additions & 2 deletions lib/bamboo/mailer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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

Expand All @@ -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
Expand All @@ -241,6 +247,9 @@ defmodule Bamboo.Mailer do
end

{:ok, email}
else
%Bamboo.Email{blocked: true} = email -> {:ok, email}
response -> response
end
end

Expand Down Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions test/lib/bamboo/email_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
84 changes: 79 additions & 5 deletions test/lib/bamboo/mailer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions test/support/deny_list_interceptor.ex
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions test/support/env_interceptor.ex
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 733b9e5

Please sign in to comment.