mailglass - Email, made visible.

Mailglass

Mail you can see through.

CIHex.pmHexDocsLicense

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 2.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

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 is an optional exploration tool. The installation path below is the supported path for an adopter app; it does not require any repository example.

Installation

Add mailglass to your dependencies:

# mix.exs
def deps do
[
{:mailglass, "~> 2.4"},
{:mailglass_admin, "~> 2.4"}
]
end

Fetch deps, run the installer, and migrate:

mix deps.get
mix mailglass.install
mix ecto.migrate

The installer generates the public migration wrapper and current Mailglass schema. Run the generated migration through your own Repo with mix ecto.migrate; do not copy a table list or migration implementation from this repository.

Quickstart

Run the full onboarding path first:

mix deps.get
mix mailglass.install
mix ecto.migrate
mix compile

The installer generates a config block in config/runtime.exs. Confirm your repo and adapter are wired:

# config/runtime.exs
config :mailglass,
repo: MyApp.Repo,
adapter:
{Mailglass.Adapters.Swoosh,
swoosh_adapter: {Swoosh.Adapters.Postmark, api_key: System.fetch_env!("POSTMARK_API_KEY")}},
telemetry: [default_logger: true]

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
end

Each mailable has exactly one recipient—one envelope recipient total—across to, cc, and bcc; Mailglass rejects recipient fan-out. With the default unstamped single-tenant resolver, that delivery belongs to tenant "default". Send it synchronously or durably through normal Oban processing on the canonical :mailglass_outbound queue:

MyApp.UserMailer.welcome(user) |> Mailglass.deliver()
MyApp.UserMailer.welcome(user) |> Mailglass.deliver_later()

Observe the resulting delivery in the authenticated operator surface or through your adapter/telemetry. deliver_later/2 stores private transport input first, then enqueues one durable job; successful payload content is scrubbed according to the configured retention policy. Continue with the complete Getting Started guide, including signed feedback, one-click suppression, and mix mailglass.preflight before production traffic.

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.

API Stability

The canonical 2.x contract inventory for the core package lives in docs/api_stability.md.

The canonical 2.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:

mailglass_admin has its own narrow contract inventory, and mailglass_inbound has its own stable 2.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 2.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

Packages

PackageStatusWhat it is
mailglass2.x contract inventory documented in docs/api_stability.mdCore library: mailables, rendering, delivery pipeline, event ledger, webhook ingest, streams, unsubscribe, suppressions, tenancy.
mailglass_adminNarrow 2.x admin contract documented separatelyMountable LiveView dashboard with stable router/auth/operator seams and internal UI implementation details.
mailglass_inboundStable 2.0 contract documented separatelyInbound routing (Action Mailbox equivalent): recipient/subject/header matchers, ingress plugs per provider, storage adapters, Oban routing.

Roadmap

Full trajectory in .planning/ROADMAP.md and .planning/PROJECT.md.

Documentation

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.