# `PhoenixKit.Mailer`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.14.2/lib/phoenix_kit/mailer.ex#L1)

Mailer module for PhoenixKit emails.

This module handles sending emails such as
confirmation emails, password reset emails, magic link emails, etc.

It can work in two modes:
1. **Built-in mode**: Uses PhoenixKit's own Swoosh mailer (default)
2. **Delegation mode**: Uses the parent application's mailer when configured

## Configuration

To use your application's mailer instead of PhoenixKit's built-in one:

    config :phoenix_kit,
      mailer: MyApp.Mailer

When delegation is configured, all emails will be sent through your application's
mailer, allowing you to use a single mailer configuration across your entire application.

# `deliver`

```elixir
@spec deliver(Swoosh.Email.t(), Keyword.t()) :: {:ok, term()} | {:error, term()}
```

Delivers an email.

If the email is delivered it returns an `{:ok, result}` tuple. If it fails,
returns an `{:error, error}` tuple.

# `deliver!`

```elixir
@spec deliver!(Swoosh.Email.t(), Keyword.t()) :: term() | no_return()
```

Delivers an email, raises on error.

If the email is delivered, it returns the result. If it fails, it raises
a `DeliveryError`.

# `deliver_email`

Delivers an email using the appropriate mailer.

If Settings key `"default_email_integration_uuid"` is set and resolves to
an Integrations connection with valid credentials, delivery is routed
through that connection via `deliver_via_integration/3` (set on the core
Email Sending settings page). Otherwise, if a parent application mailer is
configured, delegates to it; failing that, uses the built-in PhoenixKit
mailer. The setting being absent, blank, or pointing at a
deleted/unconfigured connection is a no-op — behavior is unchanged from
before this routing existed.

This function also integrates with the email tracking system to log
outgoing emails when tracking is enabled. Recipients blocklisted by the
emails module (hard bounces, complaints, manual blocks — in `to`, `cc` or
`bcc`) are rejected before any tracking or delivery is attempted; see
`check_recipient_allowed/1`. Send-rate limits are deliberately NOT enforced
here — see the soft-dependency note at the top of this module.

## Return shape

Normally the adapter's own `{:ok, metadata}` (metadata shape is adapter
specific — `%{id: message_id}` for most). When an email provider takes the
message over for later delivery (`maybe_enqueue/2`), the result is instead
`{:ok, %{id: ref, queued: true}}`, where `ref` identifies the queued message,
not a message id from a relay that has not seen it yet. Callers that only
match `{:ok, _}` are unaffected; a caller that needs the message sent on this
process can pass `skip_queue: true`.

When the resolved transport is the local dev mailbox and the
`dev_mailbox_enabled` setting is off (its default — see issue #687), the
message is written to the server log instead of being handed to any adapter,
and the result is `{:ok, %{id: "dev-mailbox-suppressed", suppressed: true}}`.
It reads as success on purpose — auth flows must not error over a dev-only
transport — but no mail was sent and nothing was recorded by tracking.

# `deliver_many`

```elixir
@spec deliver_many(
  [
    %Swoosh.Email{
      assigns: term(),
      attachments: term(),
      bcc: term(),
      cc: term(),
      from: term(),
      headers: term(),
      html_body: term(),
      private: term(),
      provider_options: term(),
      reply_to: term(),
      subject: term(),
      text_body: term(),
      to: term()
    }
  ],
  Keyword.t()
) :: {:ok, term()} | {:error, term()}
```

Delivers a list of emails.

It accepts a list of `%Swoosh.Email{}` as its first parameter.

# `deliver_via_integration`

```elixir
@spec deliver_via_integration(Swoosh.Email.t(), String.t(), keyword()) ::
  {:ok, term()} | {:error, term()}
```

Delivers an email via a specific Integrations connection (AWS SES,
universal SMTP, or Brevo API), selected by the connection's `uuid`.

Unlike `deliver_email/2`, this does **not** go through
`deliver_with_runtime_config/2` — that path is hardcoded to AWS SES
(`config[:adapter] == Swoosh.Adapters.AmazonSES`, credentials only from
`Provider.current().get_aws_*`), so a Brevo or SMTP send routed through
it would be misrouted or ignored. This function resolves the Swoosh
adapter and config directly from the chosen integration's stored
credentials instead, while preserving the same interception seam
`deliver_email/2` uses so tracking keeps working.

## Returns

- `{:ok, term()}` — delivered
- `{:error, {:blocked, atom()}}` — a recipient (`to`/`cc`/`bcc`) is
  blocklisted by the emails module (checked before the integration is even
  resolved). Send-rate limits are NOT enforced here — see the module's
  soft-dependency note.
- `{:error, :not_configured | :deleted}` — the integration uuid didn't resolve
- `{:error, {:incomplete_credentials, [String.t()]}}` — the connection's
  `status` says connected, but a required field (e.g. SMTP `host`, SES
  `aws_region`) is blank — most likely edited after the last successful
  validation. Listed field names are the credential keys, never values.
- `{:error, {:unsupported_provider, String.t()} | :unsupported_provider}` —
  the integration's provider has no known Swoosh adapter mapping (the bare
  atom when the credentials carry no provider key at all)
