This guide covers all ten mailglass error structs — their causes, fix actions, and remediation pointers. Every struct has a closed :type atom set. Always match on the struct module and :type field, never on :message:
case result do
{:error, %Mailglass.SuppressedError{type: :address}} -> ...
{:error, %Mailglass.RateLimitError{retry_after_ms: ms}} -> ...
{:error, %Mailglass.SendError{}} -> ...
endMessage strings are a presentation concern. The closed :type atom set is the stable contract. Canonical closed atom sets and Retryable values for each struct are documented in docs/api_stability.md.
For symptom-first incident runbooks — where you start from what went wrong in production rather than which struct appeared — use the guides/operator-incident-support.md runbook in the repository. For webhook-specific incidents, see the guides/webhook-troubleshooting.md runbook.
SendError
Raised when email delivery fails.
The four type atoms are :adapter_failure, :rendering_failed, :preflight_rejected, and :serialization_failed. :adapter_failure means the Swoosh adapter returned an error — this is the only retryable type. :rendering_failed means the HEEx or CSS-inlining pipeline could not produce a valid message. :preflight_rejected means a suppression or rate-limit check blocked the send before reaching the adapter. :serialization_failed means the rendered message could not be serialized into the form the adapter requires.
To fix an :adapter_failure, check your ESP credentials, the provider's status page, and the :cause field for the raw adapter error. To fix :rendering_failed, inspect the :cause for the underlying Mailglass.TemplateError. To fix :preflight_rejected, the upstream suppression or rate-limit check is the authoritative signal — inspect the wrapped error in :cause. For :serialization_failed, verify the message assigns produce valid content and that attachments are well-formed.
The :delivery_id field is populated when the delivery row was persisted before the failure; use it to correlate with the mailglass_events ledger.
For the canonical closed :type atom set and Retryable policy, see docs/api_stability.md.
TemplateError
Raised when a template cannot be compiled or rendered.
The four type atoms are :heex_compile, :missing_assign, :helper_undefined, and :inliner_failed. :heex_compile means HEEx compilation failed — a syntax error, unclosed tag, or invalid expression in the template source. :missing_assign means a required assign was not passed to render/3; the :context map carries :assign with the name of the missing key. :helper_undefined means the template references a helper function that is not defined. :inliner_failed means Premailex or the configured CSS inliner raised an error during the CSS-inlining pass.
To fix a :heex_compile error, correct the template syntax and recompile. For :missing_assign, add the assign to the deliver/2 call or set a default in the mailable's build/2. For :helper_undefined, define the helper or import the module that contains it. For :inliner_failed, check that the CSS is valid and that the inliner dependency is available.
TemplateError is never retryable — the template must be fixed before re-sending.
For the canonical closed :type atom set and Retryable policy, see docs/api_stability.md.
SignatureError
Raised when webhook signature verification fails.
SignatureError is never retryable. A signature failure means the request is either misconfigured (wrong secret, rotated key not yet deployed) or is a forgery. Let the process crash under supervision and surface a 4xx to the provider — do not suppress or catch this error silently.
The current type atoms are :missing_header, :malformed_header, :bad_credentials, :ip_disallowed, :bad_signature, :timestamp_skew, and :malformed_key. Three legacy atoms remain in the closed set for backward compatibility: :missing (alias of :missing_header), :malformed (alias of :malformed_header), and :mismatch (alias of :bad_signature). Match the current atoms in new code; the legacy atoms will not be removed in a minor release.
:missing_header — the provider's signature header is absent from the request. :malformed_header — the header is present but cannot be parsed (bad Base64, missing prefix); the :context map may carry a :detail string. :bad_credentials — Postmark Basic Auth user/pass mismatch. :ip_disallowed — the source IP is not in the configured Postmark allowlist (opt-in check). :bad_signature — HMAC or ECDSA verification returned false. :timestamp_skew — the signed timestamp is outside the acceptable tolerance window. :malformed_key — the PEM or DER decode failed at config validate-at-boot time; fix your provider config and restart.
To fix most signature errors: verify the secret in your deployment environment matches the credential in the provider dashboard, run mix mailglass.doctor to confirm CachingBodyReader is wired, and redeploy. For :malformed_key, check the PEM/DER format of the key you have configured.
For webhook-specific incident recovery, see the guides/webhook-troubleshooting.md runbook in the repository.
For the canonical closed :type atom set and Retryable policy, see docs/api_stability.md.
SuppressedError
Raised when delivery is blocked by the suppression list.
SuppressedError is a permanent policy block — never retryable. A suppressed recipient opted out, hard-bounced, or was explicitly excluded by the tenant. Treat this as expected behavior, not a delivery failure.
The three type atoms are :address, :domain, and :address_stream. :address means the recipient address is globally suppressed. :domain means the recipient domain is globally suppressed. :address_stream means the recipient is suppressed for a specific stream (for example, :bulk); the :context map carries :stream with the stream atom.
The :context map is PII-safe and carries :tenant_id, :stream, :reason, :source, and :expires_at for diagnostic correlation.
To fix: if the suppression was created in error, remove the record via Mailglass.Suppression and re-send. If the suppression is legitimate, your application should not retry delivery to this recipient.
For RFC 8058 List-Unsubscribe wiring and suppression record management, see Unsubscribe.
For the canonical closed :type atom set and Retryable policy, see docs/api_stability.md.
RateLimitError
Raised when a rate limit is exceeded.
RateLimitError is always retryable. The :retry_after_ms field carries the number of milliseconds to wait before retrying. When Oban is in use, the worker handles backoff automatically.
The three type atoms are :per_domain, :per_tenant, and :per_stream. :per_domain means the recipient domain is over its configured rate limit. :per_tenant means the sending tenant is over its configured rate limit. :per_stream means the delivery stream (:transactional, :operational, or :bulk) is over its limit.
To fix: honor the :retry_after_ms value. For persistent rate-limit failures, review your mailglass rate-limit config and your ESP's sending quotas, and consider reducing the Oban queue concurrency for the :mailglass queue.
For the canonical closed :type atom set and Retryable policy, see docs/api_stability.md.
ConfigError
Raised when mailglass is misconfigured.
ConfigError is never retryable. The host application must fix the configuration and restart. Mailglass.Config.validate_at_boot!/0 raises this at application startup for the most critical config problems.
The type atoms are: :missing (a required config key is not set), :invalid (a key is present but its value is not valid), :conflicting (two or more keys contradict each other), :optional_dep_missing (an optional dependency required for the selected config is not loaded), :tracking_on_auth_stream (open/click tracking is enabled on a mailable whose function name matches an auth-stream heuristic — forbidden), :tracking_host_missing (tracking is enabled but no tracking host is configured), :tracking_endpoint_missing (tracking is enabled but no Phoenix endpoint is configured for token signing), :webhook_verification_key_missing (a webhook provider is configured but its verification credentials are not set), and :webhook_caching_body_reader_missing (a webhook request arrived but conn.private[:raw_body] is missing, meaning CachingBodyReader is not wired).
The :context map carries :key for :missing and :invalid, :dep for :optional_dep_missing, and :hint for webhook config errors.
To fix: read the message string for the specific remediation instruction, correct the config in your application's config/ files, and restart. For :webhook_caching_body_reader_missing, run mix mailglass.install and verify with mix mailglass.doctor.
For the canonical closed :type atom set and Retryable policy, see docs/api_stability.md.
EventLedgerImmutableError
Raised when the mailglass_events append-only immutability trigger fires.
The event ledger is append-only by design. A Postgres BEFORE UPDATE OR DELETE trigger on mailglass_events raises SQLSTATE 45A01 for every mutation attempt. Mailglass.Repo.transact/1 translates the resulting %Postgrex.Error{} into this struct so callers pattern-match a mailglass-owned error rather than the raw Postgrex one.
The two type atoms are :update_attempt and :delete_attempt. EventLedgerImmutableError is never retryable — an immutability violation is a bug in the calling code.
To fix: locate the code path that attempted to UPDATE or DELETE a row in mailglass_events and remove it. Events must only be inserted; they can never be modified or deleted. The :pg_code field carries "45A01" for log correlation.
For the canonical closed :type atom set and Retryable policy, see docs/api_stability.md.
TenancyError
Raised when tenant context is required but not stamped on the process.
Mailglass.Tenancy.tenant_id!/0 raises this when the calling process has not been stamped via Mailglass.Tenancy.put_current/1. TenancyError is never retryable — the caller failed to establish tenant context before invoking mailglass.
The two type atoms are :unstamped and :webhook_tenant_unresolved. :unstamped means no tenant_id is present in the process dictionary. :webhook_tenant_unresolved means Mailglass.Tenancy.resolve_webhook_tenant/1 returned {:error, _} for a cryptographically verified webhook request — the request is authentic but your tenancy module could not map it to a known tenant.
To fix :unstamped: call Mailglass.Tenancy.put_current/1 in your on_mount/4 LiveView callback, your Plug pipeline, or your test setup before any mailglass call. To fix :webhook_tenant_unresolved: inspect :context for :provider and :reason, then update your tenancy resolver to handle the provider or tenant mapping that is failing.
For the canonical closed :type atom set and Retryable policy, see docs/api_stability.md.
StreamPolicyError
Raised when a message violates stream policy.
The single type atom is :stream_policy_violated — the message violates rules for its assigned stream. StreamPolicyError is never retryable — the message must be corrected before re-sending.
The :detail field, when present, carries a map with :rule (the rule atom that triggered) and :suggestion (a human-readable remediation hint). Inspect :detail to understand which rule fired and how to resolve it.
To fix: review the stream policy configured for the message's stream and correct the mailable so it satisfies the rule. The :detail map's :suggestion field provides a starting point.
Note: StreamPolicyError's type set is sourced from its module (lib/mailglass/errors/stream_policy_error.ex); it does not currently have a dedicated section in docs/api_stability.md. For general error contract context, see docs/api_stability.md.
PublishError
Raised when installer golden drift is detected during the mix mailglass.publish.check task.
The single type atom is :publish_blocked_golden_drift — the generated installer snippets do not match the expected golden files. PublishError is never retryable as a runtime error; it requires regenerating the goldens.
To fix: run the golden regeneration command:
MIX_INSTALLER_ACCEPT_GOLDEN=1 mix test test/mailglass/install/install_golden_test.exs --warnings-as-errors
This regenerates the golden files to match the current installer output. Review the diff, commit the updated goldens, and re-run mix mailglass.publish.check.
The :context map may carry :output with the subprocess output for diagnosis.
For the canonical closed :type atom set and Retryable policy, see docs/api_stability.md.