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:

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

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

Mailboxes 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_supervisor

The 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:

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-errors

Those 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