Mailglass.Webhook.Plug (Mailglass v1.4.4)

Copy Markdown View Source

Single-ingress webhook orchestrator.

Plugged at adopter-mounted paths via Mailglass.Webhook.Router. Owns the full request lifecycle:

  1. Extract raw_body from conn.private[:raw_body] (populated by Mailglass.Webhook.CachingBodyReader in the adopter's Plug.Parsers :body_reader)
  2. Dispatch to Mailglass.Webhook.Provider impl per route opts (provider: :postmark | :sendgrid | :mailgun)
  3. Provider.verify!/3 — raises %SignatureError{} on failure
  4. Mailglass.Tenancy.resolve_webhook_tenant/1 — runs after verify
  5. Mailglass.Tenancy.with_tenant/2 BLOCK form — clean tenant cleanup on raise (Pitfall 7)
  6. Provider.normalize/2 — pure, returns [%Event{}]
  7. Mailglass.Webhook.Ingest.ingest_multi/3 — single Ecto.Multi inside Repo.transact/1
  8. Post-commit: Mailglass.Outbound.Projector.broadcast_delivery_updated/3 per matched delivery (events_with_deliveries 3-tuples)
  9. send_resp(conn, 200, "")

Response code matrix

OutcomeStatusNotes
Success200Normal happy path
Duplicate replay (UNIQUE collision)200Idempotent — provider sees no error
%SignatureError{} (any of 7+ atoms)401Logger.warning with provider + atom
%TenancyError{:webhook_tenant_unresolved}422Distinct from signature failure
%ConfigError{:webhook_caching_body_reader_missing}500Adopter wiring gap
%ConfigError{:webhook_verification_key_missing}500Missing provider secret
Ingest {:error, reason}500Logger.error with reason atom only

Telemetry

Emits [:mailglass, :webhook, :ingest, :start | :stop | :exception] around the entire call/2 body via Mailglass.Webhook.Telemetry.ingest_span/2. Stop metadata is intentionally whitelisted: %{provider, tenant_id, status, event_count, duplicate, failure_reason} — never IP, headers, or payload bytes.

Also emits [:mailglass, :webhook, :signature, :verify, :start | :stop | :exception] around Provider.verify!/3 via Mailglass.Webhook.Telemetry.verify_span/2.

Failure log discipline

Logger.warning on signature failure includes provider + atom reason only. Never the source IP, headers, or payload excerpts. Adopters wanting IP-based abuse triage attach their own telemetry handler on [:mailglass, :webhook, :signature, :verify, :stop] with status: :failed and pull conn.remote_ip from their own plug lineage.

Forward-declared contracts

Mailglass.Webhook.Ingest.ingest_multi/3 is referenced directly; the @compile {:no_warn_undefined, ...} attribute below suppresses compile warnings until the module is available.