Orchestrates inbound SNS webhook delivery: token auth, signature verification, SES event normalization, persistence, and suppression rules.
This is the single entry point the webhook controller calls with the raw
request body and the :token path parameter. Every inbound payload is
logged to webhook_logs (via SquatchMail.Tracker.log_webhook/1)
regardless of outcome, so ingestion failures are always inspectable.
SES event schema families
SES publishes events in two shapes depending on how the host configured notifications:
- Event publishing (configuration sets) - top-level
"eventType", e.g."Bounce","Send","Open". - Legacy notifications (identity-level feedback forwarding) -
top-level
"notificationType", limited to"Bounce","Complaint","Delivery".
Both are normalized to the same lowercase event_type vocabulary before
reaching SquatchMail.Tracker.record_event/1.
Idempotency
SNS is at-least-once delivery and retries on non-2xx responses, so the same
SES event can arrive more than once. Events are deduped on
(message_id, event_type, recipient, occurred_at) via an application-level
existence check immediately before insert. This is a check-then-insert, not
a database constraint, so there is a race window between two concurrent
deliveries of the same retried event; a unique index would close it fully
but requires a migration version bump, which is out of scope here. Given
SNS retries are seconds-to-minutes apart in practice (not concurrent), this
tradeoff is acceptable for now - noted so a future migration can add the
constraint.
Summary
Functions
Processes a raw inbound webhook request body for the source identified by
token (the path token, SquatchMail.Source.webhook_token).
Types
@type outcome() :: :processed | :ignored
@type reason() :: :invalid_token | :invalid_json | :signature_invalid | term()
Functions
Processes a raw inbound webhook request body for the source identified by
token (the path token, SquatchMail.Source.webhook_token).
Returns {:ok, :processed} for messages that resulted in stored events,
confirmed subscriptions, etc; {:ok, :ignored} for messages intentionally
not acted on (e.g. UnsubscribeConfirmation, unknown event types);
{:error, reason} otherwise. Every call logs to webhook_logs, including
on error paths that occur before a Source can even be resolved.