lib/swoosh/adapters/sparkpost.ex

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