Skip to content

About

Emails Module for PhoenixKit. Email tracking, analytics, and AWS SES integration.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

PhoenixKitEmails

Elixir License: MIT

Email tracking, analytics, and AWS SES integration for PhoenixKit. Drop-in email logging, delivery event tracking, engagement metrics, templating, rate limiting, and an admin dashboard.

Features

  • Email logging — comprehensive logging of all outgoing emails with optional body/header storage
  • AWS SES integration — SQS-based event polling for delivery, bounce, complaint, open, and click events
  • Analytics & metrics — system stats, engagement rates, campaign performance, and provider comparison
  • Email templates — file-based now (phoenix_kit_templates); this package keeps only the one-time export of legacy database rows
  • Rate limiting — configurable protection against abuse and spam via Hammer
  • Blocklist — manage blocked email addresses
  • Queue management — view and manage outgoing email queue
  • Archival — automatic cleanup, body compression, and S3 archival of old email data
  • Webhook processing — process incoming delivery/bounce/complaint webhooks
  • CSV export — export email logs via controller endpoint
  • Email open/click tracking — pixel tracking and link rewriting for engagement metrics
  • Admin dashboard — LiveView pages for logs, metrics, queue, blocklist, and settings
  • Auto-discovery — implements PhoenixKit.Module behaviour; PhoenixKit finds it at startup with zero config

Upgrading after address verification was removed

Address verification is no longer part of this package. The Verification API and workers, SMTP settings section, runner download route and machine-API tools (emails.claim_smtp_jobs and emails.report_smtp_results) are removed. Newsletters uses a host-configured verification engine instead.

If you used this feature, prepare the host while the old version is still running, before updating this dependency:

  1. Stop starting verification batches and stop external SMTP runners. In Settings → Email Sending → Mailbox verification (SMTP), switch the SMTP mode off; this settles queued SMTP checks. Remove any verification worker from Oban.Plugins.Cron, especially PhoenixKit.Modules.Emails.Verification.QueueSweepWorker. A cron entry for a removed worker prevents Oban from starting after the upgrade.
  2. Keep the email_verification queue configured while in-flight batches finish. Wait for executing jobs to finish and for remaining batch work to settle. Scheduled sweep/runner jobs can remain after that. Pause the verification queue on every node, wait for executing jobs to finish, then cancel the remaining available, scheduled and retryable jobs for these workers through the host's Oban instance: PhoenixKit.Modules.Emails.Verification.BatchStartWorker, PhoenixKit.Modules.Emails.Verification.VerifyEmailWorker, PhoenixKit.Modules.Emails.Verification.QueueSweepWorker and PhoenixKit.Modules.Emails.Verification.LocalRunnerWorker.
  3. Remove email_verification from the host's Oban queues, then update the dependency. Leaving jobs behind makes them fail as unknown workers if the queue still runs, or leaves them unprocessed if the queue is removed.
  4. Revoke runner API keys with scope emails.smtp_runner:run. If the machine API was enabled only for the runner, disable it under Settings → API. Remove host calls to PhoenixKit.Modules.Emails.Verification and its submodules; they have no replacement API in this package.

Hosts that never used verification can update directly; remove any configured verification cron entries or queue as well.

Existing verification tables and their rows are preserved, including batch, domain-cache, SMTP-queue and runner records. Migrations V4 and V5 retain their version numbers but create nothing; fresh installs get no verification tables, and repair no longer manages them. Old email_verify_* settings and :phoenix_kit_emails :verification_* configuration keys are no longer read.

⚠️ Email templates are files now (core 3.0)

Message templates no longer live in the database. They are files a host owns in its own repository — version-controlled and reviewable — that core reads through phoenix_kit_templates, over the defaults core and each module ship. The template editor, the database layer and the phoenix_kit_email_templates table are retired: a fresh install never creates the table.

Before upgrading from core 2.x, export your edits. Core 3.0 no longer reads the table, so an edit that is only in the database stops taking effect:

mix phoenix_kit_emails.templates.export   # --dry-run to preview first

Review the diff and commit the files. Nothing is deleted and no row is touched (this package never drops the table), so an export that turns out wrong costs nothing but the files. On a database without the table the task stops and says so.