- `{:error, {:invalid_smtp_port, term()}}` — the SMTP connection's port is
  not a number
- `{:error, {:invalid_security | :invalid_verify_cert | :invalid_auth |
  :invalid_timeout, term()}}` / `{:error, :invalid_ca_cert}` — SMTP only: one
  of the operator's transport settings does not parse. Unknown values are
  refused rather than coerced back to the default, so a typo can't silently
  downgrade the connection's encryption
- `{:error, :no_ca_store}` — SMTP only, and a **behaviour change**: there is no
  system CA bundle, so the relay's certificate cannot be verified and the
  password would go out to an unauthenticated server. Sending stops. It used to
  proceed with `verify: :verify_none`, which is why slim images (distroless,
  scratch, some Alpine builds) never noticed they had no CA store. Install one
  (e.g. `ca-certificates`) to restore sending. A relay with no credentials to
  protect still degrades rather than failing.

# `get_from_email`

```elixir
@spec get_from_email() :: String.t()
```

Gets the effective "from" email address.

Priority: Settings Database (runtime) > Config file (compile-time) >
built-in default (`"noreply@localhost"`). Public so the Email Sending
settings page can display the value that's actually in effect, even
when no Settings override is set.

# `get_from_name`

```elixir
@spec get_from_name() :: String.t()
```

Gets the effective "from" name.

Priority: Settings Database (runtime) > Config file (compile-time) >
built-in default (`"PhoenixKit"`). Public so the Email Sending settings
page can display the value that's actually in effect, even when no
Settings override is set.

# `get_mailer`

Gets the mailer module to use for sending emails.

Returns the configured parent application mailer if set,
otherwise returns the built-in PhoenixKit.Mailer.

## Examples

    iex> PhoenixKit.Mailer.get_mailer()
    MyApp.Mailer  # if configured

    iex> PhoenixKit.Mailer.get_mailer()
    PhoenixKit.Mailer  # default

# `resolved_send_path`

```elixir
@spec resolved_send_path() ::
  {:integration, String.t()} | {:mailer, module(), module() | nil}
```

Where an outgoing message will actually be sent, resolved the same way
`deliver_email/2` resolves it: the operator's default send integration wins,
then the delegated host mailer (`config :phoenix_kit, :mailer`), then the
built-in one. The adapter element is `nil` when the resolved mailer has no
adapter configured.

`PhoenixKit.Config.mailer_local?/0` derives from this — anything that needs
to know "does mail land in the local dev mailbox?" must go through one of
the two, never through a raw config read (issue #687: the raw read answered
for a mailer that may not be the one sending, in both directions).

For a delegated mailer the adapter is read from
`PhoenixKit.Config.get_parent_app/0`'s env, which assumes that app matches
the mailer's own `:otp_app` — the same assumption the SES detection in
delivery has always made. Set `:parent_app_name` explicitly if they differ.

# `send_from_template`

Sends an email using a template from the database.

This is the main function for sending emails using PhoenixKit's template system.
It automatically:
- Loads the template by name
- Renders it with provided variables
- Tracks template usage
- Sends the email with tracking
- Logs to EmailSystem

## Parameters

- `template_name` - Name of the template in the database (e.g., "welcome_email")
- `recipient` - Email address (string) or {name, email} tuple
- `variables` - Map of variables to substitute in the template
- `opts` - Additional options:
  - `:user_uuid` - Associate email with a user (for tracking)
  - `:campaign_id` - Campaign identifier (for analytics)
  - `:from` - Override from address (default: configured from_email)
  - `:reply_to` - Reply-to address
  - `:metadata` - Additional metadata map for tracking

## Returns

- `{:ok, email}` - Email sent successfully
- `{:error, :template_not_found}` - Template doesn't exist
- `{:error, :template_inactive}` - Template is not active
- `{:error, reason}` - Other error

## Examples

    # Simple welcome email
    PhoenixKit.Mailer.send_from_template(
      "welcome_email",
      "user@example.com",
      %{"user_name" => "John", "url" => "https://app.com"}
    )

    # With user tracking
    PhoenixKit.Mailer.send_from_template(
      "password_reset",
      {"Jane Doe", "jane@example.com"},
      %{"reset_url" => "https://app.com/reset/token123"},
      user_uuid: user.uuid,
      campaign_id: "password_recovery"
    )

    # With metadata
    PhoenixKit.Mailer.send_from_template(
      "order_confirmation",
      customer.email,
      %{"order_id" => "12345", "total" => "$99.99"},
      user_uuid: customer.uuid,
      campaign_id: "orders",
      metadata: %{order_id: order.id, amount: order.total}
    )

# `send_magic_link_email`

Sends a magic link email to the user.

Uses the 'magic_link' template from the database if available,
falls back to hardcoded template if not found.

## Examples

    iex> PhoenixKit.Mailer.send_magic_link_email(user, "https://app.com/magic/token123")
    {:ok, %Swoosh.Email{}}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
