Mail you can see through.
Mailglass is a batteries-included transactional email framework for Phoenix. It composes on top of Swoosh and ships the framework layer Swoosh deliberately leaves out: HEEx-native components with Outlook MSO/VML fallbacks, a LiveView preview/admin dashboard, normalized webhook events, an append-only event ledger with Postgres trigger immutability, multi-tenant routing, message streams, RFC 8058 List-Unsubscribe with signed tokens, suppression lists, and webhook-driven auto-suppression.
It is shipped as three sibling packages: mailglass (core),
mailglass_admin (mountable LiveView dashboard), and
mailglass_inbound (inbound routing; stable 1.0). It is for senior
Phoenix teams building production transactional email — welcome flows,
password resets, magic links, receipts, notifications — who today
rebuild the same 40% of framework plumbing on every project.
Requirements
- Elixir
~> 1.18and OTP27+ - Phoenix
~> 1.8 - Phoenix LiveView
~> 1.1 - Ecto / Ecto SQL
~> 3.13 - PostgreSQL 14+ (trigger support required;
citextused for case-insensitive address match) - Swoosh
~> 1.25(compose any Swoosh adapter for transport)
Demo App
For a realistic local click-around, run the B2B SaaS Ops demo (needs Docker):
make demo
It builds, starts, waits until healthy, then prints the URLs for the
dashboard, preview, and outbound/inbound operator journeys (default
http://localhost:4015) over seeded data. Stop with make demo-down. Ports are
configurable so the demo runs alongside other library demos without collisions —
see guides/run-the-demo.md for the full walkthrough.
The demo lives in reference/demo_app; the narrower reference/host_app remains
the maintained trust-proof baseline.
Installation
Add mailglass to your dependencies:
# mix.exs
def deps do
[
{:mailglass, "~> 1.5"},
{:mailglass_admin, "~> 1.5", only: [:dev]}
]
endFetch deps, run the installer, and migrate:
mix deps.get
mix mailglass.install
mix ecto.migrate
The installer generates: a MyApp.Mailing context, the three-table
migration (mailglass_deliveries, mailglass_events,
mailglass_suppressions plus the immutability trigger), router mounts
for the dev preview and webhook plug, a default mailable and layout,
an Oban worker stub (when Oban is installed), and a config/runtime.exs
configuration block.
Quickstart
Run the full onboarding path first:
mix deps.get
mix mailglass.install
mix ecto.migrate
mix compile
Define a mailable:
defmodule MyApp.UserMailer do
use Mailglass.Mailable, stream: :transactional
def welcome(user) do
new()
|> to(user.email)
|> from({"MyApp", "support@example.com"})
|> subject("Welcome to MyApp")
|> html_body("<h1>Welcome to MyApp</h1>")
|> text_body("Welcome to MyApp")
|> Mailglass.Message.put_function(:welcome)
end
endSend it — synchronously, asynchronously (via Oban when available), or in a batch:
MyApp.UserMailer.welcome(user) |> Mailglass.deliver()
MyApp.UserMailer.welcome(user) |> Mailglass.deliver_later()
Mailglass.deliver_many(Enum.map(users, &MyApp.UserMailer.welcome/1))Preview mailables in dev at http://localhost:4000/dev/mail — sidebar
of discovered mailables, device width and dark-mode toggles,
HTML/Text/Raw/Headers tabs, live-editable assigns.
Deliverability Doctor
Run the DNS-only doctor against one explicit domain at a time:
mix mail.doctor --domain example.com
mix mail.doctor --domain example.com --dkim-selector default --dkim-selector selector2
mix mail.doctor --domain example.com --verbose
mix mail.doctor --domain example.com --format json
mix mail.doctor reports DNS truth and remediation guidance for SPF,
DKIM, DMARC, MX, and BIMI. It can return honest cannot_verify
outcomes when DNS alone is insufficient, and it does not promise inbox
placement certainty or a deliverability grade.
--domainis required, and each run checks exactly one domain.--dkim-selectoris repeatable so you can name the selectors your mail stream actually uses.--verboseincludes supporting evidence inline.--format jsonemits the shared machine-readable result shape withschema_version: 1.
API Stability
The canonical v1.x contract inventory for the core package lives in
docs/api_stability.md.
The canonical 1.x compatibility, deprecation, and support-matrix policy
lives in
guides/compatibility-and-deprecations.md.
Use that document, not root-module reachability, as the source of truth for:
- which
Mailglassmodules, behaviours, Mix tasks, telemetry families, structs, and documented fields are stable - which exported surfaces are intentionally
internal - which hooks exist only for first-party sibling-package integration
mailglass_admin has its own narrow contract inventory, and
mailglass_inbound has its own stable 1.0 contract inventory in
mailglass_inbound/docs/api_stability.md;
it remains an independent package release line rather than part of the linked
core/admin v1.x group.
For release posture, support floors, retained legacy bridges, and upgrade expectations, use the compatibility guide rather than inferring policy from the stability inventory alone.
Feature highlights
- HEEx-native components (
container,section,row,column,heading,text,button,img,link,hr,preheader) with MSO VML fallbacks for Outlook. No Node toolchain. - Pure render pipeline — HEEx → Premailex CSS inlining →
data-mg-*strip → auto-plaintext via Floki walker. ~4ms on a ten-component template. - Append-only event ledger —
mailglass_eventstable protected by a Postgres trigger that raisesSQLSTATE 45A01on UPDATE/DELETE. - Native mailable setters —
Mailglass.Message.to/2,from/2,subject/2,html_body/2,text_body/2,header/3,attach/2, andput_tag/2keep the common path free of directSwoosh.Email.*calls whileupdate_swoosh/2remains the escape hatch. - Stream-aware deliverability —
:transactional,:operational, and:bulkare enforced message streams. RFC 8058 one-click unsubscribe headers are injected automatically for:bulkand can be opted into on:operational. - Idempotency — partial
UNIQUEindex onidempotency_key WHERE idempotency_key IS NOT NULL; replay-safe webhooks and delivery retries. - Multi-tenant from day one —
tenant_idon every record,Mailglass.Tenancybehaviour,SingleTenantdefault resolver, runtime per-tenant adapter resolution through tenancy callbacks plus namedadapter_refroutes, and an Oban tenancy middleware (conditionally compiled). - Fake adapter as the release gate — deterministic, in-memory, time-advanceable; merge-blocking in CI so the full pipeline is testable without real provider credentials.
- Swoosh as transport — compose on any Swoosh adapter (Postmark, SendGrid, Mailgun, SES, Resend, local SMTP, etc.).
- Normalized webhook events — Anymail event taxonomy verbatim
(
queued,sent,bounced,delivered,opened,clicked,complained,unsubscribed, …) withreject_reasonenum. Postmark, SendGrid, Mailgun, SES, and Resend are all shipped first-party providers, and matched:bounced,:complained, and:unsubscribedevents project suppressions automatically. - Test assertions —
assert_mail_sent/1,last_mail/0,wait_for_mail/1, plusMailerCase,WebhookCase,AdminCasetemplates. - Telemetry spans on every entry point with a PII whitelist (counts, IDs, and latencies — never addresses or bodies).
- Optional deps gated via
Mailglass.OptionalDeps.*:oban,opentelemetry,mjml,gen_smtp,sigra.
Packages
| Package | Status | What it is |
|---|---|---|
mailglass | v1.x contract inventory documented in docs/api_stability.md | Core library: mailables, rendering, delivery pipeline, event ledger, webhook ingest, streams, unsubscribe, suppressions, tenancy. |
mailglass_admin | Narrow v1.x admin contract documented separately | Mountable LiveView dashboard with stable router/auth/operator seams and internal UI implementation details. |
mailglass_inbound | Stable 1.0 contract documented separately | Inbound routing (Action Mailbox equivalent): recipient/subject/header matchers, ingress plugs per provider, storage adapters, Oban routing. |
Roadmap
- v0.2 — Production-credible core — native
Mailglass.Messagesetter API,mix mailglass.upgrade.v0_2, message-stream policy, RFC 8058 unsubscribe, webhook-driven suppression projection, linked release hardening, and release-blocking Tier 1 docs. - v0.5 — Deliverability + admin — prod-mountable admin,
mix mail.doctordeliverability checks, per-tenant adapter resolver, per-domain rate limiting. - v1.0 — API stability lock, production references, long-lived deprecation policy.
Full trajectory in .planning/ROADMAP.md and
.planning/PROJECT.md.
Documentation
guides/getting-started.md— install, route mounting, and first deliveryguides/run-the-demo.md— see mailglass working locally in one command (make demo)guides/compatibility-and-deprecations.md— canonical1.xcompatibility, deprecation, and support-matrix policyguides/upgrading-to-v1_0.md— canonical latest-0.xto1.0upgrade pathguides/upgrading-from-v0_1.md— codemod-backed upgrade path for existing adoptersguides/migration-from-swoosh.md— move from raw Swoosh to the mailglass pipelineguides/authoring-mailables.md— native setter API andupdate_swoosh/2escape hatchguides/unsubscribe.md— RFC 8058 route, token, and rollout contractguides/dkim-setup.md— DKIMh=checks for one-click unsubscribeguides/webhooks.md— webhook ingest, verification, suppression, and retentionguides/rate-limiting.md— multi-bucket throughput protection
Contributing
Mailglass is developed in public. Contributor conventions, decision
log, and phase-by-phase roadmap live in CLAUDE.md and
.planning/PROJECT.md; a dedicated CONTRIBUTING.md lands in
Phase 7.
Reproduce the default CI gate locally:
mix verify.foundation
mix verify.cold_start
mix compile --no-optional-deps --warnings-as-errors
License
MIT. The license is declared in mix.exs and applies across
all sibling packages.