lib/bamboo/adapters/flowmailer_adapter.ex

defmodule Bamboo.FlowMailerAdapter do
  @moduledoc """
  Sends email using FlowMailer API.
  Use this adapter to send emails through FlowMailer API.
  """
  alias Bamboo.Email
  alias FlowMailer.Token
  import Bamboo.ApiError
  import FlowMailer.Shared

  @behaviour Bamboo.Adapter

  @default_flowmailer_host "https://api.flowmailer.net"
  @service_name "FlowMailer"

  @doc false
  def supports_attachments?, do: true

  @doc false
  def handle_config(config) do
    Token.handle_config(config)
    config
  end

  defp send_path(config) do
    account_id = get_config_value(config, :flowmailer_account_id)
    "#{account_id}/messages/submit"
  end

  defp default_flowmailer_host(config) do
    get_config_value(config, :flowmailer_host, @default_flowmailer_host)
  end

  def deliver(email, config) do
    body = email |> to_flowmailer_body(config) |> Bamboo.json_library().encode!()
    url = default_flowmailer_host(config) |> URI.merge(send_path(config)) |> to_string()
    token = get_token!(config)

    case :hackney.post(
           url,
           headers_with_token(token),
           body,
           hackney_opts(config)
         ) do
      {:ok, status, _headers, response} when status > 299 ->
        filtered_params = body |> Bamboo.json_library().decode!()
        {:error, build_api_error(@service_name, response, filtered_params)}

      {:ok, status, headers, response} ->
        {:ok, %{status_code: status, headers: headers, body: response}}

      {:error, reason} ->
        {:error, build_api_error(inspect(reason))}
    end
  catch
    :throw, {:error, _} = error -> error
  end

  defp get_token!(config) do
    case Token.get(config) do
      {:ok, token} -> token
      {:error, reason} -> raise "Token error: #{inspect(reason)}"
    end
  end

  defp headers_with_token(token) do
    [
      {"Authorization", "Bearer #{token}"},
      {"Accept", "Application/json; Charset=utf-8"},
      {"Content-Type", "application/json"}
    ]
  end

  defp to_flowmailer_body(%Email{} = email, _config) do
    base_message()
    |> put_attachments(email)
    |> put_flow_selector(email)
    |> put_data(email)
    |> put_header_from_address(email)
    |> put_header_from_name(email)
    |> put_header_to_address(email)
    |> put_header_to_name(email)
    |> put_headers(email)
    |> put_html(email)
    |> put_mime(email)
    |> put_recipient_address(email)
    |> put_schedule_at(email)
    |> put_sender_address(email)
    |> put_subject(email)
    |> put_tags(email)
    |> put_text(email)
  end

  defp base_message() do
    %{messageType: "EMAIL"}
  end

  defp put_attachments(message, %Email{attachments: attachments}) do
    transformed =
      attachments
      |> Enum.reverse()
      |> Enum.map(fn attachment ->
        %{
          content: Base.encode64(attachment.data),
          contentId: attachment.content_id,
          disposition: "attachment",
          contentType: attachment.content_type,
          filename: attachment.filename
        }
      end)

    Map.put(message, :attachments, transformed)
  end

  defp put_data(message, %Email{assigns: nil}), do: message

  defp put_data(message, %Email{assigns: assigns}) do
    Map.put(message, "data", assigns)
  end

  defp put_flow_selector(message, %Email{private: %{flowmailer_flow_selector: flow_selector}}) do
    Map.put(message, "flowSelector", flow_selector)
  end

  defp put_flow_selector(message, _), do: message

  defp put_header_from_address(message, %Email{headers: %{"From" => from}}) do
    Map.put(message, "headerFromAddress", from)
  end

  defp put_header_from_address(message, %Email{headers: %{"from" => from}}) do
    Map.put(message, "headerFromAddress", from)
  end

  defp put_header_from_address(message, %Email{}), do: message

  defp put_header_from_name(message, %Email{from: {name, _email}}) when is_binary(name) do
    Map.put(message, "headerFromName", name)
  end

  defp put_header_from_name(message, %Email{}), do: message

  defp put_header_to_address(message, %Email{headers: %{"To" => to}}) do
    Map.put(message, "headerToAddress", to)
  end

  defp put_header_to_address(message, %Email{headers: %{"to" => to}}) do
    Map.put(message, "headerToAddress", to)
  end

  defp put_header_to_address(message, _), do: message

  defp put_header_to_name(message, %Email{to: {name, _email}}) when is_binary(name) do
    Map.put(message, "headerToName", name)
  end

  defp put_header_to_name(message, %Email{}), do: message

  defp put_headers(message, %Email{headers: headers}) when is_map(headers) do
    headers_without_tuple_values =
      headers
      |> Map.delete("To")
      |> Map.delete("to")
      |> Map.delete("from")
      |> Map.delete("From")

    Map.put(message, "headers", headers_without_tuple_values)
  end

  defp put_headers(message, _), do: message

  defp put_html(message, %Email{html_body: nil}), do: message

  defp put_html(message, %Email{html_body: html}) do
    Map.put(message, "html", html)
  end

  defp put_mime(message, _), do: message

  defp put_recipient_address(message, %Email{to: to}) do
    Map.put(message, "recipientAddress", to_address(to))
  end

  defp put_schedule_at(message, %Email{private: %{flowmailer_schedule_at: nil}}), do: message

  defp put_schedule_at(message, %Email{private: %{flowmailer_schedule_at: schedule_at}}) do
    Map.put(message, "scheduleAt", schedule_at)
  end

  defp put_schedule_at(message, _), do: message

  defp put_sender_address(message, %Email{from: from}) do
    Map.put(message, "senderAddress", to_address(from))
  end

  defp put_subject(message, %Email{subject: nil}), do: message

  defp put_subject(message, %Email{subject: subject}) do
    Map.put(message, "subject", subject)
  end

  defp put_tags(message, %Email{private: %{flowmailer: %{tags: tags}}}) when is_list(tags) do
    Map.put(message, "tags", tags)
  end

  defp put_tags(message, _), do: message

  defp put_text(message, %Email{text_body: nil}), do: message

  defp put_text(message, %Email{text_body: text}) do
    Map.put(message, "text", text)
  end

  defp to_address({_, address}), do: address
  defp to_address([{_, address} | _]), do: address
  defp to_address(nil), do: nil
end