defmodule Swoosh.Adapters.SparkPost do
@moduledoc ~S"""
An adapter that sends email using the SparkPost API.
For reference: [SparkPost API docs](https://developers.sparkpost.com/api/)
## Example
# config/config.exs
config :sample, Sample.Mailer,
adapter: Swoosh.Adapters.SparkPost,
api_key: "my-api-key",
endpoint: "https://api.sparkpost.com/api/v1"
# or "https://YOUR_DOMAIN.sparkpostelite.com/api/v1" for enterprise
# lib/sample/mailer.ex
defmodule Sample.Mailer do
use Swoosh.Mailer, otp_app: :sample
end
## Using with SparkPost templates
import Swoosh.Email
new()
|> from("tony.stark@example.com")
|> to("steve.rogers@example.com")
|> subject("Hello, Avengers!")
|> put_provider_option(:template_id, "my-first-email")
|> put_provider_option(:substitution_data, %{
first_name: "Peter",
last_name: "Parker"
})
## Setting SparkPost transmission options
Full options can be found at [SparkPost Transmissions API Docs](https://developers.sparkpost.com/api/transmissions/#header-request-body)
import Swoosh.Email
new()
|> from("tony.stark@example.com")
|> to("steve.rogers@example.com")
|> subject("Hello, Avengers!")
|> put_provider_option(:options, %{
click_tracking: false,
open_tracking: false,
transactional: true,
inline_css: true
})
## Provider Options
* `:options` (map) - customization on how the email is sent
* `:template_id` (string) - id of the template to use
* `:substitution_data` (map) - data passed to the template language in
the content, and take precendence over the other data like `:metadata`
"""
use Swoosh.Adapter, required_config: [:api_key]
alias Swoosh.Email
import Swoosh.Email.Render
@endpoint "https://api.sparkpost.com/api/v1"
@impl true
def deliver(%Email{} = email, config \\ []) do
headers = prepare_headers(email, config)
body = email |> prepare_body |> Swoosh.json_library().encode!
url = [endpoint(config), "/transmissions"]
case Swoosh.ApiClient.post(url, headers, body, email) do
{:ok, 200, _headers, body} ->
{:ok, Swoosh.json_library().decode!(body)}
{:ok, code, _headers, body} when code > 399 ->
case Swoosh.json_library().decode(body) do
{:ok, error} -> {:error, {code, error}}
{:error, _} -> {:error, {code, body}}
end
{:error, reason} ->
{:error, reason}
end
end
defp endpoint(config), do: config[:endpoint] || @endpoint
defp prepare_headers(_email, config) do
[
{"User-Agent", "swoosh/#{Swoosh.version()}"},
{"Authorization", config[:api_key]},
{"Content-Type", "application/json"}
]
end
defp prepare_body(
%{
from: {name, address},
to: to,
subject: subject,
text_body: text,
html_body: html
} = email
) do
%{
content: %{
from: %{
name: name,
email: address
},
subject: subject,
text: text,
html: html,
headers: %{}
},
recipients: prepare_recipients(to, to)
}
|> prepare_reply_to(email)
|> prepare_cc(email)
|> prepare_bcc(email)
|> prepare_custom_headers(email)
|> prepare_attachments(email)
|> prepare_template_id(email)
|> prepare_substitutions(email)
|> prepare_options(email)
end
defp prepare_reply_to(body, %{reply_to: nil}), do: body
defp prepare_reply_to(body, %{reply_to: reply_to}) do
put_in(body, [:content, :reply_to], render_recipient(reply_to))
end
defp prepare_cc(body, %{cc: []}), do: body
defp prepare_cc(body, %{cc: cc, to: to}) do
body
|> update_in([:recipients], fn list ->
list ++ prepare_recipients(cc, to)
end)
|> put_in([:content, :headers, "CC"], render_recipient(cc))
end
defp prepare_bcc(body, %{bcc: []}), do: body
defp prepare_bcc(body, %{bcc: bcc, to: to}) do
update_in(body.recipients, fn list ->
list ++ prepare_recipients(bcc, to)
end)
end
defp prepare_recipients(recipients, to) do
Enum.map(recipients, fn {name, address} ->
%{
address: %{
name: name,
email: address,
header_to: raw_email_addresses(to)
}
}
end)
end
defp raw_email_addresses(mailboxes) do
mailboxes |> Enum.map_join(",", fn {_name, address} -> address end)
end
defp prepare_attachments(body, %{attachments: []}), do: body
defp prepare_attachments(body, %{attachments: attachments}) do
{standalone_attachments, inline_attachments} =
Enum.split_with(attachments, fn %{type: type} -> type == :attachment end)
body
|> inject_attachments(:attachments, standalone_attachments)
|> inject_attachments(:inline_images, inline_attachments)
end
defp inject_attachments(body, _key, []), do: body
defp inject_attachments(body, key, attachments) do
put_in(
body.content[key],
Enum.map(attachments, fn %{content_type: type, filename: name} = attachment ->
%{type: type, name: name, data: Swoosh.Attachment.get_content(attachment, :base64)}
end)
)
end
defp prepare_template_id(body, %{provider_options: %{template_id: template_id}}) do
put_in(body, [:content, :template_id], template_id)
end
defp prepare_template_id(body, _email), do: body
defp prepare_substitutions(body, %{provider_options: %{substitution_data: substitution_data}}) do
Map.put(body, :substitution_data, substitution_data)
end
defp prepare_substitutions(body, _email), do: body
defp prepare_options(body, %{provider_options: %{options: options}}) do
Map.put(body, :options, options)
end
defp prepare_options(body, _email), do: body
defp prepare_custom_headers(body, %{headers: headers}) do
custom_headers = Map.merge(body.content.headers, headers)
put_in(body, [:content, :headers], custom_headers)
end
end