# `PhoenixKit.Modules.Emails`
[🔗](https://github.com/BeamLabEU/phoenix_kit_emails/blob/0.4.2/lib/phoenix_kit/modules/emails/emails.ex#L1)

Email system for PhoenixKit - main API module.

This module provides the primary interface for email functionality,
including system configuration, log management, event management, and analytics.

## Core Features

- **Email Logging**: Comprehensive logging of all outgoing emails
- **Event Management**: Manage delivery, bounce, complaint, open, and click events
- **AWS SES Integration**: Deep integration with AWS SES for event management
- **Analytics**: Detailed metrics and engagement analysis
- **System Settings**: Configurable options for system behavior
- **Rate Limiting**: Protection against abuse and spam
- **Archival**: Automatic cleanup and archival of old data

## System Settings

All settings are stored in the PhoenixKit settings system with module "email_system":

- `email_enabled` - Enable/disable the entire system
- `email_save_body` - Save full email body (vs preview only)
- `email_save_headers` - Save email headers (vs empty map)
- `email_ses_events` - Manage AWS SES delivery events
- `email_retention_days` - Days to keep emails (default: 90)
- `aws_ses_configuration_set` - AWS SES configuration set name
- `email_compress_body` - Compress body after N days
- `email_archive_to_s3` - Enable S3 archival
- `email_s3_bucket` - S3 bucket archives are written to
- `email_s3_integration` - Integrations connection that signs uploads (unset = ExAws chain)
- `email_sampling_rate` - Percentage of emails to fully log
- `email_create_placeholder_logs` - Create placeholder logs for orphaned events (default: false)

## Core Functions

### System Management
- `enabled?/0` - Check if email system is enabled
- `enable_system/0` - Enable email system
- `disable_system/0` - Disable email system
- `get_config/0` - Get current system configuration
- `placeholder_logs_enabled?/0` - Check if placeholder log creation is enabled
- `set_placeholder_logs/1` - Enable/disable placeholder log creation

### Email Log Management
- `list_logs/1` - Get emails with filters
- `get_log!/1` - Get email log by ID
- `create_log/1` - Create new email log
- `update_log_status/2` - Update log status

### Event Management
- `create_event/1` - Create system event
- `list_events_for_log/1` - Get events for specific log
- `process_webhook_event/1` - Process incoming webhook

### Analytics & Metrics
- `get_system_stats/1` - Overall system statistics
- `get_engagement_metrics/1` - Open/click rate analysis
- `get_campaign_stats/1` - Campaign-specific metrics
- `get_provider_performance/1` - Provider comparison

### Maintenance
- `cleanup_old_logs/1` - Remove old logs
- `compress_old_bodies/1` - Compress storage
- `archive_to_s3/1` - Archive to S3

## Usage Examples

    # Check if system is enabled
    if PhoenixKit.Modules.Emails.enabled?() do
      # System is active
    end

    # Get system statistics
    stats = PhoenixKit.Modules.Emails.get_system_stats(:last_30_days)
    # => %{total_sent: 5000, delivered: 4850, bounce_rate: 2.5, open_rate: 23.4}

    # Get campaign performance
    campaign_stats = PhoenixKit.Modules.Emails.get_campaign_stats("newsletter_2024")
    # => %{total_sent: 1000, delivery_rate: 98.5, open_rate: 25.2, click_rate: 4.8}

    # Process webhook from AWS SES
    {:ok, event} = PhoenixKit.Modules.Emails.process_webhook_event(webhook_data)

    # Clean up old data
    {deleted_count, _} = PhoenixKit.Modules.Emails.cleanup_old_logs(90)

## Configuration Example

    # In your application config
    config :phoenix_kit,
      email_enabled: true,
      email_save_body: false,
      email_retention_days: 90,
      aws_ses_configuration_set: "my-app-system"

# `archive_to_s3`

Archives old emails to S3 if archival is enabled.

Delegates to `PhoenixKit.Modules.Emails.Archiver.archive_to_s3/2`, which is
where the upload actually happens. Until this delegation existed the function
selected the rows and returned them untouched, so archival reported success
having written nothing to S3.

`opts` are the archiver's: `:bucket`, `:prefix`, `:batch_size`, `:format`,
`:include_events`, `:delete_after_archive`.

## Examples

    iex> PhoenixKit.Modules.Emails.archive_to_s3()
    {:ok, 100}

    iex> PhoenixKit.Modules.Emails.archive_to_s3(180, delete_after_archive: true)
    {:ok, 42}

# `aws_configured?`

Checks if AWS credentials are configured, from any of the sources
`get_aws_access_key/0`/`get_aws_secret_key/0` read (highest priority
first): the selected `aws_ses` Integrations connection
(`emails_aws_integration_uuid`), then the legacy `aws_access_key_id`/
`aws_secret_access_key` Settings, then environment variables.

# `aws_ses_credentials`

```elixir
@spec aws_ses_credentials(String.t()) :: map()
```

The decrypted credential map for ONE `aws_ses` Integrations connection,
memoized under `{:credentials, uuid}`.

Keyed by uuid rather than a single `:credentials` slot because the SQS
poller now walks several accounts per cycle: a single-slot cache handed
whichever account was resolved first to every subsequent lookup, which
with two accounts means polling one account's queue with the other's
keys. Cleared by `invalidate_aws_credentials_cache/0`, which every write
site that can change a resolution already calls.

## Examples

    iex> PhoenixKit.Modules.Emails.aws_ses_credentials("some-uuid")
    %{"access_key" => "AKIA...", "secret_key" => "...", "aws_region" => "eu-north-1"}

# `brevo_events_enabled?`

Checks if Brevo event polling is enabled.

Unlike SQS (which separates "event tracking" from "polling mechanism"
into two flags), Brevo has only the one mechanism — this single flag
gates both.

## Examples

    iex> PhoenixKit.Modules.Emails.brevo_events_enabled?()
    false

# `cleanup_old_logs`

Removes emails older than the specified number of days.

Uses the system retention setting if no days specified.

## Examples

    iex> PhoenixKit.Modules.Emails.cleanup_old_logs()
    {150, nil}  # Deleted 150 records

    iex> PhoenixKit.Modules.Emails.cleanup_old_logs(180)
    {75, nil}   # Deleted 75 records older than 180 days

# `compress_old_bodies`

Compresses body_full field for old emails to save storage.

## Examples

    iex> PhoenixKit.Modules.Emails.compress_old_bodies()
    {25, nil}  # Compressed 25 records

    iex> PhoenixKit.Modules.Emails.compress_old_bodies(60)
    {40, nil}  # Compressed 40 records older than 60 days

# `count_logs`

Counts emails with optional filtering (without loading all records).

## Parameters

- `filters` - Map of filters to apply (optional)

## Examples

    iex> PhoenixKit.Modules.Emails.count_logs(%{status: "bounced"})
    42

# `create_event`

Creates an email system event.

## Examples

    iex> PhoenixKit.Modules.Emails.create_event(%{
      email_log_uuid: log.uuid,
      event_type: "open"
    })
    {:ok, %Event{}}

# `create_log`

Creates an email log if system is enabled.

## Examples

    iex> PhoenixKit.Modules.Emails.create_log(%{
      message_id: "abc123",
      to: "user@example.com",
      from: "app@example.com"
    })
    {:ok, %Log{}}

# `current_provider`

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

Returns the email provider actually in effect, detected from the host
app's mailer configuration (see `PhoenixKit.Modules.Emails.Utils.mailer_adapter_status/0`).

Falls back to `"aws_ses"` when the adapter can't be statically detected
(e.g. a delegated mailer configured at runtime) but AWS credentials are
present — the same heuristic `Interceptor.detect_provider/2` already uses
for delegation-mode hosts — otherwise `"unknown"`.

## Examples

    iex> PhoenixKit.Modules.Emails.current_provider()
    "aws_ses"

# `delete_aws_tracking`

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

Deletes one account's stored AWS tracking settings, if any.

## Examples

    iex> PhoenixKit.Modules.Emails.delete_aws_tracking("some-uuid")
    {:ok, %Setting{}}

# `delete_brevo_watermark`

Deletes one integration's stored Brevo watermark, if any. A no-op
(still `{:ok, ...}`-shaped via the underlying delete) if none exists.

## Examples

    iex> PhoenixKit.Modules.Emails.delete_brevo_watermark("some-uuid")
    {:ok, %Setting{}}

# `delete_log`

Deletes an email log.

## Examples

    iex> log = PhoenixKit.Modules.Emails.get_log!(1)
    iex> PhoenixKit.Modules.Emails.delete_log(log)
    {:ok, %Log{}}

# `disable_system`

Disables the email system.

Sets the "email_enabled" setting to false.

## Examples

    iex> PhoenixKit.Modules.Emails.disable_system()
    {:ok, %Setting{}}

# `email_log_exists?`

Whether an email log exists for `message_id`, matched against either the
internal `message_id` or the provider `aws_message_id` column.

A cheap existence check (no struct load); `false` when the system is
disabled or the id is not a binary. Used by the Brevo poller to skip
events for mail this app never sent — the account may be shared with
other senders.

# `email_status_topic`

PubSub topic on which email log status changes are broadcast.

Subscribed to by admin LiveViews (e.g. the emails list) and broadcast from
`PhoenixKit.Modules.Emails.Log.update_log/2` whenever a log's status changes.
Single source of truth for both the broadcaster and the subscribers.

# `enable_system`

Enables the email system.

Sets the "email_enabled" setting to true.

## Examples

    iex> PhoenixKit.Modules.Emails.enable_system()
    {:ok, %Setting{}}

# `enabled?`

Checks if the email system is enabled.

Returns true if the "email_enabled" setting is true.

## Examples

    iex> PhoenixKit.Modules.Emails.enabled?()
    true

# `fetch_dlq_events_for_message`

Fetch SES events from DLQ queue for specific message ID.

## Parameters

- `message_id` - The AWS SES message ID to search for

## Returns

List of SES events matching the message ID from DLQ.
Account-aware: see `search_targets/1`. A message whose log carries an
`integration_uuid` is looked for in THAT account's queue; otherwise every
configured account is searched.

# `fetch_sqs_events_for_message`

Fetch SES events from main SQS queue for specific message ID.

## Parameters

- `message_id` - The AWS SES message ID to search for

## Returns

List of SES events matching the message ID.
Account-aware: see `search_targets/1`. A message whose log carries an
`integration_uuid` is looked for in THAT account's queue; otherwise every
configured account is searched.

# `get_aws_access_key`

Gets AWS access key.

Priority: selected Integrations connection → Settings Database →
Environment Variables (see `aws_ses_credentials/0`).

## Examples

    iex> PhoenixKit.Modules.Emails.get_aws_access_key()
    "AKIA..."

# `get_aws_region`

Gets the AWS region for SES and SQS services.

Priority: selected Integrations connection → Settings Database →
Environment Variables (see `aws_ses_credentials/0`).

## Examples

    iex> PhoenixKit.Modules.Emails.get_aws_region()
    "eu-north-1"

# `get_aws_secret_key`

Gets AWS secret key.

Priority: selected Integrations connection → Settings Database →
Environment Variables (see `aws_ses_credentials/0`).

## Examples

    iex> PhoenixKit.Modules.Emails.get_aws_secret_key()
    "secret..."

# `get_aws_tracking`

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

The per-account SES/SQS tracking pipeline for one `aws_ses` integration —
the multi-account replacement for the single global `aws_sqs_queue_url` /
`aws_ses_configuration_set` / … settings, stored as one JSON setting per
account under the `aws_tracking:<integration_uuid>` key (the same
prefix-keyed shape `get_brevo_watermark/1` uses for its cursor).

Returns a map with every key in ["queue_url", "dlq_url", "queue_arn", "sns_topic_arn", "configuration_set", "region"] (missing
or blank entries come back as `nil`), or `nil` when this account has no
tracking settings at all — which is what the legacy single-queue fallback
in `SQSPollingJob` keys off, so an existing env-configured deployment
keeps polling exactly as before.

## Examples

    iex> PhoenixKit.Modules.Emails.get_aws_tracking("some-uuid")
    %{queue_url: "https://sqs.eu-north-1.amazonaws.com/1/q", region: "eu-north-1", ...}

# `get_brevo_last_polled_at`

Gets the timestamp of the last completed Brevo polling cycle, if any.
Observability only — dedup/idempotency is enforced at the DB level by
`Event`'s unique indexes, this is not used as a fetch cursor.

## Examples

    iex> PhoenixKit.Modules.Emails.get_brevo_last_polled_at()
    "2026-07-19T12:00:00Z"

# `get_brevo_polling_excluded_integrations`

Integration uuids the operator has explicitly opted OUT of Brevo event
polling — an empty list (the default) means every active Brevo
integration gets polled. `BrevoPollingJob` reads this fresh every
cycle (no cache invalidation needed).

## Examples

    iex> PhoenixKit.Modules.Emails.get_brevo_polling_excluded_integrations()
    []

# `get_brevo_polling_interval`

Gets the Brevo polling interval in milliseconds.

## Examples

    iex> PhoenixKit.Modules.Emails.get_brevo_polling_interval()
    120_000

# `get_brevo_watermark`

The stored Brevo poll watermark for one integration — how far
`BrevoPollingJob` has progressed through that integration's event
stream, as `%{date: Date.t(), offset: non_neg_integer()}`. `nil` if
this integration has never been polled under the watermark scheme yet
(cold start).

## Examples

    iex> PhoenixKit.Modules.Emails.get_brevo_watermark("some-uuid")
    %{date: ~D[2026-07-19], offset: 2500}

# `get_campaign_stats`

Gets statistics for a specific campaign.

## Examples

    iex> PhoenixKit.Modules.Emails.get_campaign_stats("newsletter_2024")
    %{
      total_sent: 1000,
      delivery_rate: 98.5,
      open_rate: 25.2,
      click_rate: 4.8
    }

# `get_config`

Gets the current email system configuration.

Returns a map with all current settings.

## Examples

    iex> PhoenixKit.Modules.Emails.get_config()
    %{
      enabled: true,
      save_body: false,
      ses_events: true,
      retention_days: 90,
      sampling_rate: 100,
      ses_configuration_set: "my-system",
      sns_topic_arn: "arn:aws:sns:eu-north-1:123456789012:phoenixkit-email-events",
      sqs_queue_url: "https://sqs.eu-north-1.amazonaws.com/123456789012/phoenixkit-email-queue",
      sqs_polling_enabled: false,
      aws_region: "eu-north-1"
    }

# `get_daily_delivery_trends`

Gets daily delivery trend data for chart visualization.

## Examples

    iex> PhoenixKit.Modules.Emails.get_daily_delivery_trends(:last_7_days)
    %{
      labels: ["2024-09-01", "2024-09-02", ...],
      delivered: [120, 190, 300, ...],
      bounced: [5, 10, 15, ...]
    }

# `get_engagement_metrics`

Gets engagement metrics with trend analysis.

## Examples

    iex> PhoenixKit.Modules.Emails.get_engagement_metrics(:last_7_days)
    %{
      avg_open_rate: 24.5,
      avg_click_rate: 4.2,
      bounce_rate: 2.8,
      engagement_trend: :increasing
    }

# `get_geo_stats`

Gets geographic distribution of engagement events.

## Examples

    iex> PhoenixKit.Modules.Emails.get_geo_stats("open", :last_30_days)
    %{"US" => 500, "CA" => 200, "UK" => 150}

# `get_log`

Gets a single email log by ID. Returns `nil` if not found or system is disabled.

## Examples

    iex> PhoenixKit.Modules.Emails.get_log("018f1234-5678-7890-abcd-ef1234567890")
    %Log{}

    iex> PhoenixKit.Modules.Emails.get_log("nonexistent")
    nil

# `get_log!`

Gets a single email log by ID.

Raises `Ecto.NoResultsError` if the log does not exist or system is disabled.

## Examples

    iex> PhoenixKit.Modules.Emails.get_log!("018f1234-5678-7890-abcd-ef1234567890")
    %Log{}

# `get_log_by_message_id`

Gets an email log by message ID.

## Examples

    iex> PhoenixKit.Modules.Emails.get_log_by_message_id("msg-abc123")
    {:ok, %Log{}}

    iex> PhoenixKit.Modules.Emails.get_log_by_message_id("nonexistent")
    {:error, :not_found}

# `get_provider_performance`

Gets provider-specific performance metrics.

## Examples

    iex> PhoenixKit.Modules.Emails.get_provider_performance(:last_7_days)
    %{
      "aws_ses" => %{delivery_rate: 98.5, bounce_rate: 1.5},
      "smtp" => %{delivery_rate: 95.0, bounce_rate: 5.0}
    }

# `get_retention_days`

Gets the configured retention period for emails in days.

## Examples

    iex> PhoenixKit.Modules.Emails.get_retention_days()
    90

# `get_s3_bucket`

Gets the S3 bucket archives are written to, or `nil` when unset.

# `get_s3_integration`

Gets the uuid of the Integrations connection whose credentials sign archive
uploads, or `nil` to leave ExAws to its own resolution chain.

# `get_sampling_rate`

Gets the sampling rate for email logging (percentage).

## Examples

    iex> PhoenixKit.Modules.Emails.get_sampling_rate()
    100  # Log 100% of emails

# `get_ses_configuration_set`

Gets the AWS SES configuration set name.

## Examples

    iex> PhoenixKit.Modules.Emails.get_ses_configuration_set()
    "my-app-system"

# `get_sns_topic_arn`

Gets the AWS SNS Topic ARN for email events.

## Examples

    iex> PhoenixKit.Modules.Emails.get_sns_topic_arn()
    "arn:aws:sns:eu-north-1:123456789012:phoenixkit-email-events"

# `get_sqs_config`

Gets comprehensive SQS configuration.

## Examples

    iex> PhoenixKit.Modules.Emails.get_sqs_config()
    %{
      sns_topic_arn: "arn:aws:sns:...",
      queue_url: "https://sqs.eu-north-1.amazonaws.com/...",
      polling_enabled: true,
      polling_interval_ms: 5000,
      max_messages_per_poll: 10
    }

# `get_sqs_dlq_url`

Gets the AWS SQS Dead Letter Queue URL.

## Examples

    iex> PhoenixKit.Modules.Emails.get_sqs_dlq_url()
    "https://sqs.eu-north-1.amazonaws.com/123456789012/phoenixkit-email-dlq"

# `get_sqs_max_messages`

Gets the maximum number of SQS messages to receive per polling cycle.

## Examples

    iex> PhoenixKit.Modules.Emails.get_sqs_max_messages()
    10

# `get_sqs_polling_excluded_integrations`

```elixir
@spec get_sqs_polling_excluded_integrations() :: [String.t()]
```

Integration uuids the operator has explicitly opted OUT of SQS event
polling — an empty list (the default) means every active `aws_ses`
integration gets polled. `SQSPollingJob` reads this fresh every cycle (no
cache invalidation needed). Same comma-separated storage format as
`get_brevo_polling_excluded_integrations/0`.

## Examples

    iex> PhoenixKit.Modules.Emails.get_sqs_polling_excluded_integrations()
    []

# `get_sqs_polling_interval`

Gets the SQS polling interval in milliseconds.

## Examples

    iex> PhoenixKit.Modules.Emails.get_sqs_polling_interval()
    5000  # 5 seconds

# `get_sqs_queue_arn`

Gets the AWS SQS Queue ARN for email events.

## Examples

    iex> PhoenixKit.Modules.Emails.get_sqs_queue_arn()
    "arn:aws:sqs:eu-north-1:123456789012:phoenixkit-email-queue"

# `get_sqs_queue_url`

Gets the AWS SQS Queue URL for email events.

## Examples

    iex> PhoenixKit.Modules.Emails.get_sqs_queue_url()
    "https://sqs.eu-north-1.amazonaws.com/123456789012/phoenixkit-email-queue"

# `get_sqs_visibility_timeout`

Gets the SQS message visibility timeout in seconds.

## Examples

    iex> PhoenixKit.Modules.Emails.get_sqs_visibility_timeout()
    300  # 5 minutes

# `get_system_stats`

Gets overall system statistics for a time period.

## Examples

    iex> PhoenixKit.Modules.Emails.get_system_stats(:last_30_days)
    %{
      total_sent: 5000,
      delivered: 4850,
      bounced: 150,
      opened: 1200,
      clicked: 240,
      delivery_rate: 97.0,
      bounce_rate: 3.0,
      open_rate: 24.7,
      click_rate: 20.0
    }

# `get_template_stats`

Gets template-specific performance metrics.

## Examples

    iex> PhoenixKit.Modules.Emails.get_template_stats(:last_30_days)
    %{
      "welcome_email" => %{sent: 100, delivered: 95, opened: 45, clicked: 12},
      "password_reset" => %{sent: 50, delivered: 48, opened: 30, clicked: 8}
    }

# `get_top_links`

Gets the most clicked links for a time period.

## Examples

    iex> PhoenixKit.Modules.Emails.get_top_links(:last_30_days, 10)
    [%{url: "https://example.com/product", clicks: 150}, ...]

# `invalidate_aws_credentials_cache`

```elixir
@spec invalidate_aws_credentials_cache() :: :ok
```

Invalidates the cached AWS SES credential resolution (`aws_ses_credentials/0`).

Call after anything that changes which credentials `get_aws_access_key/0`,
`get_aws_secret_key/0`, or `get_aws_region/0` should resolve to — selecting
a different `aws_ses` Integrations connection, clearing the selection back
to legacy Settings, or updating the selected connection's own credentials.
Without this, a switch would still take effect, just not sooner than the
cache's 60000ms TTL.

# `list_aws_tracking_integration_uuids`

```elixir
@spec list_aws_tracking_integration_uuids() :: [String.t()]
```

Every integration uuid that currently has stored AWS tracking settings —
used by `SQSPollingJob` to prune settings for integrations that no longer
exist, so those rows don't accumulate forever.

## Examples

    iex> PhoenixKit.Modules.Emails.list_aws_tracking_integration_uuids()
    ["some-uuid"]

# `list_brevo_watermark_integration_uuids`

Every integration uuid that currently has a stored Brevo watermark —
used by `BrevoPollingJob` to prune watermarks for integrations that
are no longer active (deleted, or opted out of polling), so those
settings rows don't accumulate forever.

## Examples

    iex> PhoenixKit.Modules.Emails.list_brevo_watermark_integration_uuids()
    ["some-uuid"]

# `list_events_for_log`

Lists events for a specific email log.

## Examples

    iex> PhoenixKit.Modules.Emails.list_events_for_log("550e8400-e29b-41d4-a716-446655440000")
    [%Event{}, ...]

# `list_logs`

Lists emails with optional filters.

## Options

- `:status` - Filter by status (sent, delivered, bounced, etc.)
- `:campaign_id` - Filter by campaign
- `:template_name` - Filter by template
- `:provider` - Filter by email provider
- `:from_date` - Emails sent after this date
- `:to_date` - Emails sent before this date
- `:recipient` - Filter by recipient email
- `:limit` - Limit results (default: 50)
- `:offset` - Offset for pagination

## Examples

    iex> PhoenixKit.Modules.Emails.list_logs(%{status: "bounced", limit: 10})
    [%Log{}, ...]

# `placeholder_logs_enabled?`

Checks if placeholder log creation is enabled.

When enabled, the system creates placeholder logs for events received from AWS SES
that don't have an existing email log. This can help recover from synchronization issues
but may mask underlying problems.

Default: false (recommended for production to expose synchronization issues)

## Examples

    iex> PhoenixKit.Modules.Emails.placeholder_logs_enabled?()
    false

# `process_webhook_event`

Processes an incoming webhook event (typically from AWS SES).

## Examples

    iex> webhook_data = %{
      "eventType" => "bounce",
      "mail" => %{"messageId" => "abc123"}
    }
    iex> PhoenixKit.Modules.Emails.process_webhook_event(webhook_data)
    {:ok, %Event{}}

# `s3_archival_credentials`

```elixir
@spec s3_archival_credentials(String.t()) :: map()
```

The decrypted credential map for ONE S3-archival Integrations connection,
memoized under `{:s3_archival_credentials, uuid}` (same cache table and
invalidation as `aws_ses_credentials/1`, distinct key so the two never
collide).

Deliberately a separate function rather than widening `aws_ses_credentials/1`
itself: that one backs the SEND path (`selected_aws_integration_uuid/0`,
the mailer's access/secret/region getters, per-account SQS polling) and
must stay `aws_ses`-only — an `object_storage` connection has no business
signing outgoing mail. This one backs `Archiver.s3_request_config/0`
instead, which needs credentials for uploading to S3, not for sending
through it, so it accepts either provider shape: `aws_ses` (the connection
an install may already have pointed archival at, before `object_storage`
existed) or `object_storage` (the type built for this — see
`PhoenixKit.Integrations.Providers.object_storage/0` in core).

# `save_body_enabled?`

Checks if full email body saving is enabled.

Returns true if the "email_save_body" setting is true.

## Examples

    iex> PhoenixKit.Modules.Emails.save_body_enabled?()
    false

# `save_headers_enabled?`

Checks if email headers saving is enabled.

Returns true if the "email_save_headers" setting is true.

## Examples

    iex> PhoenixKit.Modules.Emails.save_headers_enabled?()
    false

# `selected_aws_integration_uuid`

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

The single `aws_ses` Integrations connection selected via the
`emails_aws_integration_uuid` setting, or `nil`.

This is the LEGACY, one-account selection: it names the connection the
global `aws_*` settings (queue URL, configuration set, …) have always
described, and it is still what the send-path `get_aws_*` getters resolve
through. Multi-account callers should go through
`PhoenixKit.Modules.Emails.AwsIntegrations.active_integration_uuids/0`
instead; this getter exists so they can recognise which one account the
legacy globals may still be attributed to.

## Examples

    iex> PhoenixKit.Modules.Emails.selected_aws_integration_uuid()
    "019f562e-0000-7000-8000-000000000000"

# `ses_events_enabled?`

Checks if AWS SES event management is enabled.

## Examples

    iex> PhoenixKit.Modules.Emails.ses_events_enabled?()
    true

# `set_aws_region`

Sets the AWS region for SES and SQS services.

## Examples

    iex> PhoenixKit.Modules.Emails.set_aws_region("eu-north-1")
    {:ok, %Setting{}}

# `set_aws_tracking`

```elixir
@spec set_aws_tracking(String.t(), map()) :: {:ok, term()} | {:error, term()}
```

Persists one account's SES/SQS tracking pipeline. Accepts string- or
atom-keyed attrs; unknown keys are dropped and blank values are stored as
`nil`, so the stored JSON is always exactly the
["queue_url", "dlq_url", "queue_arn", "sns_topic_arn", "configuration_set", "region"] shape `get_aws_tracking/1` reads back.

Callers that change this must also reconcile the SES tracker (a queue URL
appearing/disappearing changes `SQSPollingManager.eligible?/0`) — see
`PhoenixKit.Modules.Emails.EventTrackerReconciler.reconcile_tracker/1`.

## Examples

    iex> PhoenixKit.Modules.Emails.set_aws_tracking("some-uuid", %{"queue_url" => "https://..."})
    {:ok, %Setting{}}

# `set_brevo_events_enabled`

Enables or disables Brevo event polling.

## Examples

    iex> PhoenixKit.Modules.Emails.set_brevo_events_enabled(true)
    {:ok, %Setting{}}

# `set_brevo_last_polled_at`

Records the timestamp of the last completed Brevo polling cycle.

## Examples

    iex> PhoenixKit.Modules.Emails.set_brevo_last_polled_at(DateTime.utc_now())
    {:ok, %Setting{}}

# `set_brevo_polling_excluded_integrations`

Sets the list of Brevo integration uuids excluded from polling.

## Examples

    iex> PhoenixKit.Modules.Emails.set_brevo_polling_excluded_integrations(["uuid-1"])
    {:ok, %Setting{}}

# `set_brevo_polling_interval`

Sets the Brevo polling interval in milliseconds (minimum 30 000 — Brevo's
rate limits are looser than SQS's long-poll cadence, so this floor is
higher than SQS's 1 000ms).

## Examples

    iex> PhoenixKit.Modules.Emails.set_brevo_polling_interval(60_000)
    {:ok, %Setting{}}

# `set_brevo_watermark`

Persists one integration's Brevo poll watermark.

## Examples

    iex> PhoenixKit.Modules.Emails.set_brevo_watermark("some-uuid", ~D[2026-07-19], 2500)
    {:ok, %Setting{}}

# `set_compress_after_days`

Sets the number of days after which to compress email bodies.

## Examples

    iex> PhoenixKit.Modules.Emails.set_compress_after_days(30)
    {:ok, %Setting{}}

# `set_placeholder_logs`

Enables or disables placeholder log creation.

## Parameters

- `enabled` - true to enable placeholder logs, false to disable

## Examples

    iex> PhoenixKit.Modules.Emails.set_placeholder_logs(false)
    {:ok, %Setting{}}

# `set_retention_days`

Sets the retention period for emails.

## Examples

    iex> PhoenixKit.Modules.Emails.set_retention_days(180)
    {:ok, %Setting{}}

# `set_s3_archival`

Enables or disables S3 archival for old email data.

## Examples

    iex> PhoenixKit.Modules.Emails.set_s3_archival(true)
    {:ok, %Setting{}}

# `set_s3_bucket`

Sets the S3 bucket archives are written to. A blank string clears it.

The bucket had a reader and no writer, so archival could never be pointed
anywhere: enabling it always failed with `:no_bucket_configured`.

Clearing DELETES the row rather than storing `""` — the settings changeset
rejects an empty value outright ("must provide either value or value_json"),
so a blank write would otherwise fail instead of unsetting.

# `set_s3_integration`

Sets the Integrations connection used for archive uploads. `""` clears it,
which falls back to the environment, an instance profile, or a task role.

# `set_sampling_rate`

Sets the sampling rate for email logging.

## Examples

    iex> PhoenixKit.Modules.Emails.set_sampling_rate(80)  # Log 80% of emails
    {:ok, %Setting{}}

# `set_save_body`

Enables or disables full email body saving.

## Examples

    iex> PhoenixKit.Modules.Emails.set_save_body(true)
    {:ok, %Setting{}}

# `set_save_headers`

Enables or disables email headers saving.

## Examples

    iex> PhoenixKit.Modules.Emails.set_save_headers(true)
    {:ok, %Setting{}}

# `set_ses_configuration_set`

Sets the AWS SES configuration set name.

## Examples

    iex> PhoenixKit.Modules.Emails.set_ses_configuration_set("my-system-set")
    {:ok, %Setting{}}

# `set_ses_events`

Enables or disables AWS SES event management.

## Examples

    iex> PhoenixKit.Modules.Emails.set_ses_events(true)
    {:ok, %Setting{}}

# `set_sns_topic_arn`

Sets the AWS SNS Topic ARN for email events.

## Examples

    iex> PhoenixKit.Modules.Emails.set_sns_topic_arn("arn:aws:sns:eu-north-1:123456789012:phoenixkit-email-events")
    {:ok, %Setting{}}

# `set_sqs_dlq_url`

Sets the AWS SQS Dead Letter Queue URL.

## Examples

    iex> PhoenixKit.Modules.Emails.set_sqs_dlq_url("https://sqs.eu-north-1.amazonaws.com/123456789012/phoenixkit-email-dlq")
    {:ok, %Setting{}}

# `set_sqs_max_messages`

Sets the maximum number of SQS messages to receive per polling cycle.

## Examples

    iex> PhoenixKit.Modules.Emails.set_sqs_max_messages(20)
    {:ok, %Setting{}}

# `set_sqs_polling`

Enables or disables SQS polling.

## Examples

    iex> PhoenixKit.Modules.Emails.set_sqs_polling(true)
    {:ok, %Setting{}}

# `set_sqs_polling_excluded_integrations`

```elixir
@spec set_sqs_polling_excluded_integrations([String.t()]) ::
  {:ok, term()} | {:error, term()}
```

Sets the list of `aws_ses` integration uuids excluded from SQS polling.

## Examples

    iex> PhoenixKit.Modules.Emails.set_sqs_polling_excluded_integrations(["uuid-1"])
    {:ok, %Setting{}}

# `set_sqs_polling_interval`

Sets the SQS polling interval in milliseconds.

## Examples

    iex> PhoenixKit.Modules.Emails.set_sqs_polling_interval(3000)  # 3 seconds
    {:ok, %Setting{}}

# `set_sqs_queue_arn`

Sets the AWS SQS Queue ARN for email events.

## Examples

    iex> PhoenixKit.Modules.Emails.set_sqs_queue_arn("arn:aws:sqs:eu-north-1:123456789012:phoenixkit-email-queue")
    {:ok, %Setting{}}

# `set_sqs_queue_url`

Sets the AWS SQS Queue URL for email events.

## Examples

    iex> PhoenixKit.Modules.Emails.set_sqs_queue_url("https://sqs.eu-north-1.amazonaws.com/123456789012/phoenixkit-email-queue")
    {:ok, %Setting{}}

# `set_sqs_visibility_timeout`

Sets the SQS message visibility timeout in seconds.

## Examples

    iex> PhoenixKit.Modules.Emails.set_sqs_visibility_timeout(600)  # 10 minutes
    {:ok, %Setting{}}

# `sqs_polling_enabled?`

Checks if SQS polling is enabled.

## Examples

    iex> PhoenixKit.Modules.Emails.sqs_polling_enabled?()
    true

# `sync_email_status`

Manually sync email status by fetching events from SQS queues.

This function searches for events in both the main SQS queue and DLQ
that match the given message_id and processes them to update email status.

## Parameters

- `message_id` - The AWS SES message ID or internal PhoenixKit message ID to sync

## Returns

- `{:ok, result}` - Successful sync with processing results
- `{:error, reason}` - Error during sync process

## Examples

    iex> PhoenixKit.Modules.Emails.sync_email_status("0110019971abc123-...")
    {:ok, %{events_processed: 3, log_updated: true}}

    iex> PhoenixKit.Modules.Emails.sync_email_status("pk_abc123...")
    {:ok, %{events_processed: 1, log_updated: true}}

# `update_log_status`

Updates the status of an email log.

## Examples

    iex> PhoenixKit.Modules.Emails.update_log_status(log, "delivered")
    {:ok, %Log{}}

---

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