Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ability to define interceptors #591

Merged
merged 7 commits into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
StephaneRob marked this conversation as resolved.
Show resolved Hide resolved
@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()
StephaneRob marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the change to these tests. They are far easier to read 🎉


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