Work through this checklist before routing live traffic through mailglass 2.x.
Each section is a focused verification step with links to the canonical guide for
detail. Complete every section, then run mix mailglass.preflight as the final
release gate before marking the deployment ready.
Deliverability: mix mail.doctor
Run DNS-only DKIM, SPF, and DMARC checks against your sending domain before launch:
mix mail.doctor --domain yourdomain.com
mix mail.doctor --domain yourdomain.com --dkim-selector default
mix mail.doctor --domain yourdomain.com --verbose
mix mail.doctor --domain yourdomain.com --format json
This command requires the application to be running — it calls Mix.Task.run("app.start") internally. Run it against the real sending domain, not localhost. A clean run confirms DNS records are discoverable by receiving mail servers.
For DKIM record setup and selector configuration, see DKIM Setup.
Webhook wiring: mix mailglass.doctor
Verify that Mailglass.Webhook.CachingBodyReader is wired correctly in endpoint.ex before accepting webhook events from your provider:
mix mailglass.doctor
This check is OFFLINE — it reads endpoint.ex from disk and does not boot the host application. Three-state exit:
- Exit
0—CachingBodyReaderis wired correctly; webhooks will verify. - Exit
1—CachingBodyReaderis absent. Runmix mailglass.installto fix. - Exit
2— Cannot diagnose;endpoint.exwas not found or the OTP app is not detectable.
If exit 1, run mix mailglass.install to wire the parser automatically, then re-run mix mailglass.doctor to confirm exit 0. An unwired CachingBodyReader produces silent 401 failures on every incoming webhook — signature verification cannot proceed without the raw request bytes.
For the full webhook setup walkthrough, see Webhooks.
Webhook secret provisioning and rotation
Provision the webhook verification secret from your provider's dashboard at deploy time. For Postmark, this is the Basic Auth user and password. For SendGrid, it is the Event Webhook public key. For Mailgun, it is the Webhook Signing Key.
Set the credential as an environment variable and reference it in your mailglass config — never hardcode secrets in config/prod.exs.
To rotate a webhook secret:
- Obtain the new credential from the provider dashboard.
- Update the environment variable in your deployment environment.
- Redeploy.
- Run
mix mailglass.doctorto confirm the wiring is intact.
For setup details and provider-specific config keys, see Webhooks. For incident recovery when webhooks stop verifying after a rotation, see the guides/webhook-troubleshooting.md runbook in the repository.
Durable async readiness and final preflight
Mailglass.Outbound.Worker runs under Oban when you call deliver_later/2. Production must explicitly select the durable adapter and configure its only outbound queue, :mailglass_outbound. A concurrency of 10 is a conservative starting point for moderate send volume; adjust it for delivery lag and your ESP's rate limits.
Before routing live traffic, add this configuration:
config :mailglass, async_adapter: :oban
config :my_app, Oban,
queues: [mailglass_outbound: 10]Then run the final preflight from a booted release environment:
mix mailglass.preflight
mix mailglass.preflight reports the bounded readiness classes for Repo, schema
and migrations, adapter, signing, :mailglass_outbound queue, payload
maintenance, and authenticated operator access. Fix every reported class and
rerun it; it never emits configured secret values. It rejects :task_supervisor:
Task.Supervisor is not durable. Normal Oban must both advertise
mailglass_outbound and have a live consumer polling it.
The generated-host proof exercises this same package-shaped configuration with an active consumer; adopters should still run this command in their release.
If you only call deliver/2, this section does not apply — deliver/2 is synchronous and bypasses the job queue entirely.
For authoring mailables and choosing between deliver/2 and deliver_later/2, see Authoring Mailables.
Payload retention and reconciliation
Provider acceptance and Mailglass persistence are separate systems. Mailglass
uses an at-least-once boundary and records :retryable, :terminal, or
:uncertain outcomes. An uncertain outcome may already have reached the
provider: reconcile with provider evidence and correlation data; there is no
automatic resend.
Set a finite private-content policy appropriate to your support window. The
defaults are terminal/discarded/abandoned 14 days, uncertain 30 days,
legacy queued content 14 days, and one prune batch of 500 payloads:
config :mailglass,
outbound_payload_retention: [
terminal_days: 14,
uncertain_days: 30,
legacy_days: 14,
prune_batch_size: 500
]Successful durable payloads are atomically scrubbed, preserving only a
non-content tombstone. Run the manual, Oban-independent operation for one
tenant at a time; --tenant TENANT_ID is mandatory and each invocation handles
at most one batch:
mix mailglass.outbound.payloads.prune --tenant TENANT_ID
For optional scheduling, use Mailglass.Outbound.PayloadPrunerWorker on
:mailglass_maintenance with exactly one mailglass_tenant_id argument. Do
not configure an all-tenant sweep or add maintenance work to
:mailglass_outbound delivery readiness.
Per-tenant adapter routing
If your application routes email through different ESPs per tenant — for example, one tenant on Postmark and another on SendGrid — implement the Mailglass.Tenancy.resolve_outbound_adapter_ref/1 callback in your tenancy module. It receives a context map (%{tenant_id, message, mode}) and returns {:ok, adapter_ref} or :default:
defmodule MyApp.Tenancy do
@behaviour Mailglass.Tenancy
@impl true
def resolve_outbound_adapter_ref(%{tenant_id: tenant_id}) do
{:ok, adapter_ref_for(tenant_id)}
end
endWithout this callback, all tenants share the single adapter configured under config :mailglass, adapter:.
For the full callback interface, named adapter ref setup, and config examples, see Multi-Tenancy.
Suppression strategy
The suppression list blocks delivery to opted-out, hard-bounced, and tenant-excluded recipients. When delivery is blocked, mailglass raises Mailglass.SuppressedError with a :type of :address, :domain, or :address_stream. This is a permanent policy block — never retryable.
Confirm your application handles Mailglass.SuppressedError without treating it as an unexpected failure: a suppression hit is expected behavior, not a bug.
For RFC 8058 List-Unsubscribe wiring and suppression record management, see Unsubscribe. The Mailglass.Suppression module exposes functions for querying and managing suppression records programmatically.
One-click unsubscribe convergence
Exercise the real built-in route with a real signed bulk-delivery link, then
check the actual Mailglass.Outbound preflight boundary. The POST and valid
replay each return a byte-empty 200 with no redirect; a genuine convergence
failure is byte-empty 500 and must leave no partial event/suppression pair.
After the first POST, verify that a same-tenant message to the Delivery-derived
normalized address in its originating stream is blocked before the adapter runs.
Use transactional and unrelated-stream messages as negative controls; they must
remain eligible. Also use a second tenant as an isolation control. This confirms
the canonical event plus immutable :address_stream suppression and the actual
prefix/tenant-safe preflight behavior, not merely row existence.
Record only bounded outcome facts (for example, Delivery ID and blocked/pass classification). Never log or paste the signed token, recipient address, or private message content into support artifacts. Lifecycle and broadcast work is separate best-effort post-commit work; verify its failure does not change the already-successful POST. This checklist does not prove arbitrary-host exactly once behavior.
Signed feedback and suppression outcome
Configure each provider feedback route with its signing material and perform signed feedback verification with one valid request before launch. A forged or unsigned request must fail closed with no durable delivery, event, or suppression change. Do not log the request body, address, token, or secret. Verify the expected suppression outcome: a matching same-tenant bulk delivery is blocked before adapter work; transactional and unrelated-stream controls remain eligible. one-click POST uses the same scoped suppression contract.
Authenticated operator access
Mount mailglass_operator_routes only inside a host-owned authenticated browser
pipeline. Verify anonymous access is denied and an authorized operator can open
the route after preflight passes. Mailglass does not provide a login system or
decide who may operate this surface.
Telemetry and alerting
mailglass emits telemetry on these event families:
[:mailglass, :outbound, :dispatch, :start | :stop | :exception][:mailglass, :render, :message, :start | :stop | :exception][:mailglass, :webhook, :ingest, :start | :stop | :exception][:mailglass, :webhook, :reconcile, :start | :stop | :exception]
Before going live, attach at minimum:
- An
:exceptionhandler on:outbound, :dispatchto track delivery failures. - An
:exceptionhandler on:webhook, :reconcileto catch reconciliation failures.
PII is never emitted in telemetry metadata by convention — :to, :from, :body, :subject, and :recipient are not present in any metadata map.
For the full telemetry reference including all metadata keys, see Telemetry.