{"id":49692882,"url":"https://github.com/sztheory/mailglass","last_synced_at":"2026-06-03T00:00:40.581Z","repository":{"id":353524422,"uuid":"1219599205","full_name":"szTheory/mailglass","owner":"szTheory","description":"Phoenix transactional email with preview/admin UI","archived":false,"fork":false,"pushed_at":"2026-06-02T17:48:50.000Z","size":8243,"stargazers_count":1,"open_issues_count":8,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-02T19:10:04.134Z","etag":null,"topics":["ecto","elixir","email","heex","hex","mailer","multi-tenant","phoenix","phoenix-liveview","swoosh","transactional-email","webhooks"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/mailglass/","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/szTheory.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-24T03:17:35.000Z","updated_at":"2026-06-02T17:47:41.000Z","dependencies_parsed_at":null,"dependency_job_id":"ea029e05-9aad-4ede-a8e8-bc923624d3de","html_url":"https://github.com/szTheory/mailglass","commit_stats":null,"previous_names":["sztheory/mailglass"],"tags_count":30,"template":false,"template_full_name":null,"purl":"pkg:github/szTheory/mailglass","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/szTheory%2Fmailglass","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/szTheory%2Fmailglass/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/szTheory%2Fmailglass/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/szTheory%2Fmailglass/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/szTheory","download_url":"https://codeload.github.com/szTheory/mailglass/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/szTheory%2Fmailglass/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33841996,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ecto","elixir","email","heex","hex","mailer","multi-tenant","phoenix","phoenix-liveview","swoosh","transactional-email","webhooks"],"created_at":"2026-05-07T19:17:05.274Z","updated_at":"2026-06-03T00:00:40.542Z","avatar_url":"https://github.com/szTheory.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mailglass\n\n\u003e *Mail you can see through.*\n\n[![CI](https://github.com/szTheory/mailglass/actions/workflows/ci.yml/badge.svg)](https://github.com/szTheory/mailglass/actions/workflows/ci.yml)\n[![Hex.pm](https://img.shields.io/hexpm/v/mailglass.svg)](https://hex.pm/packages/mailglass)\n[![HexDocs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/mailglass)\n[![License](https://img.shields.io/hexpm/l/mailglass.svg)](https://github.com/szTheory/mailglass/blob/main/LICENSE)\n\nMailglass is a batteries-included transactional email framework for\nPhoenix. It composes on top of [Swoosh](https://hex.pm/packages/swoosh)\nand ships the framework layer Swoosh deliberately leaves out: HEEx-native\ncomponents with Outlook MSO/VML fallbacks, a LiveView preview/admin\ndashboard, normalized webhook events, an append-only event ledger with\nPostgres trigger immutability, multi-tenant routing, message streams,\nRFC 8058 List-Unsubscribe with signed tokens, suppression lists, and\nwebhook-driven auto-suppression.\n\nIt is shipped as three sibling packages: **`mailglass`** (core),\n**`mailglass_admin`** (mountable LiveView dashboard), and\n**`mailglass_inbound`** (inbound routing; stable 1.0). It is for senior\nPhoenix teams building production transactional email — welcome flows,\npassword resets, magic links, receipts, notifications — who today\nrebuild the same 40% of framework plumbing on every project.\n\n## Requirements\n\n- **Elixir** `~\u003e 1.18` and **OTP** `27+`\n- **Phoenix** `~\u003e 1.8`\n- **Phoenix LiveView** `~\u003e 1.1`\n- **Ecto / Ecto SQL** `~\u003e 3.13`\n- **PostgreSQL** 14+ (trigger support required; `citext` used for\n  case-insensitive address match)\n- **Swoosh** `~\u003e 1.25` (compose any Swoosh adapter for transport)\n\n## Demo App\n\nFor a realistic local click-around, run the B2B SaaS Ops demo:\n\n```bash\ndocker compose -f compose.demo.yml up demo\n```\n\nOpen http://localhost:4015 to inspect seeded preview, outbound operator, and\ninbound operator journeys. The demo lives in `reference/demo_app`; the narrower\n`reference/host_app` remains the maintained trust-proof baseline.\n\n## Installation\n\nAdd `mailglass` to your dependencies:\n\n```elixir\n# mix.exs\ndef deps do\n  [\n    {:mailglass, \"~\u003e 1.4\"},\n    {:mailglass_admin, \"~\u003e 1.4\", only: [:dev]}\n  ]\nend\n```\n\nFetch deps, run the installer, and migrate:\n\n```bash\nmix deps.get\nmix mailglass.install\nmix ecto.migrate\n```\n\nThe installer generates: a `MyApp.Mailing` context, the three-table\nmigration (`mailglass_deliveries`, `mailglass_events`,\n`mailglass_suppressions` plus the immutability trigger), router mounts\nfor the dev preview and webhook plug, a default mailable and layout,\nan Oban worker stub (when Oban is installed), and a `config/runtime.exs`\nconfiguration block.\n\n## Quickstart\n\nRun the full onboarding path first:\n\n```bash\nmix deps.get\nmix mailglass.install\nmix ecto.migrate\nmix compile\n```\n\nDefine a mailable:\n\n```elixir\ndefmodule MyApp.UserMailer do\n  use Mailglass.Mailable, stream: :transactional\n\n  def welcome(user) do\n    new()\n    |\u003e to(user.email)\n    |\u003e from({\"MyApp\", \"support@example.com\"})\n    |\u003e subject(\"Welcome to MyApp\")\n    |\u003e html_body(\"\u003ch1\u003eWelcome to MyApp\u003c/h1\u003e\")\n    |\u003e text_body(\"Welcome to MyApp\")\n    |\u003e Mailglass.Message.put_function(:welcome)\n  end\nend\n```\n\nSend it — synchronously, asynchronously (via Oban when available), or\nin a batch:\n\n```elixir\nMyApp.UserMailer.welcome(user) |\u003e Mailglass.deliver()\nMyApp.UserMailer.welcome(user) |\u003e Mailglass.deliver_later()\nMailglass.deliver_many(Enum.map(users, \u0026MyApp.UserMailer.welcome/1))\n```\n\nPreview mailables in dev at `http://localhost:4000/dev/mail` — sidebar\nof discovered mailables, device width and dark-mode toggles,\nHTML/Text/Raw/Headers tabs, live-editable assigns.\n\n## Deliverability Doctor\n\nRun the DNS-only doctor against one explicit domain at a time:\n\n```bash\nmix mail.doctor --domain example.com\nmix mail.doctor --domain example.com --dkim-selector default --dkim-selector selector2\nmix mail.doctor --domain example.com --verbose\nmix mail.doctor --domain example.com --format json\n```\n\n`mix mail.doctor` reports DNS truth and remediation guidance for SPF,\nDKIM, DMARC, MX, and BIMI. It can return honest `cannot_verify`\noutcomes when DNS alone is insufficient, and it does not promise inbox\nplacement certainty or a deliverability grade.\n\n- `--domain` is required, and each run checks exactly one domain.\n- `--dkim-selector` is repeatable so you can name the selectors your\n  mail stream actually uses.\n- `--verbose` includes supporting evidence inline.\n- `--format json` emits the shared machine-readable result shape with\n  `schema_version: 1`.\n\n## API Stability\n\nThe canonical `v1.x` contract inventory for the core package lives in\n[`docs/api_stability.md`](docs/api_stability.md).\n\nThe canonical `1.x` compatibility, deprecation, and support-matrix policy\nlives in\n[`guides/compatibility-and-deprecations.md`](guides/compatibility-and-deprecations.md).\n\nUse that document, not root-module reachability, as the source of truth for:\n\n- which `Mailglass` modules, behaviours, Mix tasks, telemetry families,\n  structs, and documented fields are stable\n- which exported surfaces are intentionally `internal`\n- which hooks exist only for first-party sibling-package integration\n\n`mailglass_admin` has its own narrow contract inventory, and\n`mailglass_inbound` has its own stable `1.0` contract inventory in\n[`mailglass_inbound/docs/api_stability.md`](mailglass_inbound/docs/api_stability.md);\nit remains an independent package release line rather than part of the linked\ncore/admin `v1.x` group.\n\nFor release posture, support floors, retained legacy bridges, and upgrade\nexpectations, use the compatibility guide rather than inferring policy from the\nstability inventory alone.\n\n## Feature highlights\n\n- **HEEx-native components** (`container`, `section`, `row`, `column`,\n  `heading`, `text`, `button`, `img`, `link`, `hr`, `preheader`) with\n  MSO VML fallbacks for Outlook. No Node toolchain.\n- **Pure render pipeline** — HEEx → Premailex CSS inlining →\n  `data-mg-*` strip → auto-plaintext via Floki walker. ~4ms on a\n  ten-component template.\n- **Append-only event ledger** — `mailglass_events` table protected by\n  a Postgres trigger that raises `SQLSTATE 45A01` on UPDATE/DELETE.\n- **Native mailable setters** — `Mailglass.Message.to/2`, `from/2`,\n  `subject/2`, `html_body/2`, `text_body/2`, `header/3`, `attach/2`,\n  and `put_tag/2` keep the common path free of direct `Swoosh.Email.*`\n  calls while `update_swoosh/2` remains the escape hatch.\n- **Stream-aware deliverability** — `:transactional`, `:operational`,\n  and `:bulk` are enforced message streams. RFC 8058 one-click\n  unsubscribe headers are injected automatically for `:bulk` and can be\n  opted into on `:operational`.\n- **Idempotency** — partial `UNIQUE` index on\n  `idempotency_key WHERE idempotency_key IS NOT NULL`; replay-safe\n  webhooks and delivery retries.\n- **Multi-tenant from day one** — `tenant_id` on every record,\n  `Mailglass.Tenancy` behaviour, `SingleTenant` default resolver,\n  runtime per-tenant adapter resolution through tenancy callbacks plus\n  named `adapter_ref` routes, and an Oban tenancy middleware\n  (conditionally compiled).\n- **Fake adapter as the release gate** — deterministic, in-memory,\n  time-advanceable; merge-blocking in CI so the full pipeline is\n  testable without real provider credentials.\n- **Swoosh as transport** — compose on any Swoosh adapter (Postmark,\n  SendGrid, Mailgun, SES, Resend, local SMTP, etc.).\n- **Normalized webhook events** — Anymail event taxonomy verbatim\n  (`queued`, `sent`, `bounced`, `delivered`, `opened`, `clicked`,\n  `complained`, `unsubscribed`, …) with `reject_reason` enum.\n  Postmark, SendGrid, Mailgun, SES, and Resend are all shipped\n  first-party providers, and matched `:bounced`, `:complained`, and\n  `:unsubscribed` events project suppressions automatically.\n- **Test assertions** — `assert_mail_sent/1`, `last_mail/0`,\n  `wait_for_mail/1`, plus `MailerCase`, `WebhookCase`, `AdminCase`\n  templates.\n- **Telemetry spans** on every entry point with a PII whitelist\n  (counts, IDs, and latencies — never addresses or bodies).\n- **Optional deps** gated via `Mailglass.OptionalDeps.*`:\n  [`oban`](https://hex.pm/packages/oban),\n  [`opentelemetry`](https://hex.pm/packages/opentelemetry),\n  [`mjml`](https://hex.pm/packages/mjml),\n  [`gen_smtp`](https://hex.pm/packages/gen_smtp),\n  [`sigra`](https://hex.pm/packages/sigra).\n\n## Packages\n\n| Package             | Status                   | What it is |\n|---------------------|--------------------------|------------|\n| `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. |\n| `mailglass_admin`   | Narrow `v1.x` admin contract documented separately | Mountable LiveView dashboard with stable router/auth/operator seams and internal UI implementation details. |\n| `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. |\n\n## Roadmap\n\n- **v0.2 — Production-credible core** — native `Mailglass.Message`\n  setter API, `mix mailglass.upgrade.v0_2`, message-stream policy,\n  RFC 8058 unsubscribe, webhook-driven suppression projection, linked\n  release hardening, and release-blocking Tier 1 docs.\n- **v0.5 — Deliverability + admin** — prod-mountable admin,\n  `mix mail.doctor` deliverability checks,\n  per-tenant adapter resolver, per-domain rate limiting.\n- **v1.0** — API stability lock, production references, long-lived\n  deprecation policy.\n\nFull trajectory in [`.planning/ROADMAP.md`](.planning/ROADMAP.md) and\n[`.planning/PROJECT.md`](.planning/PROJECT.md).\n\n## Documentation\n\n- [`guides/getting-started.md`](guides/getting-started.md) — install,\n  route mounting, and first delivery\n- [`guides/compatibility-and-deprecations.md`](guides/compatibility-and-deprecations.md)\n  — canonical `1.x` compatibility, deprecation, and support-matrix policy\n- [`guides/upgrading-to-v1_0.md`](guides/upgrading-to-v1_0.md) — canonical\n  latest-`0.x` to `1.0` upgrade path\n- [`guides/upgrading-from-v0_1.md`](guides/upgrading-from-v0_1.md) —\n  codemod-backed upgrade path for existing adopters\n- [`guides/migration-from-swoosh.md`](guides/migration-from-swoosh.md)\n  — move from raw Swoosh to the mailglass pipeline\n- [`guides/authoring-mailables.md`](guides/authoring-mailables.md) —\n  native setter API and `update_swoosh/2` escape hatch\n- [`guides/unsubscribe.md`](guides/unsubscribe.md) — RFC 8058 route,\n  token, and rollout contract\n- [`guides/dkim-setup.md`](guides/dkim-setup.md) — DKIM `h=` checks for\n  one-click unsubscribe\n- [`guides/webhooks.md`](guides/webhooks.md) — webhook ingest,\n  verification, suppression, and retention\n- [`guides/rate-limiting.md`](guides/rate-limiting.md) — multi-bucket throughput protection\n\n## Contributing\n\nMailglass is developed in public. Contributor conventions, decision\nlog, and phase-by-phase roadmap live in [`CLAUDE.md`](CLAUDE.md) and\n[`.planning/PROJECT.md`](.planning/PROJECT.md); a dedicated `CONTRIBUTING.md` lands in\nPhase 7.\n\nReproduce the default CI gate locally:\n\n```bash\nmix verify.foundation\nmix verify.cold_start\nmix compile --no-optional-deps --warnings-as-errors\n```\n\n## License\n\nMIT. The license is declared in [`mix.exs`](mix.exs) and applies across\nall sibling packages.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsztheory%2Fmailglass","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsztheory%2Fmailglass","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsztheory%2Fmailglass/lists"}