defmodule Swoosh.Mailer do @moduledoc ~S""" Defines a mailer. A mailer is a wrapper around an adapter that makes it easy for you to swap the adapter without having to change your code. It is also responsible for doing some sanity checks before handing down the email to the adapter. When used, the mailer expects `:otp_app` as an option. The `:otp_app` should point to an OTP application that has the mailer configuration. For example, the mailer: defmodule Sample.Mailer do use Swoosh.Mailer, otp_app: :sample end Could be configured with: config :sample, Sample.Mailer, adapter: Swoosh.Adapters.Sendgrid, api_key: "SG.x.x" Most of the configuration that goes into the config is specific to the adapter, so check the adapter's documentation for more information. Per module configuration is also supported, it has priority over mix configs: defmodule Sample.Mailer do use Swoosh.Mailer, otp_app: :sample, adapter: Swoosh.Adapters.Sendgrid, api_key: "SG.x.x" end ## Usage Once configured you can use your mailer like this: # in an IEx console iex> email = new |> from("tony.stark@example.com") |> to("steve.rogers@example.com") %Swoosh.Email{from: {"", "tony.stark@example.com"}, ...} iex> Mailer.deliver(email) {:ok, %{...}} ## Dynamic config You can also pass an extra config argument to `deliver/2` that will be merged with your Mailer's config: # in an IEx console iex> email = new |> from("tony.stark@example.com") |> to("steve.rogers@example.com") %Swoosh.Email{from: {"", "tony.stark@example.com"}, ...} iex> Mailer.deliver(email, domain: "jarvis.com") {:ok, %{...}} ## Telemetry Each mailer outputs the following telemetry events: - `[:swoosh, :deliver, :start]`: occurs when `Mailer.deliver/2` begins. - `[:swoosh, :deliver, :stop]`: occurs when `Mailer.deliver/2` completes. - `[:swoosh, :deliver, :exception]`: occurs when `Mailer.deliver/2` throws an exception. - `[:swoosh, :deliver_many, :start]`: occurs when `Mailer.deliver_many/2` begins. - `[:swoosh, :deliver_many, :stop]`: occurs when `Mailer.deliver_many/2` completes. - `[:swoosh, :deliver_many, :exception]`: occurs when `Mailer.deliver_many/2` throws an exception. ### Capturing events You can capture events by calling `:telemetry.attach/4` or `:telemetry.attach_many/4`. Here's an example: # tracks the number of emails sent successfully/errored defmodule MyHandler do def handle_event([:swoosh, :deliver, :stop], _measurements, metadata, _config) do if Map.get(metadata, :error) do StatsD.increment("mail.sent.failure", 1, %{mailer: metadata.mailer}) else StatsD.increment("mail.sent.success", 1, %{mailer: metadata.mailer}) end end def handle_event([:swoosh, :deliver, :exception], _measurements, metadata, _config) do StatsD.increment("mail.sent.failure", 1, %{mailer: metadata.mailer}) end def handle_event([:swoosh, :deliver_many, :stop], _measurements, metadata, _config) do if Map.get(metadata, :error) do StatsD.increment("mail.sent.failure", length(metadata.emails), %{mailer: metadata.mailer}) else StatsD.increment("mail.sent.success", length(metadata.emails), %{mailer: metadata.mailer}) end end def handle_event([:swoosh, :deliver_many, :exception], _measurements, metadata, _config) do StatsD.increment("mail.sent.failure", length(metadata.emails), %{mailer: metadata.mailer}) end end in `c:Application.start/2` callback: :telemetry.attach_many("my-handler", [ [:swoosh, :deliver, :stop], [:swoosh, :deliver, :exception], [:swoosh, :deliver_many, :stop], [:swoosh, :deliver_many, :exception], ], &MyHandler.handle_event/4, nil) """ alias Swoosh.DeliveryError defmacro __using__(opts) do quote bind_quoted: [opts: opts] do alias Swoosh.Mailer @otp_app Keyword.fetch!(opts, :otp_app) @mailer_config opts Swoosh.Mailer.deprecated_system_env(@otp_app, __MODULE__) @doc ~S""" Delivers an email. If the email is delivered it returns an `{:ok, result}` tuple. If it fails, returns an `{:error, error}` tuple. """ @spec deliver(Swoosh.Email.t(), Keyword.t()) :: {:ok, term} | {:error, term} def deliver(email, config \\ []) def deliver(email, config) do metadata = %{email: email, config: config} instrument(:deliver, metadata, fn -> Mailer.deliver(email, parse_config(config)) end) end @doc ~S""" Delivers an email, raises on error. If the email is delivered, it returns the result. If it fails, it raises a `DeliveryError`. """ @spec deliver!(Swoosh.Email.t(), Keyword.t()) :: term | no_return def deliver!(email, config \\ []) def deliver!(email, config) do case deliver(email, config) do {:ok, result} -> result {:error, reason} -> raise DeliveryError, reason: reason end end @doc ~S""" Delivers a list of emails. It accepts a list of `%Swoosh.Email{}` as its first parameter. """ @spec deliver_many(list(%Swoosh.Email{}), Keyword.t()) :: {:ok, term} | {:error, term} def deliver_many(emails, config \\ []) def deliver_many(emails, config) do metadata = %{emails: emails, config: config} instrument(:deliver_many, metadata, fn -> Mailer.deliver_many(emails, parse_config(config)) end) end @on_load :validate_dependency @doc false def validate_dependency do adapter = Keyword.get(parse_config([]), :adapter) Mailer.validate_dependency(adapter) end defp parse_config(config) do Mailer.parse_config(@otp_app, __MODULE__, @mailer_config, config) end defp instrument(key, metadata, fun) do metadata = Map.merge(metadata, %{mailer: __MODULE__}) :telemetry.span([:swoosh, key], metadata, fn -> case fun.() do {:ok, result} -> {{:ok, result}, Map.put(metadata, :result, result)} {:error, error} -> {{:error, error}, Map.put(metadata, :error, error)} end end) end end end def deliver(%Swoosh.Email{from: nil}, _config) do {:error, :from_not_set} end def deliver(%Swoosh.Email{from: {_name, address}}, _config) when address in ["", nil] do {:error, :from_not_set} end @doc "Delivers an email." def deliver(%Swoosh.Email{} = email, config) do adapter = Keyword.fetch!(config, :adapter) :ok = adapter.validate_config(config) adapter.deliver(email, config) end @doc """ The implementation for `deliver_many/2` is on case-by-case basis. Check the adapter that you use to see if it has `deliver_many/2` implemented. """ def deliver_many(emails, config) do adapter = Keyword.fetch!(config, :adapter) :ok = adapter.validate_config(config) adapter.deliver_many(emails, config) end @doc false def parse_config(otp_app, mailer, mailer_config, dynamic_config) do Application.get_env(otp_app, mailer, []) |> Keyword.merge(mailer_config) |> Keyword.merge(dynamic_config) |> interpolate_env_vars() end @doc false def deprecated_system_env(otp_app, mailer) do config = Application.get_env(otp_app, mailer, []) if Keyword.keyword?(config) do Enum.each(config, fn {key, {:system, _env_var}} -> IO.warn( "Using {:system, env_var} to configure Swoosh's #{inspect(mailer)} is deprecated, " <> "please use config/runtime.exs instead (found under key #{inspect(key)})" ) {_key, _value} -> :ok end) end end defp interpolate_env_vars(config) do Enum.map(config, fn {key, {:system, env_var}} -> {key, System.get_env(env_var)} {key, value} -> {key, value} end) end @doc false def validate_dependency(adapter) do require Logger with adapter when not is_nil(adapter) <- adapter, {:module, _} <- Code.ensure_compiled(adapter), true <- function_exported?(adapter, :validate_dependency, 0), :ok <- adapter.validate_dependency() do :ok else no_match when no_match in [nil, false] -> :ok {:error, :nofile} -> Logger.error("#{adapter} does not exist") :abort {:error, deps} when is_list(deps) -> Logger.error(Swoosh.Mailer.missing_deps_message(adapter, deps)) :abort end end @doc false def missing_deps_message(adapter, deps) do deps = deps |> Enum.map(fn {lib, module} -> "#{module} from #{inspect(lib)}" module -> inspect(module) end) |> Enum.map(&"\n- #{&1}") """ The following dependencies are required to use #{inspect(adapter)}: #{deps} """ end end