SquatchMail.SNS.Processor (SquatchMail v0.1.0)

Copy Markdown View Source

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

outcome()

@type outcome() :: :processed | :ignored

reason()

@type reason() :: :invalid_token | :invalid_json | :signature_invalid | term()

Functions

process(raw_body, token)

@spec process(String.t(), String.t()) :: {:ok, outcome()} | {:error, reason()}

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.