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; v0.5+). 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.
- 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)
Add mailglass to your dependencies:
# mix.exs
def deps do
[
{:mailglass, "~> 0.3"},
{:mailglass_admin, "~> 0.3", only: [:dev]}
]
endFetch deps, run the installer, and migrate:
mix deps.get
mix mailglass.install
mix ecto.migrateThe 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.
Run the full onboarding path first:
mix deps.get
mix mailglass.install
mix ecto.migrate
mix compileDefine 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.
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 jsonmix 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.
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 is outside the v1.x stability promise for this
milestone.
For release posture, support floors, retained legacy bridges, and upgrade expectations, use the compatibility guide rather than inferring policy from the stability inventory alone.
- 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.
| 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 |
v0.5+ | Inbound routing (Action Mailbox equivalent): recipient/subject/header matchers, ingress plugs per provider, storage adapters, Oban routing. |
- 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.
guides/getting-started.md— install, route mounting, and first deliveryguides/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
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-errorsMIT. The license is declared in mix.exs and applies across
all sibling packages.