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

Add Sparkpost adapter #118

Closed
Closed
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
162 changes: 162 additions & 0 deletions lib/bamboo/adapters/sparkpost_adapter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
defmodule Bamboo.SparkpostAdapter do
@moduledoc """
Sends email using Sparkpost's JSON API.

Use this adapter to send emails through Sparkpost's API. Requires that an API
key is set in the config. See `Bamboo.SparkpostHelper` for extra functions that
can be used by `Bamboo.SparkpostAdapter` (tagging, merge vars, etc.)
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like there is no SparkpostHeloer yet so maybe this could be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have added the helper


## Example config

# In config/config.exs, or config/prod.exs, etc.
config :my_app, MyApp.Mailer,
adapter: Bamboo.SparkpostAdapter,
api_key: "my_api_key"

# Define a Mailer. Maybe in lib/my_app/mailer.ex
defmodule MyApp.Mailer do
use Bamboo.Mailer, otp_app: :my_app
end
"""

@default_base_uri "https://api.sparkpost.com/"
@send_message_path "api/v1/transmissions"
@behaviour Bamboo.Adapter

defmodule ApiError do
defexception [:message]

def exception(%{params: params, response: response}) do
filtered_params = params |> Poison.decode! |> Map.put("key", "[FILTERED]")

message = """
There was a problem sending the email through the Sparkpost API.

Here is the response:

#{inspect response, limit: :infinity}


Here are the params we sent:

#{inspect filtered_params, limit: :infinity}
"""
%ApiError{message: message}
end
end

def deliver(email, config) do
api_key = get_key(config)
params = email |> convert_to_sparkpost_params |> Poison.encode!
case request!(@send_message_path, params, api_key) do
%{status_code: status} = response when status > 299 ->
raise(ApiError, %{params: params, response: response})
response -> response
end
end

@doc false
def handle_config(config) do
if config[:api_key] in [nil, ""] do
raise_api_key_error(config)
else
config
end
end

defp get_key(config) do
case Map.get(config, :api_key) do
nil -> raise_api_key_error(config)
key -> key
end
end

defp raise_api_key_error(config) do
raise ArgumentError, """
There was no API key set for the Sparkpost adapter.

* Here are the config options that were passed in:

#{inspect config}
"""
end

defp convert_to_sparkpost_params(email) do
%{
content: %{
from: %{
name: email.from |> elem(0),
email: email.from |> elem(1),
},
subject: email.subject,
text: email.text_body,
html: email.html_body,
reply_to: extract_reply_to(email),
headers: drop_reply_to(email_headers(email)),
},
recipients: recipients(email),
}
|> add_message_params(email)
end

defp email_headers(email) do
if email.cc == [] do
email.headers
else
Map.put_new(email.headers, "CC", Enum.map(email.cc, fn({_,addr}) -> addr end) |> Enum.join(","))
end
end

defp extract_reply_to(email) do
email.headers["Reply-To"]
end

defp drop_reply_to(headers) do
Map.delete(headers, "Reply-To")
end

defp add_message_params(sparkpost_message, %{private: %{message_params: message_params}}) do
Enum.reduce(message_params, sparkpost_message, fn({key, value}, sparkpost_message) ->
Map.put(sparkpost_message, key, value)
end)
end
defp add_message_params(sparkpost_message, _), do: sparkpost_message

defp recipients(email) do
[]
|> add_recipients(email.to)
|> add_b_cc(email.cc, email.to)
|> add_b_cc(email.bcc, email.to)
end

defp add_recipients(recipients, new_recipients) do
Enum.reduce(new_recipients, recipients, fn(recipient, recipients) ->
recipients ++ [%{"address" => %{
name: recipient |> elem(0),
email: recipient |> elem(1),
}}]
end)
end

defp add_b_cc(recipients, new_recipients, to) do
Enum.reduce(new_recipients, recipients, fn(recipient, recipients) ->
recipients ++ [%{"address" => %{
name: recipient |> elem(0),
email: recipient |> elem(1),
header_to: Enum.map(to, fn({_,addr}) -> addr end) |> Enum.join(","),
}}]
end)
end

defp headers(api_key) do
%{"content-type" => "application/json", "authorization" => api_key}
end

defp request!(path, params, api_key) do
HTTPoison.post!("#{base_uri}/#{path}", params, headers(api_key))
end

defp base_uri do
Application.get_env(:bamboo, :sparkpost_base_uri) || @default_base_uri
end
end
116 changes: 116 additions & 0 deletions lib/bamboo/adapters/sparkpost_helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
defmodule Bamboo.SparkpostHelper do
@moduledoc """
Functions for using features specific to Sparkpost e.g. tagging
"""

alias Bamboo.Email

@doc """
Put extra message parameters that are used by Sparkpost

Parameters set with this function are sent to Sparkpost when used along with
the Bamboo.SparkpostAdapter. You can set things like `important`, `merge_vars`,
and whatever else you need that the Sparkpost API supports.

## Example

email
|> put_param([:options, :open_tracking], true)
|> put_param(:tags, ["foo", "bar"])
|> put_param(:meta_data, %{foo: "bar"})
"""
def put_param(email, keys, value) do
keys = List.wrap(keys)
message_params = (email.private[:message_params] || %{})
|> ensure_keys(keys)
|> update_value(keys, value)

email
|> Email.put_private(:message_params, message_params)
end

@doc """
Set a single tag or multiple tags for an email.

## Example

tag(email, "welcome-email")
tag(email, ["welcome-email", "marketing"])
"""
def tag(email, tags) do
put_param(email, :tags, List.wrap(tags))
end

@doc ~S"""
Add meta data to an email

## Example

email
|> meta_data(foo: bar)
|> meta_data(%{bar: "baz")
"""
def meta_data(email, map) when is_map(map) do
put_param(email, :metadata, map)
end
def meta_data(email, map) do
put_param(email, :metadata, Enum.into(map, %{}))
end

@doc ~S"""
Mark an email as transactional

## Example
email |> mark_transactional
"""
def mark_transactional(email) do
put_param(email, [:options, :transactional], true)
end

@doc ~S"""
Enable open tracking

## Example
email |> track_opens
"""
def track_opens(email) do
put_param(email, [:options, :open_tracking], true)
end

@doc ~S"""
Enable click tracking

## Example
email |> track_clicks
"""
def track_clicks(email) do
put_param(email, [:options, :click_tracking], true)
end

defp update_value(map, keys, value) when is_list(value) do
map
|> update_in(keys, fn
nil -> value
val -> val ++ value
end)
end
defp update_value(map, keys, value) when is_map(value) do
map
|> update_in(keys, fn
nil -> value
val -> Map.merge(val, value)
end)
end
defp update_value(map, keys, value) do
map
|> put_in(keys, value)
end

defp ensure_keys(map, [key]) do
Map.update(map, key, nil, fn(value) -> value end)
end
defp ensure_keys(map, [key | tail]) do
Map.update(map, key, ensure_keys(%{}, tail), fn(value) -> ensure_keys(value, tail) end)
end
defp ensure_keys(map, key), do: ensure_keys(map, [key])
end
Loading