# `PostHog.Emails`
[🔗](https://github.com/posthog/posthog-elixir/blob/v2.20.0/lib/posthog/emails.ex#L1)

Send transactional email, such as a password reset or a receipt, and read its status.

An email goes out in one of two forms:

- **A template made in PostHog**, sent by its ID with `:template_id` and `:template_data`.
- **Content your code rendered**, named by `:template_key`, with `:subject` and `:html`,
  `:text` or both. To send a React email, render it to HTML in your app, for example with
  `@react-email/render`, and pass the result as `:html`. The SDK adds no rendering.

## Setup

1. In PostHog, open the email template under Workflows, make it transactional, and copy its
   ID. `:template_id` is that UUID. The code form needs no template.
2. Create a project secret API key (`phs_...`) with the "Send transactional email" preset
   (`transactional_email:write`) in your project settings. Add `transactional_email:read` too
   if you read email statuses with a key that can't send. Keys made with `hog_flow:write`
   still work. The project API key (`phc_...`) captures events but cannot send email.
3. Set it as `:secret_key` in your PostHog config:

       config :posthog,
         api_host: "https://eu.i.posthog.com",
         api_key: "phc_...",
         secret_key: "phs_..."

## Examples

Send a template made in PostHog:

    {:ok, %PostHog.Emails.SendResult{id: message_id}} =
      PostHog.Emails.send(
        to: "user@example.com",
        template_id: "0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b",
        template_data: %{name: "Ada"},
        distinct_id: "user_123",
        idempotency_key: "welcome:user_123"
      )

Send content your code rendered:

    PostHog.Emails.send(
      to: "user@example.com",
      template_key: "password-reset",
      subject: "Reset your password",
      html: rendered_html,
      distinct_id: "user_123",
      idempotency_key: "password-reset:#{reset.id}"
    )

Read what happened to it:

    {:ok, %PostHog.Emails.Status{status: status}} = PostHog.Emails.get(message_id)

Always pass `:distinct_id`. Without it, the email's events go to a person whose distinct ID
is the email address, not to your user. The SDK never takes it from `PostHog.set_context/2`.

## What a result means

`{:ok, result}` means PostHog queued the email. Delivery happens later: read it with `get/2`,
or in the template's workflow in PostHog.

## Where requests go

A client on PostHog Cloud sends to its region's app host, `https://us.posthog.com` or
`https://eu.posthog.com`. For any other `:api_host`, such as a reverse proxy, set `:app_host`
in the config, because email requests must not send the secret key to the proxy. Without it,
a send returns the error `missing_app_host`.

`:secret_key` also turns on local feature flag evaluation, which fetches flag definitions
from `:api_host` with that key. When `:api_host` is a proxy that must not see the key, set
`enable_local_evaluation: false` as well.

## Timeouts, retries and idempotency

Each request times out after `:email_request_timeout_ms` (15 seconds by default). A send
retries network errors, rate limits, an idempotency key still in flight and an unavailable
service `:email_request_max_retries` times (2 by default), waiting about 1 and then 2 seconds,
or the server's `Retry-After` when it is longer. A `Retry-After` over 30 seconds fails at once
with `:retry_after_ms` set. A monthly quota is never retried.

Every attempt sends the same idempotency key, so a retry never sends twice. Without
`:idempotency_key`, the SDK makes a UUID for the call. When a job runner such as Oban
already retries the send, pass `max_retries: 0` and an `:idempotency_key` that names the
email, such as `"welcome:user_123"`. A generated key doesn't survive a job retry.

Sends and status reads block the calling process until they finish. The SDK keeps no email
queue.

## Errors

Every failure returns `{:error, %PostHog.Emails.Error{}}`. Branch on `:code`. The ones a
setup usually hits:

- `missing_secret_key`: the config has no `:secret_key`.
- `missing_app_host`: the `:api_host` is not PostHog Cloud. Set `:app_host`.
- `authentication_failed` (401) or `permission_denied` (403): the key is wrong, or it lacks
  `transactional_email:write`.
- `not_found` (404): the template ID is wrong, or transactional email is not enabled for the
  project.
- `client_disabled`: the PostHog instance is not running or has no `:api_key`, so it sends
  nothing.
- `client_closed`: the PostHog instance stopped while a call waited to retry, so the retry
  was not sent.

An option the SDK refuses returns an error with an `invalid_*` code, such as
`invalid_template_id`, and the name `:validation_error`, before any request.

## Testing

Requests go through the configured `:api_client_module`, so a `Mox` mock of
`PostHog.API.Client` sees every email request without network calls.

# `send_option`

```elixir
@type send_option() ::
  {:to, String.t()}
  | {:distinct_id, String.t() | nil}
  | {:idempotency_key, String.t() | nil}
  | {:max_retries, non_neg_integer() | nil}
  | {:template_id, String.t()}
  | {:template_data, map() | nil}
  | {:template_key, String.t()}
  | {:subject, String.t()}
  | {:html, String.t() | nil}
  | {:text, String.t() | nil}
  | {:from, String.t() | nil}
  | {:reply_to, String.t() | [String.t()] | nil}
  | {:capture_engagement, boolean() | nil}
```

Options for `send/2`. Pass `:template_id` or `:template_key`, never both.

Every send takes:

- `:to` - one recipient email address. Required.
- `:distinct_id` - the recipient's distinct ID in your product. The email's events go to
  this person.
- `:idempotency_key` - 1 to 255 visible ASCII characters. A send with a key PostHog already
  accepted queues nothing and returns the first send's result. Template sends and code sends
  share one set of keys: a key already used for another email returns
  `idempotency_key_reused`.
- `:max_retries` - overrides the config's `:email_request_max_retries` for this send. `0`
  turns retries off.

A template made in PostHog takes:

- `:template_id` - the UUID of a template made transactional in PostHog.
- `:template_data` - a map of values for the template's `{{ variables.* }}` placeholders.

Content your code rendered takes:

- `:template_key` - names the email, such as `"password-reset"`, so its metrics, logs and
  status group under it in PostHog. Letters, digits, `-` and `_`, at most 400.
- `:subject` - the subject line. Required.
- `:html` and `:text` - the bodies. Give one or both. Without `:text`, PostHog derives it from
  `:html`.
- `:from` - a verified sender, such as `"Acme <security@example.com>"`. Without it, the
  project's default sender.
- `:reply_to` - one address or a list of addresses.
- `:capture_engagement` - whether PostHog captures the email's opens and clicks as events of
  the recipient. Defaults to `true`. A project whose admin turned engagement capture off
  captures nothing, whatever the send asks.

An option set to `nil` counts as left out.

# `get`

```elixir
@spec get(PostHog.supervisor_name(), String.t()) ::
  {:ok, PostHog.Emails.Status.t()} | {:error, PostHog.Emails.Error.t()}
```

Reads the status of an email that `send/2` queued, for 30 days after the send.

Returns `{:ok, %PostHog.Emails.Status{}}` or `{:error, %PostHog.Emails.Error{}}`. The error
code is `not_found` when no email of this project has the ID, when the email is older than 30
days, or for a few seconds after the send. A key with only `transactional_email:read` can read
statuses but not send. Network errors, rate limits and an unavailable service are retried.

## Examples

    {:ok, %PostHog.Emails.Status{status: "bounced", reason: reason}} =
      PostHog.Emails.get(message_id)

# `send`

```elixir
@spec send(PostHog.supervisor_name(), [send_option()] | map()) ::
  {:ok, PostHog.Emails.SendResult.t()} | {:error, PostHog.Emails.Error.t()}
```

Queues one email, from a template made in PostHog or from content your code rendered.

Returns `{:ok, %PostHog.Emails.SendResult{}}` once PostHog queued the email, or
`{:error, %PostHog.Emails.Error{}}`. See `t:send_option/0` for the options. `name` is the
PostHog instance to send through, `PostHog` by default.

## Examples

    PostHog.Emails.send(
      to: "user@example.com",
      template_id: "0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b",
      distinct_id: "user_123"
    )

    PostHog.Emails.send(MyPostHog,
      to: "user@example.com",
      template_key: "receipt",
      subject: "Your receipt",
      html: html,
      distinct_id: "user_123"
    )

---

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