lib/swoosh/adapters/mailjet.ex

defmodule Swoosh.Adapters.Mailjet do
  @moduledoc ~S"""
  An adapter that sends email using the Mailjet API.

  For reference: [Mailjet API docs](https://dev.mailjet.com/guides/#send-api-v3-1)

  ## Dependency

  Mailjet adapter requires `Plug` to work properly.

  ## Example

      # config/config.exs
      config :sample, Sample.Mailer,
        adapter: Swoosh.Adapters.Mailjet,
        api_key: "my-api-key",
        secret: "my-secret-key"

      # lib/sample/mailer.ex
      defmodule Sample.Mailer do
        use Swoosh.Mailer, otp_app: :sample
      end

  ## Using with provider options

      import Swoosh.Email

      new()
      |> from({"Billi Wang", "billi_wang@example.com"})
      |> to({"Nai Nai", "nainai@example.com"})
      |> reply_to("a24@example.com")
      |> cc({"Haiyan Wang", "haiyan_wang@example.com"})
      |> cc("lujian@example.com")
      |> bcc({"Hao Hao", "haohao@example.com"})
      |> bcc("aiko@example.com")
      |> subject("Hello, Nai Nai!")
      |> html_body("<h1>Hello</h1>")
      |> text_body("Hello")
      |> put_provider_option(:template_id, 123)
      |> put_provider_option(:template_error_deliver, true)
      |> put_provider_option(:template_error_reporting, "developer@example.com")
      |> put_provider_option(:variables, %{firstname: "lulu", lastname: "wang"})

  ## Provider options

    * `:template_id` (integer) - `TemplateID`, unique template id of the
      template to be used as email content

    * `:template_error_deliver` (boolean) - `TemplateErrorDeliver`,
      send even if error in template if `true`, otherwise stop email delivery
      immediately upon error

    * `:template_error_reporting` (string | tuple | map) - `TemplateErrorReporting`,
      email address or a tuple of name and email address of a recipient to send a
      carbon copy upon error

    * `:variables` (map) - `Variables`, custom key-value variable for the email
      content

  """

  use Swoosh.Adapter,
    required_config: [:api_key, :secret],
    required_deps: [plug: Plug.Conn.Query]

  alias Swoosh.{Email, Attachment}

  @base_url "https://api.mailjet.com/v3.1"
  @api_endpoint "send"

  @impl true
  def deliver(%Email{} = email, config \\ []) do
    send_request(prepare_body(email), email, config)
  end

  @impl true
  def deliver_many(emails, config \\ [])

  def deliver_many([], _config) do
    {:ok, []}
  end

  def deliver_many(emails, config) when is_list(emails) do
    send_request(prepare_body(emails), emails, config)
  end

  defp send_request(body, email_or_emails, config) do
    email = email_or_emails |> List.wrap() |> List.first()
    headers = prepare_headers(config)
    url = [base_url(config), "/", @api_endpoint]

    case Swoosh.ApiClient.post(url, headers, body, email) do
      {:ok, 200, _headers, body} ->
        {:ok, parse_results(body)}

      {:ok, error_code, _headers, body} when error_code >= 400 ->
        {:error, {error_code, parse_results(body)}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp parse_results(%{"Messages" => results}) do
    results =
      Enum.map(results, fn
        %{"Status" => "success"} = result -> get_message_id(result)
        per_message_error -> per_message_error
      end)

    case results do
      [single] -> single
      multiple -> multiple
    end
  end

  defp parse_results(body) when is_binary(body) do
    body
    |> Swoosh.json_library().decode!
    |> parse_results()
  end

  defp parse_results(global_error) do
    global_error
  end

  defp get_message_id(%{"To" => [%{"MessageID" => message_id}]}) do
    %{id: message_id}
  end

  defp get_message_id(%{"To" => multiple_receivers}) do
    %{
      id:
        Enum.map(
          multiple_receivers,
          fn %{"MessageID" => message_id} ->
            message_id
          end
        )
    }
  end

  defp base_url(config), do: config[:base_url] || @base_url

  defp prepare_headers(config) do
    [
      {"User-Agent", "swoosh/#{Swoosh.version()}"},
      {"Authorization", "Basic #{auth(config)}"},
      {"Content-Type", "application/json"}
    ]
  end

  defp auth(config), do: Base.encode64("#{config[:api_key]}:#{config[:secret]}")

  defp prepare_body(emails) do
    emails
    |> List.wrap()
    |> Enum.map(&prepare_message/1)
    |> wrap_messages()
    |> Swoosh.json_library().encode!()
  end

  defp prepare_message(email) do
    %{}
    |> prepare_from(email)
    |> prepare_to(email)
    |> prepare_subject(email)
    |> prepare_html(email)
    |> prepare_text(email)
    |> prepare_cc(email)
    |> prepare_bcc(email)
    |> prepare_reply_to(email)
    |> prepare_attachments(email)
    |> prepare_variables(email)
    |> prepare_template(email)
    |> prepare_custom_headers(email)
    |> prepare_custom_id(email)
  end

  defp wrap_messages(body) when is_list(body), do: %{Messages: body}

  defp prepare_custom_id(body, %{provider_options: %{custom_id: custom_id}}),
    do: Map.put(body, "CustomID", custom_id)

  defp prepare_custom_id(body, _options), do: body

  defp prepare_custom_headers(body, %{headers: headers}),
    do: Map.put(body, "Headers", headers)

  defp prepare_attachments(body, %{attachments: []}), do: body

  defp prepare_attachments(body, %{attachments: attachments}) do
    {normal_attachments, inline_attachments} =
      Enum.split_with(attachments, fn %{type: type} -> type == :attachment end)

    body
    |> Map.put(
      "Attachments",
      Enum.map(normal_attachments, &prepare_attachment/1)
    )
    |> Map.put(
      "InlinedAttachments",
      Enum.map(inline_attachments, &prepare_attachment/1)
    )
  end

  defp prepare_attachment(attachment) do
    %{
      "ContentType" => attachment.content_type,
      "Filename" => attachment.filename,
      "Base64Content" => Attachment.get_content(attachment, :base64)
    }
  end

  defp prepare_recipients(recipients),
    do: Enum.map(recipients, &prepare_recipient(&1))

  defp prepare_recipient({name, address}),
    do: %{"Name" => name, "Email" => address}

  defp prepare_from(body, %{from: from}),
    do: Map.put(body, "From", prepare_recipient(from))

  defp prepare_to(body, %{to: to}),
    do: Map.put(body, "To", prepare_recipients(to))

  defp prepare_reply_to(body, %{reply_to: nil}), do: body

  defp prepare_reply_to(body, %{reply_to: reply_to}),
    do: Map.put(body, "ReplyTo", prepare_recipient(reply_to))

  defp prepare_cc(body, %{cc: []}), do: body

  defp prepare_cc(body, %{cc: cc}),
    do: Map.put(body, "Cc", prepare_recipients(cc))

  defp prepare_bcc(body, %{bcc: []}), do: body

  defp prepare_bcc(body, %{bcc: bcc}),
    do: Map.put(body, "Bcc", prepare_recipients(bcc))

  defp prepare_subject(body, %{subject: subject}),
    do: Map.put(body, "Subject", subject)

  defp prepare_text(body, %{text_body: nil}), do: body

  defp prepare_text(body, %{text_body: text_body}),
    do: Map.put(body, "TextPart", text_body)

  defp prepare_html(body, %{html_body: nil}), do: body

  defp prepare_html(body, %{html_body: html_body}),
    do: Map.put(body, "HTMLPart", html_body)

  defp prepare_variables(body, %{provider_options: %{variables: variables}}) do
    Map.put(body, "Variables", variables)
  end

  defp prepare_variables(body, _email), do: body

  defp prepare_template(body, %{
         provider_options: %{template_id: template_id} = provider_options
       }) do
    body =
      body
      |> Map.put("TemplateID", template_id)
      |> Map.put("TemplateLanguage", true)
      |> Map.put(
        "TemplateErrorDeliver",
        !!provider_options[:template_error_deliver]
      )

    case provider_options[:template_error_reporting] do
      nil ->
        body

      {name, email} when is_binary(name) and is_binary(email) ->
        Map.put(body, "TemplateErrorReporting", %{
          "Email" => email,
          "Name" => name
        })

      email when is_binary(email) ->
        Map.put(body, "TemplateErrorReporting", %{
          "Email" => email,
          "Name" => ""
        })

      map when is_map(map) ->
        Map.put(body, "TemplateErrorReporting", map)
    end
  end

  defp prepare_template(body, _email), do: body
end