# `MishkaCmsCore.Mail`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/mail.ex#L5)

Sending email, for every site through its own provider.

A consumer — a sign-in sender, a notification channel, an extension — builds a `Swoosh.Email`
(`email/2` fills the site's sender) and calls `deliver/2` with the site it is for. It never names a provider: each scope picks one on the settings page's Email tab
(`mailer.provider`), and providers are `mishka_installer` plugins (`MishkaCmsCore.Mail.Provider`),
the core's own `Resend` included.

## No borrowing

Each scope sends with what it set itself: a site with no provider, or one missing its key, sends
nothing — not even through the installation's provider. `nil` as the site is the installation's
own scope, which sends a master's email.

## The path of one email

  1. `"mail:deliver:before"` — a plugin may change the email (`{:reply, %{data | email: email}}`)
     or stop it (`{:reply, :halt, Map.put(data, :suppress, true)}`). A test email passes here too.
  2. A `MishkaCmsCore.Mail.Delivery` row is written: `:queued`, or `:not_configured` with the
     reason, and `"mail:not_configured"` fires.
  3. A `MishkaCmsCore.Mail.Workers.DeliverWorker` job on the `mail` queue sends it — inline under
     `env_test?`. The job carries the email sealed (it may hold a sign-in link) and no key; inside
     the caller's transaction, a rolled-back registration takes its job with it.
  4. The provider answers: the row goes `:sent` and `"mail:delivered"` fires, or the attempt is
     retried while the provider says it may succeed, then the row goes `:failed` and
     `"mail:failed"` fires. Resend's idempotency key keeps a retry from sending twice.

The three notices are fired with `return: false`. Every job shows on the Activity page under its
site, and every row on the Email tab.

## The sender

`email/2` fills each scope's own `mailer.from`, `mailer.from_name` and `mailer.reply_to`
(`resolve: :own`). A site that set no address sends from `noreply@` its own domain, never from
another site's; one that set no name sends under its title.

    Mail.email(site_id, to: "ada@example.com", subject: "Reset your password", html: "<p>…</p>", text: "…")
    |> Mail.deliver(site_id: site_id, purpose: "password_reset")

## History

A site's emails older than `mailer.history_days` are deleted when it next sends one — work done
when something changed, never on a timer.

# `aggregate`

> This function is deprecated. Use `Ash.aggregate/3` instead.

# `aggregate!`

> This function is deprecated. Use `Ash.aggregate!/3` instead.

# `avg`

> This function is deprecated. Use `Ash.avg/3` instead.

# `avg!`

> This function is deprecated. Use `Ash.avg!/3` instead.

# `bulk_create`

> This function is deprecated. Use `Ash.bulk_create/4` instead.

# `bulk_create!`

> This function is deprecated. Use `Ash.bulk_create!/4` instead.

# `bulk_destroy`

> This function is deprecated. Use `Ash.bulk_destroy/4` instead.

# `bulk_destroy!`

> This function is deprecated. Use `Ash.bulk_destroy!/4` instead.

# `bulk_update`

> This function is deprecated. Use `Ash.bulk_update/4` instead.

# `bulk_update!`

> This function is deprecated. Use `Ash.bulk_update!/4` instead.

# `calculate`

> This function is deprecated. Use `Ash.calculate/3` instead.

# `calculate!`

> This function is deprecated. Use `Ash.calculate!/3` instead.

# `can`

> This function is deprecated. Use `Ash.can/3` instead.

```elixir
@spec can(
  action_or_query_or_changeset ::
    Ash.Query.t()
    | Ash.Changeset.t()
    | {Ash.Resource.t(), atom() | Ash.Resource.Actions.action()},
  actor :: term(),
  opts :: Keyword.t()
) ::
  {:ok, boolean() | :maybe}
  | {:ok, true, Ash.Changeset.t() | Ash.Query.t()}
  | {:ok, true, Ash.Changeset.t(), Ash.Query.t()}
  | {:ok, false, Exception.t()}
  | {:error, term()}
```

# `can?`

> This function is deprecated. Use `Ash.can?/3` instead.

```elixir
@spec can?(
  query_or_changeset_or_action ::
    Ash.Query.t()
    | Ash.Changeset.t()
    | {Ash.Resource.t(), atom() | Ash.Resource.Actions.action()},
  actor :: term(),
  opts :: Keyword.t()
) :: boolean() | no_return()
```

# `configured?`

```elixir
@spec configured?(String.t() | nil) :: boolean()
```

Whether `site_id` can send email now.

# `count`

> This function is deprecated. Use `Ash.count/2` instead.

# `count!`

> This function is deprecated. Use `Ash.count!/2` instead.

# `create`

> This function is deprecated. Use `Ash.create/2` instead.

# `create!`

> This function is deprecated. Use `Ash.create!/2` instead.

# `default_provider`

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

The provider a scope uses until it picks one: `"none"` — no email — unless the deployment says
otherwise (`config :mishka_cms_core, :mail_default_provider`, the development mailbox in dev).

# `default_short_name`

# `deliver`

```elixir
@spec deliver(Swoosh.Email.t(), keyword()) ::
  {:ok, MishkaCmsCore.Mail.Delivery.t() | :suppressed} | {:error, term()}
```

Hands `email` over for `opts[:site_id]` (`nil` for the installation). `:purpose` (required) says
what the email is for — `"password_reset"`, `"magic_link"`, `"confirmation"`, or anything an
extension sends; `:idempotency_key` names this one email so a retry is never sent twice.

Returns `{:ok, delivery}` once it is queued (or sent, under `env_test?`), `{:ok, :suppressed}`
when a plugin stopped it, `{:error, :not_configured}` when the scope has no ready provider, or
`{:error, reason}`. It does not raise: an email is never worth failing the action that sent it.

# `describe`

```elixir
@spec describe(term()) :: String.t()
```

Why a send failed, in words an admin can act on: the provider's own message where it gave one,
cut short. Never a key: providers' error bodies do not carry one, and nothing else is shown.

# `destroy`

> This function is deprecated. Use `Ash.destroy/2` instead.

# `destroy!`

> This function is deprecated. Use `Ash.destroy!/2` instead.

# `email`

```elixir
@spec email(String.t() | nil, keyword()) :: Swoosh.Email.t()
```

A new email from `site_id` (`nil` for the installation), with `fields` `:to`, `:subject`, `:html`
and `:text`, and the scope's own sender. A missing `:text` is left out, and the provider writes
one from the HTML.

# `exists`

> This function is deprecated. Use `Ash.exists/2` instead.

# `exists?`

> This function is deprecated. Use `Ash.exists?/2` instead.

# `first`

> This function is deprecated. Use `Ash.first/3` instead.

# `first!`

> This function is deprecated. Use `Ash.first!/3` instead.

# `from_address`

```elixir
@spec from_address(String.t() | nil) :: String.t()
```

The address `site_id` sends from: its own, or `noreply@` its domain — the master site's for Global.

# `from_name`

```elixir
@spec from_name(String.t() | nil) :: String.t()
```

The name `site_id` sends under.

# `get`

> This function is deprecated. Use `Ash.get/3` instead.

# `get!`

> This function is deprecated. Use `Ash.get!/3` instead.

# `list`

> This function is deprecated. Use `Ash.list/3` instead.

# `list!`

> This function is deprecated. Use `Ash.list!/3` instead.

# `load`

> This function is deprecated. Use `Ash.load/3` instead.

# `load!`

> This function is deprecated. Use `Ash.load!/3` instead.

# `max`

> This function is deprecated. Use `Ash.max/3` instead.

# `max!`

> This function is deprecated. Use `Ash.max!/3` instead.

# `min`

> This function is deprecated. Use `Ash.min/3` instead.

# `min!`

> This function is deprecated. Use `Ash.min!/3` instead.

# `page`

> This function is deprecated. Use `Ash.page/2` instead.

# `page!`

> This function is deprecated. Use `Ash.page!/2` instead.

# `provider`

```elixir
@spec provider(String.t() | nil) :: {:ok, module()} | {:error, atom()}
```

The provider `site_id` sends with, ready to send: `{:ok, module}`, or `{:error, reason}` —
`:not_configured` (none chosen), `:unknown_provider` (chosen, but its plugin is gone),
`:cannot_run` (chosen, while the providers' plugins cannot run) or
`:not_ready` (chosen, but missing what it needs, such as its key).

# `prune`

```elixir
@spec prune(String.t() | nil) :: :ok
```

Deletes `site_id`'s emails older than `mailer.history_days` — every site's for `nil`, the
installation's own sweep. Run when a site sends, by the job that sent it.

# `purpose_label`

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

A purpose in words, for the history table.

# `read`

> This function is deprecated. Use `Ash.read/2` instead.

# `read!`

> This function is deprecated. Use `Ash.read!/2` instead.

# `read_one`

> This function is deprecated. Use `Ash.read_one/2` instead.

# `read_one!`

> This function is deprecated. Use `Ash.read_one!/2` instead.

# `reload`

> This function is deprecated. Use `Ash.reload/2` instead.

# `reload!`

> This function is deprecated. Use `Ash.reload!/2` instead.

# `reply_to`

```elixir
@spec reply_to(String.t() | nil) :: String.t() | nil
```

Where replies to `site_id`'s email go, or `nil` for the from address.

# `run_action`

> This function is deprecated. Use `Ash.run_action/2` instead.

# `run_action!`

> This function is deprecated. Use `Ash.run_action/2` instead.

# `send_test`

```elixir
@spec send_test(String.t() | nil, String.t()) ::
  {:ok, String.t() | nil} | {:error, String.t()}
```

Sends a test email to `to` for `site_id` now, through its provider, and waits for the answer:
`{:ok, message_id}` or `{:error, why}` in words for the settings page. Recorded like any other.

# `settings`

```elixir
@spec settings() :: [MishkaCmsCore.Settings.definition()]
```

The settings the Email tab shows: which provider a scope uses, then every offered provider's own,
each drawn only while its provider is the one chosen (`MishkaCmsCore.Settings.shown_when/3`).
Part of `MishkaCmsCore.Settings`' core definitions.

# `status_label`

```elixir
@spec status_label(atom()) :: String.t()
```

A delivery status in words.

# `stream!`

> This function is deprecated. Use `Ash.stream!/2` instead.

# `sum`

> This function is deprecated. Use `Ash.sum/3` instead.

# `sum!`

> This function is deprecated. Use `Ash.sum!/3` instead.

# `update`

> This function is deprecated. Use `Ash.update/2` instead.

# `update!`

> This function is deprecated. Use `Ash.update!/2` instead.

---

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