lib/swoosh/adapters/smtp.ex

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

  Underneath this adapter uses the
  [gen_smtp](https://github.com/gen-smtp/gen_smtp) library, add it to your mix.exs file.

  ## Example

      # mix.exs
      def deps do
        [
         {:swoosh, "~> 1.3"},
         {:gen_smtp, "~> 1.1"}
        ]
      end

      # config/config.exs
      config :sample, Sample.Mailer,
        adapter: Swoosh.Adapters.SMTP,
        relay: "smtp.avengers.com",
        username: "tonystark",
        password: "ilovepepperpotts",
        ssl: true,
        tls: :always,
        auth: :always,
        port: 1025,
        dkim: [
          s: "default", d: "domain.com",
          private_key: {:pem_plain, File.read!("priv/keys/domain.private")}
        ],
        retries: 2,
        no_mx_lookups: false

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

  ## Note

  With `STARTTLS` you should omit the ssl configuration or set it to false.

  For more details, please see [gen_smtp docs](https://hexdocs.pm/gen_smtp/readme.html)
  """

  use Swoosh.Adapter, required_config: [:relay], required_deps: [gen_smtp: :gen_smtp_client]

  alias Swoosh.Email
  alias Swoosh.Adapters.SMTP.Helpers

  def deliver(%Email{} = email, config) do
    sender = Helpers.sender(email)
    recipients = all_recipients(email)
    body = Helpers.body(email, config)

    case :gen_smtp_client.send_blocking(
           {sender, recipients, body},
           gen_smtp_config(config)
         ) do
      receipt when is_binary(receipt) -> {:ok, receipt}
      {:error, type, message} -> {:error, {type, message}}
      {:error, reason} -> {:error, reason}
    end
  end

  @config_transformations %{
    port: &String.to_integer/1,
    retries: &String.to_integer/1,
    ssl: {&String.to_atom/1, [true, false]},
    tls: {&String.to_atom/1, [:always, :never, :if_available]},
    auth: {&String.to_atom/1, [:always, :never, :if_available]},
    no_mx_lookups: {&String.to_atom/1, [true, false]}
  }
  @config_keys Map.keys(@config_transformations)

  def gen_smtp_config(config) do
    Enum.map(config, fn {key, value} -> enforce_type!(key, value) end)
  end

  defp enforce_type!(key, value) when key in @config_keys and is_binary(value) do
    case Map.get(@config_transformations, key) do
      {func, valid_values} ->
        value = func.(value)

        if value in valid_values do
          {key, value}
        else
          raise ArgumentError, """
          #{key} is not configured properly,
          got: #{value}, expected one of the following:
          #{valid_values |> Enum.map_join(", ", &inspect/1)}
          """
        end

      func ->
        {key, func.(value)}
    end
  end

  defp enforce_type!(key, value)
       when key in [:username, :password, :relay] and not is_binary(value) do
    raise ArgumentError, """
    #{key} is not configured properly,
    got: #{inspect(value)}, expected a string
    """
  end

  defp enforce_type!(key, value), do: {key, value}

  defp all_recipients(email) do
    [email.to, email.cc, email.bcc]
    |> Enum.concat()
    |> Enum.map(fn {_name, address} -> address end)
    |> Enum.uniq()
  end
end