The task exports only what you actually edited. A template still identical to what this package ships is skipped, because core supplies those itself now, translated into every shipped locale. On an install that never opened the template editor it writes nothing at all, and you can stop reading here.

Newsletter layouts (is_system: false, picked per broadcast) are not affected — those are authored at runtime by people who cannot open a pull request, so they keep a table and an editor.

What the files look like

The template name becomes a directory; each part is a file, and any locale that is not the fallback carries its code:

priv/phoenix_kit_templates/
└── register/
    ├── subject.txt        # the fallback locale — what everyone falls through to
    ├── subject.uk.txt
    ├── text.txt
    └── html.html

The html part: a body, not a whole document

A stored html_body is a whole HTML document with its own container, header, footer and <style>. Core wraps an email built from a file in a shared layout (PhoenixKit.Email.Layout) but never wraps a document, so a document exported verbatim keeps its old chrome and the layout never applies. Where core has the layout, the export writes the body fragment only: what sits between the template's header and footer, with the styles the body needs (the button, the notice boxes) inlined. The email's <h1> title, a footer that carries a placeholder (the "copy this link" fallback, billing's company details) and anything outside the header and footer blocks are content, not decoration, and stay in the body. Only the decorative wrapping goes; any text it held is listed in the output so it can be moved into your own _layout.

mix phoenix_kit_emails.templates.export --html document   # the stored document, as before

--html is auto by default: body when the loaded core has the layout, document when it does not (a fragment on such a core would be sent bare). One known gap: billing's {{{line_items_html}}} inserts rows styled by classes from the removed <style>, so they render unstyled until billing styles its own rows; the task warns about it.

Variables in a template

A database template follows the same rules as a phoenix_kit_templates file:

  • {{name}} is HTML-escaped in the HTML body; {{{name}}} is inserted raw, for a value that is already rendered HTML. Subject and text body are plain text and never escaped.
  • {{ name }} is {{name}}. A name is an identifier — {{user.name}} or an unclosed {{name is literal text — and an unbound placeholder is left as written.
  • A value is a string, number, atom or a String.Chars struct (Date, Decimal, ...). A map or list used by a placeholder raises ArgumentError.

When upgrading:

  • mix phoenix_kit.update runs this package's V3 migration, which rewrites billing's {{line_items_html}} and {{line_items_table_html}} to the raw form in stored HTML bodies. Those two names are inserted raw in either form anyway.
  • A template you wrote yourself that passes ready-made HTML in another variable needs {{{name}}} now — otherwise the markup arrives escaped.
  • A list or charlist value that a placeholder uses is an ArgumentError now; before, it was silently turned into text.
  • A name with a dot or a dash ({{user.name}}, {{a-b}}) is literal text, not a variable.
  • Code up to 0.5.4 renders {{{x}}} as { + value + }. If you roll back, mix phoenix_kit.update to a version below V3 restores the double braces for the two billing names, but a {{{name}}} you wrote yourself stays as written.

Installation

Add phoenix_kit_emails to your dependencies in mix.exs:

def deps do
  [
    {:phoenix_kit_emails, "~> 0.2"}
  ]
end

Then fetch dependencies:

mix deps.get

Note: For development or if not yet published to Hex, you can use:

{:phoenix_kit_emails, github: "BeamLabEU/phoenix_kit_emails"}

PhoenixKit auto-discovers the module at startup — no additional configuration needed.

Quick Start

  1. Add the dependency to mix.exs
  2. Run mix deps.get
  3. Enable the module in admin settings (email_enabled: true)
  4. Configure your AWS SES configuration set name in settings
  5. Email logs and analytics are available at /admin/emails

Usage

System management

alias PhoenixKit.Modules.Emails

# Check if system is enabled
Emails.enabled?()

# Enable/disable the system
Emails.enable_system()
Emails.disable_system()

# Get current configuration
config = Emails.get_config()

Email logging

# Create a log entry
{:ok, log} = Emails.create_log(%{
  to: "user@example.com",
  from: "noreply@example.com",
  subject: "Welcome!",
  status: "sent"
})

# List logs with filters
logs = Emails.list_logs(%{status: "sent", page: 1})

# Get a specific log
log = Emails.get_log!(log_uuid)

Analytics

# Get system statistics
stats = 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 = Emails.get_campaign_stats("newsletter_2024")
# => %{total_sent: 1000, delivery_rate: 98.5, open_rate: 25.2, click_rate: 4.8}

# Get engagement metrics
metrics = Emails.get_engagement_metrics(:last_7_days)

# Get provider performance comparison
performance = Emails.get_provider_performance(:last_30_days)

Webhook processing

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

# List events for a specific log
events = Emails.list_events_for_log(log_uuid)

Maintenance

# Clean up old logs (by retention days)
{deleted_count, _} = Emails.cleanup_old_logs(90)

# Compress old email bodies to save storage
Emails.compress_old_bodies(30)

# Archive old data to S3
Emails.archive_to_s3(180)

Settings

Settings are managed through the PhoenixKit Settings API and can be configured via the admin UI at /admin/settings/email-sending.

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

Permissions

The module declares permissions via permission_metadata/0:

  • "emails" — access to email admin dashboard and all sub-pages

Use Scope.has_module_access?/2 to check permissions in your application.

Architecture

lib/
  mix/tasks/
    phoenix_kit_emails.install.ex        # Install mix task
  phoenix_kit/modules/emails/
    emails.ex                            # Context + PhoenixKit.Module behaviour
    application_integration.ex           # Application-level integration
    archiver.ex                          # S3 archival logic
    archive_worker.ex                    # Hourly Oban cron that calls the archiver
    email_log_data.ex                    # Email log data struct
    event.ex                             # Delivery/bounce/complaint event schema
    interceptor.ex                       # Email interceptor for logging
    log.ex                               # Email log schema
    metrics.ex                           # Analytics and metrics queries
    paths.ex                             # Centralized URL path helpers
    provider.ex                          # Provider behaviour
    rate_limiter.ex                      # Rate limiting via Hammer
    sqs_polling_job.ex                   # Oban job for SQS polling
    sqs_polling_manager.ex               # SQS polling lifecycle management
    sqs_processor.ex                     # SQS message processing
    supervisor.ex                        # OTP Supervisor
    table_columns.ex                     # Admin table column definitions
    template.ex                          # Legacy template row (read-only, for the export)
    templates.ex                         # send_email/4 facade + legacy defaults/rows for the export
    utils.ex                             # Shared utilities
    web/
      blocklist.ex                       # Blocklist admin LiveView
      details.ex                         # Email log detail LiveView
      emails.ex                          # Email logs admin LiveView
      email_tracking.ex                  # Open/click tracking
      export_controller.ex               # CSV export controller
      metrics.ex                         # Metrics dashboard LiveView
      queue.ex                           # Queue management LiveView
      routes.ex                          # Route definitions
      settings.ex                        # Settings admin LiveView
      webhook_controller.ex              # Webhook endpoint controller

Database Tables

Table Description
phoenix_kit_email_logs Email log records (UUIDv7 PK)
phoenix_kit_email_events Delivery/bounce/complaint/open/click events
phoenix_kit_email_templates Retired: no longer created; hosts upgraded from core 2.x keep it (read only by the export)

Development

mix deps.get       # Install dependencies
mix test           # Run tests
mix format         # Format code
mix credo --strict # Static analysis (strict mode)
mix dialyzer       # Type checking
mix docs           # Generate documentation
mix precommit      # Compile + format + credo + dialyzer
mix quality        # Format + credo + dialyzer

Troubleshooting

Emails not appearing in admin

  • Verify email_enabled is true in settings
  • Ensure the module is listed as a dependency in the parent app's mix.exs

SES events not processing

  • Verify email_ses_events is enabled in settings
  • Check that aws_ses_configuration_set is configured
  • Ensure SQS queue permissions allow the application to poll
  • Review Oban dashboard for failed SQS polling jobs

Metrics showing zero data

  • Confirm emails are being logged (check /admin/emails)
  • Verify the date range filter matches when emails were sent

License

MIT — see LICENSE for details.

Links

About

Emails Module for PhoenixKit. Email tracking, analytics, and AWS SES integration.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages