MailglassInbound
mailglass_inbound is the inbound sibling package for Mailglass. This README
is the canonical adoption lane for the shipped inbound slice: install the
package manually, wire the endpoint/body-reader path explicitly, mount the
provider plugs you need, choose the async execution mode you want, and verify
the contract with the package test lanes.
Stable Package Surface
The stable inbound package contract is intentionally narrow:
MailglassInboundMailglassInbound.InboundMessageMailglassInbound.Ingress.PlugMailglassInbound.Ingress.CachingBodyReaderMailglassInbound.RouterMailglassInbound.Mailbox
Use docs/api_stability.md as the canonical inventory
for what is stable, what is internal, and what is still deferred.
Manual Setup
Inbound setup is manual in this phase. There is no generated setup path for
mailglass_inbound, so adopters should wire the package explicitly.
1. Add the dependency and fetch deps
defp deps do
[
{:mailglass_inbound, "~> 0.3.2"},
{:mailglass, "~> 0.3.2"},
{:oban, "~> 2.21"}
]
end
If you do not want durable background execution yet, omit {:oban, "~> 2.21"}
and the package will use the bounded fallback described below.
Fetch dependencies:
mix deps.get2. Run the inbound migrations
mailglass_inbound persists canonical receive truth, raw evidence, and
append-only execution lineage. Run the package migrations in the host app
before mounting ingress:
mix ecto.migrate
3. Wire Plug.Parsers with the package body reader
Postmark verification requires the exact request bytes. SendGrid raw MIME delivery does not need the cached raw body, but the shared ingress path should still be wired once at the endpoint:
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Jason,
body_reader: {MailglassInbound.Ingress.CachingBodyReader, :read_body, []}
Without that body_reader, the Postmark path fails closed with
:webhook_caching_body_reader_missing.
4. Define the router and mailboxes
defmodule MyApp.MailglassInboundRouter do
use MailglassInbound.Router
route MyApp.Mailboxes.SupportMailbox, recipient: "support@example.com"
endMailboxes implement one stable callback:
defmodule MyApp.Mailboxes.SupportMailbox do
@behaviour MailglassInbound.Mailbox
@impl true
def process(message) do
_ = message
:accept
end
end
Supported outcomes are :accept, :ignore, {:reject, reason}, and
{:bounce, reason}.
5. Mount the provider ingress paths
Mount one obvious route per provider you are using:
forward "/inbound/:tenant_id/postmark",
MailglassInbound.Ingress.Plug,
provider: :postmark,
router: MyApp.MailglassInboundRouter
forward "/inbound/:tenant_id/sendgrid",
MailglassInbound.Ingress.Plug,
provider: :sendgrid,
router: MyApp.MailglassInboundRouter
The plug verifies first, resolves the tenant, normalizes into
%MailglassInbound.InboundMessage{}, and persists canonical and raw evidence
rows before mailbox execution is dispatched for newly inserted records.
6. Configure each provider
config :mailglass_inbound, :postmark,
basic_auth: {"postmark-user", "postmark-pass"},
ip_allowlist: []
config :mailglass_inbound, :sendgrid,
basic_auth: {"sendgrid-user", "sendgrid-pass"}Postmark uses shared-secret basic auth plus optional IP allowlisting. SendGrid ships shared-secret basic auth only in this slice.
7. Choose the async execution mode
Oban-backed execution is the durable path. When Oban is present, new matched records are enqueued through an internal worker after persistence commits.
Task.Supervisor fallback is bounded best-effort only. When Oban is absent, the package starts a supervised background task with no durable enqueue and no automatic retry. Recovery after node loss or shutdown depends on replay or operator action over the stored receive truth.
You may force fallback mode explicitly:
config :mailglass_inbound, :async_adapter, :task_supervisorThe public contract does not include Oban job shapes, queue names, worker modules, or replay orchestration details.
Provider Notes
Keep the README as the primary setup path, then use the focused provider guides for provider-specific caveats:
docs/postmark_ingress.mdfor raw-body verification, duplicate behavior, and Postmark-specific persistence notesdocs/sendgrid_ingress.mdfor raw MIME delivery, duplicate fingerprinting, and replay/recovery posture
Receive, Duplicate, And Replay Truth
The package stores two kinds of truth:
- canonical normalized rows for stable routing and mailbox inputs
- raw evidence for provider payloads, raw MIME, verification facts, parse warnings, and replay support
Fresh ingress persists canonical plus raw evidence truth before any mailbox execution is dispatched. Provider retries are acknowledged from receive truth; mailbox outcomes do not drive provider retry semantics.
Duplicate fresh ingress is explicit and provider-specific:
-
Postmark collapses on
(tenant_id, provider, provider_message_id) -
SendGrid collapses on
(tenant_id, provider, raw_mime_fingerprint)
Replay is a recovery operation over stored canonical plus raw evidence truth. It is not a fresh provider receive, it does not silently reroute to a different mailbox, and it is not a public API in this phase.
Verification Commands
Recommended package proof lanes after wiring the package:
mix test test/mailglass_inbound/docs_contract_test.exs --warnings-as-errors
mix test test/mailglass_inbound/ingress/plug_test.exs test/mailglass_inbound/replay_test.exs --warnings-as-errorsThose lanes prove the package docs, duplicate handling, async ingress acknowledgment, replay posture, and stability claims stay aligned with shipped behavior.
Deferred Beyond This Slice
These capabilities remain intentionally out of the stable inbound contract:
- a publicly stable replay/command-surface API
- an operator dashboard for inbound receive or replay flows
- direct worker contracts, queue configuration, or Oban job return values
- providers beyond Postmark and SendGrid
- matcher expansion beyond recipient, subject, and headers