# Mailglass
> *Mail you can see through.*
[](https://github.com/szTheory/mailglass/actions/workflows/ci.yml)
[](https://hex.pm/packages/mailglass)
[](https://hexdocs.pm/mailglass)
[](https://github.com/szTheory/mailglass/blob/main/LICENSE)
Mailglass is a batteries-included transactional email framework for
Phoenix. It composes on top of [Swoosh](https://hex.pm/packages/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.
## Requirements
- **Elixir** `~> 1.18` and **OTP** `27+`
- **Phoenix** `~> 1.8`
- **Phoenix LiveView** `~> 1.1`
- **Ecto / Ecto SQL** `~> 3.13`
- **PostgreSQL** 14+ (trigger support required; `citext` used for
case-insensitive address match)
- **Swoosh** `~> 1.25` (compose any Swoosh adapter for transport)
## Installation
Add `mailglass` to your dependencies:
```elixir
# mix.exs
def deps do
[
{:mailglass, "~> 0.3"},
{:mailglass_admin, "~> 0.3", only: [:dev]}
]
end
```
Fetch deps, run the installer, and migrate:
```bash
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:
```bash
mix deps.get
mix mailglass.install
mix ecto.migrate
mix compile
```
Define a mailable:
```elixir
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
end
```
Send it — synchronously, asynchronously (via Oban when available), or
in a batch:
```elixir
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:
```bash
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.
- `--domain` is required, and each run checks exactly one domain.
- `--dkim-selector` is repeatable so you can name the selectors your
mail stream actually uses.
- `--verbose` includes supporting evidence inline.
- `--format json` emits the shared machine-readable result shape with
`schema_version: 1`.
## API Stability
The canonical `v1.x` contract inventory for the core package lives in
[`docs/api_stability.md`](docs/api_stability.md).
The canonical `1.x` compatibility, deprecation, and support-matrix policy
lives in
[`guides/compatibility-and-deprecations.md`](guides/compatibility-and-deprecations.md).
Use that document, not root-module reachability, as the source of truth for:
- which `Mailglass` modules, 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.
## 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_events` table protected by
a Postgres trigger that raises `SQLSTATE 45A01` on UPDATE/DELETE.
- **Native mailable setters** — `Mailglass.Message.to/2`, `from/2`,
`subject/2`, `html_body/2`, `text_body/2`, `header/3`, `attach/2`,
and `put_tag/2` keep the common path free of direct `Swoosh.Email.*`
calls while `update_swoosh/2` remains the escape hatch.
- **Stream-aware deliverability** — `:transactional`, `:operational`,
and `:bulk` are enforced message streams. RFC 8058 one-click
unsubscribe headers are injected automatically for `:bulk` and can be
opted into on `:operational`.
- **Idempotency** — partial `UNIQUE` index on
`idempotency_key WHERE idempotency_key IS NOT NULL`; replay-safe
webhooks and delivery retries.
- **Multi-tenant from day one** — `tenant_id` on every record,
`Mailglass.Tenancy` behaviour, `SingleTenant` default resolver,
runtime per-tenant adapter resolution through tenancy callbacks plus
named `adapter_ref` routes, 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`, …) with `reject_reason` enum.
Postmark, SendGrid, Mailgun, SES, and Resend are all shipped
first-party providers, and matched `:bounced`, `:complained`, and
`:unsubscribed` events project suppressions automatically.
- **Test assertions** — `assert_mail_sent/1`, `last_mail/0`,
`wait_for_mail/1`, plus `MailerCase`, `WebhookCase`, `AdminCase`
templates.
- **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`](https://hex.pm/packages/oban),
[`opentelemetry`](https://hex.pm/packages/opentelemetry),
[`mjml`](https://hex.pm/packages/mjml),
[`gen_smtp`](https://hex.pm/packages/gen_smtp),
[`sigra`](https://hex.pm/packages/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` | v0.5+ | 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.Message`
setter 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.doctor` deliverability 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`](.planning/ROADMAP.md) and
[`.planning/PROJECT.md`](.planning/PROJECT.md).
## Documentation
- [`guides/getting-started.md`](guides/getting-started.md) — install,
route mounting, and first delivery
- [`guides/compatibility-and-deprecations.md`](guides/compatibility-and-deprecations.md)
— canonical `1.x` compatibility, deprecation, and support-matrix policy
- [`guides/upgrading-to-v1_0.md`](guides/upgrading-to-v1_0.md) — canonical
latest-`0.x` to `1.0` upgrade path
- [`guides/upgrading-from-v0_1.md`](guides/upgrading-from-v0_1.md) —
codemod-backed upgrade path for existing adopters
- [`guides/migration-from-swoosh.md`](guides/migration-from-swoosh.md)
— move from raw Swoosh to the mailglass pipeline
- [`guides/authoring-mailables.md`](guides/authoring-mailables.md) —
native setter API and `update_swoosh/2` escape hatch
- [`guides/unsubscribe.md`](guides/unsubscribe.md) — RFC 8058 route,
token, and rollout contract
- [`guides/dkim-setup.md`](guides/dkim-setup.md) — DKIM `h=` checks for
one-click unsubscribe
- [`guides/webhooks.md`](guides/webhooks.md) — webhook ingest,
verification, suppression, and retention
- [`guides/rate-limiting.md`](guides/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`](CLAUDE.md) and
[`.planning/PROJECT.md`](.planning/PROJECT.md); a dedicated `CONTRIBUTING.md` lands in
Phase 7.
Reproduce the default CI gate locally:
```bash
mix verify.foundation
mix verify.cold_start
mix compile --no-optional-deps --warnings-as-errors
```
## License
MIT. The license is declared in [`mix.exs`](mix.exs) and applies across
all sibling packages.