lib/ex_aws/ses.ex

defmodule ExAws.SES do
  import ExAws.Utils, only: [camelize_key: 1, camelize_keys: 1]

  @moduledoc """
  Operations on AWS SES.

  See https://docs.aws.amazon.com/ses/latest/APIReference/Welcome.html
  """

  @notification_types [:bounce, :complaint, :delivery]
  @service :ses
  @v2_path "/v2/email"

  @doc """
  Verifies an email address.
  """
  @spec verify_email_identity(email :: binary) :: ExAws.Operation.Query.t()
  def verify_email_identity(email) do
    request(:verify_email_identity, %{"EmailAddress" => email})
  end

  @type list_identities_opt ::
          {:max_items, pos_integer}
          | {:next_token, String.t()}
          | {:identity_type, String.t()}

  @type tag :: %{Key: String.t(), Value: String.t()}
  @type list_topic :: %{String.t() => String.t()}
  @type suppression_reason :: :BOUNCE | :COMPLAINT

  @doc "List identities associated with the AWS account"
  @spec list_identities(opts :: [] | [list_identities_opt]) :: ExAws.Operation.Query.t()
  def list_identities(opts \\ []) do
    params = build_opts(opts, [:max_items, :next_token, :identity_type])
    request(:list_identities, params)
  end

  @doc """
  Fetch identities verification status and token (for domains).
  """
  @spec get_identity_verification_attributes([binary]) :: ExAws.Operation.Query.t()
  def get_identity_verification_attributes(identities) when is_list(identities) do
    params = format_member_attribute({:identities, identities})
    request(:get_identity_verification_attributes, params)
  end

  @type list_configuration_sets_opt ::
          {:max_items, pos_integer}
          | {:next_token, String.t()}

  @doc """
  Fetch configuration sets associated with AWS account.
  """
  @spec list_configuration_sets() :: ExAws.Operation.Query.t()
  @spec list_configuration_sets(opts :: [] | [list_configuration_sets_opt]) :: ExAws.Operation.Query.t()
  def list_configuration_sets(opts \\ []) do
    params = build_opts(opts, [:max_items, :next_token])
    request(:list_configuration_sets, params)
  end

  ## Contact lists
  ######################

  @doc """
  Create a contact list via the SES V2 API,
  see (https://docs.aws.amazon.com/ses/latest/APIReference-V2/).

  ## Examples

      ExAws.SES.create_contact_list(
        "Test list",
        "Test description",
        tags: [%{"Key" => "environment", "Value" => "test"}],
        topics: [
          %{
            "TopicName": "test_topic"
            "DisplayName": "Test topic",
            "Description": "Test discription",
            "DefaultSubscriptionStatus": "OPT_IN",
          }
       ]
      )

  """
  @type create_contact_list_opt ::
          {:description, String.t()}
          | {:tags, [tag]}
          | {:topics, [%{(String.t() | atom) => String.t()}]}
  @spec create_contact_list(String.t(), opts :: [create_contact_list_opt]) ::
          ExAws.Operation.JSON.t()
  def create_contact_list(list_name, opts \\ []) do
    data =
      prune_map(%{
        "ContactListName" => list_name,
        "Description" => opts[:description],
        "Tags" => opts[:tags],
        "Topics" => opts[:topics]
      })

    request_v2(:post, "contact-lists")
    |> Map.put(:data, data)
  end

  @doc """
  Update a contact list. Only accepts description and topic updates.

  ## Examples

      ExAws.SES.update_contact_list("test_list", description: "New description")

  """
  @type topic :: %{
          required(:DefaultSubscriptionStatus) => String.t(),
          optional(:Description) => String.t(),
          required(:DisplayName) => String.t(),
          required(:TopicName) => String.t()
        }
  @type update_contact_list_opt ::
          {:description, String.t()}
          | {:topics, [topic]}
  @spec update_contact_list(String.t(), opts :: [update_contact_list_opt]) :: ExAws.Operation.JSON.t()
  def update_contact_list(list_name, opts \\ []) do
    data =
      prune_map(%{
        "ContactListName" => list_name,
        "Description" => opts[:description],
        "Topics" => opts[:topics]
      })

    request_v2(:put, "contact-lists/#{list_name}")
    |> Map.put(:data, data)
  end

  @doc """
  List contact lists.

  The API accepts pagination parameters, but they're redundant as AWS limits
  usage to a single list per account.
  """
  @spec list_contact_lists() :: ExAws.Operation.JSON.t()
  def list_contact_lists() do
    request_v2(:get, "contact-lists")
  end

  @doc """
  Show contact list.
  """
  @spec get_contact_list(String.t()) :: ExAws.Operation.JSON.t()
  def get_contact_list(list_name) do
    request_v2(:get, "contact-lists/#{list_name}")
  end

  @doc """
  Delete contact list.
  """
  @spec delete_contact_list(String.t()) :: ExAws.Operation.JSON.t()
  def delete_contact_list(list_name) do
    request_v2(:delete, "contact-lists/#{list_name}")
  end

  ## Contacts
  ######################

  @doc """
  Create a new contact in a contact list.

  Options:

  * `:attributes` - arbitrary string to be assigned to AWS SES Contact AttributesData
  * `:topic_preferences` - list of maps for subscriptions to topics.
    SubscriptionStatus should be one of "OPT_IN" or "OPT_OUT"
  * `:unsubscribe_all` - causes contact to be unsubscribed from all topics
  """
  @type topic_preference :: %{
          TopicName: String.t(),
          SubscriptionStatus: String.t()
        }
  @type contact_opt ::
          {:attributes, String.t()}
          | {:topic_preferences, [topic_preference]}
          | {:unsubscribe_all, Boolean.t()}
  @spec create_contact(String.t(), email_address, [contact_opt]) :: ExAws.Operation.JSON.t()
  def create_contact(list_name, email, opts \\ []) do
    data =
      prune_map(%{
        "EmailAddress" => email,
        "TopicPreferences" => opts[:topic_preferences],
        "AttributesData" => opts[:attributes],
        "UnsubscribeAll" => opts[:unsubscribe_all]
      })

    request_v2(:post, "contact-lists/#{list_name}/contacts")
    |> Map.put(:data, data)
  end

  @doc """
  Update a contact in a contact list.
  """
  @spec update_contact(String.t(), email_address, [contact_opt]) :: ExAws.Operation.JSON.t()
  def update_contact(list_name, email, opts \\ []) do
    data =
      prune_map(%{
        "TopicPreferences" => opts[:topic_preferences],
        "AttributesData" => opts[:attributes],
        "UnsubscribeAll" => opts[:unsubscribe_all]
      })

    uri_encoded_email = ExAws.Request.Url.uri_encode(email)

    request_v2(:put, "contact-lists/#{list_name}/contacts/#{uri_encoded_email}")
    |> Map.put(:data, data)
  end

  @doc """
  Show contacts in contact list.
  """
  @spec list_contacts(String.t()) :: ExAws.Operation.JSON.t()
  def list_contacts(list_name) do
    request_v2(:get, "contact-lists/#{list_name}/contacts")
  end

  @doc """
  Show a contact in a contact list.
  """
  @spec get_contact(String.t(), email_address) :: ExAws.Operation.JSON.t()
  def get_contact(list_name, email) do
    uri_encoded_email = ExAws.Request.Url.uri_encode(email)
    request_v2(:get, "contact-lists/#{list_name}/contacts/#{uri_encoded_email}")
  end

  @doc """
  Delete a contact in a contact list.
  """
  @spec delete_contact(String.t(), email_address) :: ExAws.Operation.JSON.t()
  def delete_contact(list_name, email) do
    uri_encoded_email = ExAws.Request.Url.uri_encode(email)
    request_v2(:delete, "contact-lists/#{list_name}/contacts/#{uri_encoded_email}")
  end

  @doc """
  Create a bulk import job to import contacts from S3.

  Params:

  * `:import_data_source`
  * `:import_destination` - requires either a `ContactListDestination` or
    `SuppressionListDestination` map.
  """
  @type import_data_source :: %{DataFormat: String.t(), S3Url: String.t()}
  @type contact_list_destination :: %{
          ContactListImportAction: String.t(),
          ContactListName: String.t()
        }
  @type suppression_list_destination :: %{SuppressionListImportAction: String.t()}
  @type import_destination :: %{
          optional(:ContactListDestination) => contact_list_destination(),
          optional(:SuppressionListDestination) => suppression_list_destination()
        }
  @spec create_import_job(import_data_source(), import_destination()) :: ExAws.Operation.JSON.t()
  def create_import_job(data_source, destination) do
    data = %{
      ImportDataSource: data_source,
      ImportDestination: destination
    }

    request_v2(:post, "import-jobs")
    |> Map.put(:data, data)
  end

  ## Suppression Lists
  ######################
  @doc """
  Add an email address to list of suppressed destinations. A suppression reason
  is mandatory (see `t:suppression_reason()`).
  """
  @spec put_suppressed_destination(String.t(), SuppressionReason.t()) :: ExAws.Operation.JSON.t()
  def put_suppressed_destination(email_address, suppression_reason) do
    request_v2(:put, "suppression/addresses")
    |> Map.put(:data, %{
      EmailAddress: email_address,
      Reason: suppression_reason
    })
  end

  @doc """
  Delete an email address from list of suppressed destinations.
  """
  @spec delete_suppressed_destination(String.t()) :: ExAws.Operation.JSON.t()
  def delete_suppressed_destination(email_address) do
    uri_encoded_email_address = ExAws.Request.Url.uri_encode(email_address)

    request_v2(:delete, "suppression/addresses/#{uri_encoded_email_address}")
  end

  ## Templates
  ######################

  @doc "Get email template"
  @spec get_template(String.t()) :: ExAws.Operation.Query.t()
  def get_template(template_name) do
    request(:get_template, %{"TemplateName" => template_name})
  end

  @type list_templates_opt ::
          {:max_items, pos_integer}
          | {:next_token, String.t()}

  @doc """
  List email templates.
  """
  @spec list_templates(opts :: [] | [list_templates_opt]) :: ExAws.Operation.Query.t()
  def list_templates(opts \\ []) do
    params = build_opts(opts, [:max_items, :next_token])
    request(:list_templates, params)
  end

  @doc """
  Creates an email template.
  """
  @type create_template_opt :: {:configuration_set_name, String.t()}
  @spec create_template(String.t(), String.t(), String.t(), String.t(), opts :: [create_template_opt]) ::
          ExAws.Operation.Query.t()
  def create_template(template_name, subject, html, text, opts \\ []) do
    template =
      %{
        "TemplateName" => template_name,
        "SubjectPart" => subject
      }
      |> put_if_not_nil("HtmlPart", html)
      |> put_if_not_nil("TextPart", text)
      |> flatten_attrs("Template")

    params =
      opts
      |> build_opts([:configuration_set_name])
      |> Map.merge(template)

    request(:create_template, params)
  end

  @doc """
  Updates an email template.
  """
  @type update_template_opt :: {:configuration_set_name, String.t()}
  @spec update_template(String.t(), String.t(), String.t(), String.t(), opts :: [update_template_opt]) ::
          ExAws.Operation.Query.t()
  def update_template(template_name, subject, html, text, opts \\ []) do
    template =
      %{
        "TemplateName" => template_name,
        "SubjectPart" => subject
      }
      |> put_if_not_nil("HtmlPart", html)
      |> put_if_not_nil("TextPart", text)
      |> flatten_attrs("Template")

    params =
      opts
      |> build_opts([:configuration_set_name])
      |> Map.merge(template)

    request(:update_template, params)
  end

  @doc """
  Deletes an email template.
  """
  @spec delete_template(binary) :: ExAws.Operation.Query.t()
  def delete_template(template_name) do
    params = %{
      "TemplateName" => template_name
    }

    request(:delete_template, params)
  end

  ## Emails
  ######################

  @type email_address :: binary

  @type message :: %{
          body: %{html: %{data: binary, charset: binary}, text: %{data: binary, charset: binary}},
          subject: %{data: binary, charset: binary}
        }
  @type destination :: %{to: [email_address], cc: [email_address], bcc: [email_address]}

  @type bulk_destination :: [%{destination: destination, replacement_template_data: binary}]

  @type send_email_opt ::
          {:configuration_set_name, String.t()}
          | {:reply_to, [email_address]}
          | {:return_path, String.t()}
          | {:return_path_arn, String.t()}
          | {:source, String.t()}
          | {:source_arn, String.t()}
          | {:tags, %{(String.t() | atom) => String.t()}}

  @doc """
  Composes an email message.
  """
  @spec send_email(dst :: destination, msg :: message, src :: binary) :: ExAws.Operation.Query.t()
  @spec send_email(dst :: destination, msg :: message, src :: binary, opts :: [send_email_opt]) ::
          ExAws.Operation.Query.t()
  def send_email(dst, msg, src, opts \\ []) do
    params =
      opts
      |> build_opts([:configuration_set_name, :return_path, :return_path_arn, :source_arn, :bcc])
      |> Map.merge(format_member_attribute(:reply_to_addresses, opts[:reply_to]))
      |> Map.merge(flatten_attrs(msg, "message"))
      |> Map.merge(format_tags(opts[:tags]))
      |> Map.merge(format_dst(dst))
      |> Map.put_new("Source", src)

    request(:send_email, params)
  end

  @doc """
  Send an email via the SES V2 API, which supports list management.

  `:content` should include one of a `Raw`, `Simple`, or `Template` key.
  """
  @type destination_v2 :: %{
          optional(:ToAddresses) => [email_address],
          optional(:CcAddresses) => [email_address],
          optional(:BccAddresses) => [email_address]
        }
  @type email_field :: %{optional(:Charset) => String.t(), required(:Data) => String.t()}
  @type email_content :: %{
          optional(:Raw) => %{Data: binary},
          optional(:Simple) => %{Body: %{Html: email_field, Text: email_field}, Subject: email_field},
          optional(:Template) => %{TemplateArn: String.t(), TemplateData: String.t(), TemplateName: String.t()}
        }
  @type(
    send_email_v2_opt ::
      {:configuration_set_name, String.t()}
      | {:tags, [tag]}
      | {:feedback_forwarding_address, String.t()}
      | {:feedback_forwarding_arn, String.t()}
      | {:from_arn, String.t()}
      | {:list_management, %{ContactListName: String.t(), TopicName: String.t()}}
      | {:reply_addresses, [String.t()]},
    @spec(send_email_v2(destination_v2, email_content, email_address, [send_email_v2_opt]))
  )
  def send_email_v2(destination, content, from_email, opts \\ []) do
    data =
      prune_map(%{
        ConfigurationSetName: opts[:configuration_set_name],
        Content: content,
        Destination: destination,
        EmailTags: opts[:tags],
        FeedbackForwardingEmailAddress: opts[:feedback_forwarding_address],
        FeedbackForwardingEmailAddressIdentityArn: opts[:feedback_forwarding_arn],
        FromEmailAddress: from_email,
        FromEmailAddressIdentityArn: opts[:from_arn],
        ListManagementOptions: opts[:list_management],
        ReplyToAddresses: opts[:reply_addresses]
      })

    request_v2(:post, "outbound-emails")
    |> Map.put(:data, data)
  end

  @doc """
  Send a raw Email.
  """
  @type send_raw_email_opt ::
          {:configuration_set_name, String.t()}
          | {:from_arn, String.t()}
          | {:return_path_arn, String.t()}
          | {:source, String.t()}
          | {:source_arn, String.t()}
          | {:tags, %{(String.t() | atom) => String.t()}}

  @spec send_raw_email(binary, opts :: [send_raw_email_opt]) :: ExAws.Operation.Query.t()
  def send_raw_email(raw_msg, opts \\ []) do
    params =
      opts
      |> build_opts([:configuration_set_name, :from_arn, :return_path_arn, :source, :source_arn])
      |> Map.merge(format_tags(opts[:tags]))
      |> Map.put("RawMessage.Data", Base.encode64(raw_msg))

    request(:send_raw_email, params)
  end

  @doc """
  Send a templated Email.
  """
  @type send_templated_email_opt ::
          {:configuration_set_name, String.t()}
          | {:return_path, String.t()}
          | {:return_path_arn, String.t()}
          | {:source, String.t()}
          | {:source_arn, String.t()}
          | {:reply_to, [email_address]}
          | {:tags, %{(String.t() | atom) => String.t()}}

  @spec send_templated_email(
          dst :: destination,
          src :: binary,
          template :: binary,
          template_data :: binary,
          opts :: [send_templated_email_opt]
        ) :: ExAws.Operation.Query.t()
  def send_templated_email(dst, src, template, template_data, opts \\ []) do
    params =
      opts
      |> build_opts([:configuration_set_name, :return_path, :return_path_arn, :source_arn, :bcc])
      |> Map.merge(format_member_attribute(:reply_to_addresses, opts[:reply_to]))
      |> Map.merge(format_tags(opts[:tags]))
      |> Map.merge(format_dst(dst))
      |> Map.put("Source", src)
      |> Map.put("Template", template)
      |> Map.put("TemplateData", format_template_data(template_data))

    request(:send_templated_email, params)
  end

  @doc """
  Send a templated email to multiple destinations.
  """
  @type send_bulk_templated_email_opt ::
          {:configuration_set_name, String.t()}
          | {:return_path, String.t()}
          | {:return_path_arn, String.t()}
          | {:source_arn, String.t()}
          | {:default_template_data, String.t()}
          | {:reply_to, [email_address]}
          | {:tags, %{(String.t() | atom) => String.t()}}

  @spec send_bulk_templated_email(
          template :: binary,
          source :: binary,
          destinations :: bulk_destination,
          opts :: [send_bulk_templated_email_opt]
        ) :: ExAws.Operation.Query.t()
  def send_bulk_templated_email(template, source, destinations, opts \\ []) do
    params =
      opts
      |> build_opts([:configuration_set_name, :return_path, :return_path_arn, :source_arn, :default_template_data])
      |> Map.merge(format_member_attribute(:reply_to_addresses, opts[:reply_to]))
      |> Map.merge(format_tags(opts[:tags]))
      |> Map.merge(format_bulk_destinations(destinations))
      |> Map.put("DefaultTemplateData", format_template_data(opts[:default_template_data]))
      |> Map.put("Source", source)
      |> Map.put("Template", template)

    request(:send_bulk_templated_email, params)
  end

  @doc """
  Deletes the specified identity (an email address or a domain) from the list
  of verified identities.
  """
  @spec delete_identity(binary) :: ExAws.Operation.Query.t()
  def delete_identity(identity) do
    request(:delete_identity, %{"Identity" => identity})
  end

  @type set_identity_notification_topic_opt :: {:sns_topic, binary}
  @type notification_type :: :bounce | :complaint | :delivery

  @doc """
  Sets the Amazon Simple Notification Service (Amazon SNS) topic to which
  Amazon SES will publish  delivery notifications for emails sent with given
  identity.

  Absent `:sns_topic` options cleans SnsTopic and disables publishing.

  Notification type can be on of the `:bounce`, `:complaint`, or `:delivery`.
  Requests are throttled to one per second.
  """
  @spec set_identity_notification_topic(binary, notification_type, set_identity_notification_topic_opt | []) ::
          ExAws.Operation.Query.t()
  def set_identity_notification_topic(identity, type, opts \\ []) when type in @notification_types do
    notification_type = Atom.to_string(type) |> String.capitalize()

    params =
      opts
      |> build_opts([:sns_topic])
      |> Map.merge(%{"Identity" => identity, "NotificationType" => notification_type})

    request(:set_identity_notification_topic, params)
  end

  @doc """
  Enables or disables whether Amazon SES forwards notifications as email.
  """
  @spec set_identity_feedback_forwarding_enabled(boolean, binary) :: ExAws.Operation.Query.t()
  def set_identity_feedback_forwarding_enabled(enabled, identity) do
    request(:set_identity_feedback_forwarding_enabled, %{"ForwardingEnabled" => enabled, "Identity" => identity})
  end

  @doc """
  Build message object.
  """
  @spec build_message(binary, binary, binary, binary) :: message
  def build_message(html, txt, subject, charset \\ "UTF-8") do
    %{
      body: %{
        html: %{data: html, charset: charset},
        text: %{data: txt, charset: charset}
      },
      subject: %{data: subject, charset: charset}
    }
  end

  @doc """
  Set whether SNS notifications should include original email headers or not.
  """
  @spec set_identity_headers_in_notifications_enabled(binary, notification_type, boolean) :: ExAws.Operation.Query.t()
  def set_identity_headers_in_notifications_enabled(identity, type, enabled) do
    notification_type = Atom.to_string(type) |> String.capitalize()

    request(
      :set_identity_headers_in_notifications_enabled,
      %{"Identity" => identity, "NotificationType" => notification_type, "Enabled" => enabled}
    )
  end

  @doc "Create a custom verification email template."
  @spec create_custom_verification_email_template(
          String.t(),
          String.t(),
          String.t(),
          String.t(),
          String.t(),
          String.t()
        ) :: ExAws.Operation.Query.t()
  def create_custom_verification_email_template(
        template_name,
        from_email_address,
        template_subject,
        template_content,
        success_redirection_url,
        failure_redirection_url
      ) do
    request(:create_custom_verification_email_template, %{
      "TemplateName" => template_name,
      "FromEmailAddress" => from_email_address,
      "TemplateSubject" => template_subject,
      "TemplateContent" => template_content,
      "SuccessRedirectionURL" => success_redirection_url,
      "FailureRedirectionURL" => failure_redirection_url
    })
  end

  @type update_custom_verification_email_template_opt ::
          {:template_name, String.t()}
          | {:from_email_address, String.t()}
          | {:template_subject, String.t()}
          | {:template_content, String.t()}
          | {:success_redirection_url, String.t()}
          | {:failure_redirection_url, String.t()}
  @doc "Update or create a custom verification email template."
  @spec update_custom_verification_email_template(opts :: [update_custom_verification_email_template_opt] | []) ::
          ExAws.Operation.Query.t()
  def update_custom_verification_email_template(opts \\ []) do
    params =
      opts
      |> build_opts([
        :template_name,
        :from_email_address,
        :template_subject,
        :template_content
      ])
      |> maybe_put_param(opts, :success_redirection_url, "SuccessRedirectionURL")
      |> maybe_put_param(opts, :failure_redirection_url, "FailureRedirectionURL")

    request(:update_custom_verification_email_template, params)
  end

  @doc "Delete custom verification email template."
  @spec delete_custom_verification_email_template(String.t()) :: ExAws.Operation.Query.t()
  def delete_custom_verification_email_template(template_name) do
    request(:delete_custom_verification_email_template, %{"TemplateName" => template_name})
  end

  @type list_custom_verification_email_templates_opt :: {:max_results, String.t()} | {:next_token, String.t()}
  @doc "Lists custom verification email templates."
  @spec list_custom_verification_email_templates(opts :: [list_custom_verification_email_templates_opt()] | []) ::
          ExAws.Operation.Query.t()
  def list_custom_verification_email_templates(opts \\ []) do
    params = build_opts(opts, [:max_results, :next_token])
    request(:list_custom_verification_email_templates, params)
  end

  @type send_custom_verification_email_opt :: {:configuration_set_name, String.t()}
  @doc "Send a verification email using a custom template."
  @spec send_custom_verification_email(String.t(), String.t(), opts :: [send_custom_verification_email_opt] | []) ::
          ExAws.Operation.Query.t()
  def send_custom_verification_email(email_address, template_name, opts \\ []) do
    params =
      opts
      |> build_opts([:configuration_set_name])
      |> Map.put("EmailAddress", email_address)
      |> Map.put("TemplateName", template_name)

    request(:send_custom_verification_email, params)
  end

  defp format_dst(dst, root \\ "destination") do
    dst =
      Enum.reduce([:to, :bcc, :cc], %{}, fn key, acc ->
        case Map.fetch(dst, key) do
          {:ok, val} -> Map.put(acc, :"#{key}_addresses", val)
          _ -> acc
        end
      end)

    dst
    |> Map.to_list()
    |> format_member_attributes([:bcc_addresses, :cc_addresses, :to_addresses])
    |> flatten_attrs(root)
  end

  defp format_template_data(nil), do: "{}"

  defp format_template_data(template_data), do: Map.get(aws_base_config(), :json_codec).encode!(template_data)

  defp format_bulk_destinations(destinations) do
    destinations
    |> Enum.with_index(1)
    |> Enum.flat_map(fn
      {%{destination: destination} = destination_member, index} ->
        root = "Destinations.member.#{index}"

        destination
        |> format_dst("#{root}.Destination")
        |> add_replacement_template_data(destination_member, root)
        |> Map.to_list()
    end)
    |> Map.new()
  end

  defp add_replacement_template_data(destination, %{replacement_template_data: replacement_template_data}, root) do
    destination
    |> Map.put("#{root}.ReplacementTemplateData", format_template_data(replacement_template_data))
  end

  defp add_replacement_template_data(destination, _, _), do: destination

  defp format_tags(nil), do: %{}

  defp format_tags(tags) do
    tags
    |> Enum.with_index(1)
    |> Enum.reduce(%{}, fn {tag, index}, acc ->
      key = camelize_key("tags.member.#{index}")
      Map.merge(acc, flatten_attrs(tag, key))
    end)
  end

  ## Request
  ######################

  defp request(action, params) do
    action_string = action |> Atom.to_string() |> Macro.camelize()

    %ExAws.Operation.Query{
      path: "/",
      params: params |> Map.put("Action", action_string),
      service: @service,
      action: action,
      parser: &ExAws.SES.Parsers.parse/2
    }
  end

  defp request_v2(method) do
    %ExAws.Operation.JSON{
      http_method: method,
      path: @v2_path,
      service: @service
    }
  end

  defp request_v2(method, resource) do
    request_v2(method)
    |> Map.put(:path, @v2_path <> "/#{resource}")
  end

  defp build_opts(opts, permitted) do
    opts
    |> Map.new()
    |> Map.take(permitted)
    |> camelize_keys
  end

  defp maybe_put_param(params, opts, key, name) do
    case opts[key] do
      nil -> params
      value -> Map.put(params, name, value)
    end
  end

  defp format_member_attributes(opts, members) do
    opts
    |> Map.new()
    |> Map.take(members)
    |> Enum.reduce(Map.new(opts), fn entry, acc -> Map.merge(acc, format_member_attribute(entry)) end)
    |> Map.drop(members)
  end

  defp format_member_attribute(key, collection), do: format_member_attribute({key, collection})

  defp format_member_attribute({_, nil}), do: %{}

  defp format_member_attribute({key, collection}) do
    collection
    |> Enum.with_index(1)
    |> Map.new(fn {item, index} ->
      {"#{camelize_key(key)}.member.#{index}", item}
    end)
  end

  defp flatten_attrs(attrs, root) do
    do_flatten_attrs({attrs, camelize_key(root)})
    |> List.flatten()
    |> Map.new()
  end

  defp do_flatten_attrs({attrs, root}) when is_map(attrs) do
    Enum.map(attrs, fn {k, v} ->
      do_flatten_attrs({v, root <> "." <> camelize_key(k)})
    end)
  end

  defp do_flatten_attrs({val, path}) do
    {camelize_key(path), val}
  end

  defp put_if_not_nil(map, _, nil), do: map
  defp put_if_not_nil(map, key, value), do: map |> Map.put(key, value)

  defp aws_base_config(), do: ExAws.Config.build_base(@service)

  defp prune_map(map) do
    map
    |> Enum.reject(fn {_k, v} -> is_nil(v) end)
    |> Map.new()
  end
end