Skip to content

Add SMTP2GO delivery webhook#139

Merged
SiteRelEnby merged 1 commit into
mainfrom
smtp2go-webhook
Jun 12, 2026
Merged

Add SMTP2GO delivery webhook#139
SiteRelEnby merged 1 commit into
mainfrom
smtp2go-webhook

Conversation

@SiteRelEnby

Copy link
Copy Markdown
Contributor

Fast-follow to the email deliverability lifecycle (#138). Wires SMTP2GO's webhook into the same machinery so bounce suppression and soft-bounce self-healing work when sending via SMTP2GO over the smtp backend - needed before migrating off SendGrid.

Event mapping

POST /v1/webhooks/smtp2go/events dispatches into the existing apply_bounce / apply_complaint / apply_delivered transitions:

  • delivered -> clears transient soft-bounce state (the greylist self-heal)
  • bounce -> hard/soft from SMTP2GO's bounce classification field. An unclassified/missing classification defaults to soft - the conservative choice, since soft doesn't block until the threshold, so a misclassification can't wrongly lock anyone out.
  • spam -> complaint
  • reject / open / click / unsubscribe / resubscribe / processed -> ignored. reject specifically is SMTP2GO refusing to send to an address that already hard-bounced/complained/unsubscribed, so it carries no new state.

Recipient is read from SMTP2GO's rcpt field (not email).

Auth and payload format

SMTP2GO doesn't sign payloads (confirmed against their docs - no HMAC), so the endpoint is guarded by a shared secret in the URL: SMTP2GO_WEBHOOK_SECRET, constant-time compared against the token query param, 404 when unset, 403 on mismatch. Pair with SMTP2GO's source-IP allowlist at the proxy for defence in depth, since the secret travels in the URL.

SMTP2GO's output type is operator-configurable (JSON or form-encoded), so the endpoint accepts both rather than silently 400ing a webhook left on the wrong type - JSON may be a single object or an array, form-encoded is a single event. Parsing is a pure parse_smtp2go_payload helper.

Tests

tests/test_webhooks_smtp2go.py - 14 unit tests: the smtp2go_event_action mapper and parse_smtp2go_payload (JSON object, JSON array, content-type sniffing, form-encoded, empty/garbage -> error). The apply_* transitions are covered by test_email_events.py. ruff clean.

Note

The endpoint is exercised here at the mapper/parser level; an end-to-end test against a live SMTP2GO event fire (and confirming whether SMTP2GO batches events into a JSON array vs one-per-POST) is worth doing when wired up - the code handles both shapes regardless.

Feeds SMTP2GO's delivery/bounce/spam events into the same
deliverability lifecycle as the SES and SendGrid handlers, so bounce
suppression and soft-bounce self-healing work when sending via SMTP2GO
over the smtp backend.

Event mapping (pure, unit-tested): delivered clears transient
soft-bounce state; bounce maps SMTP2GO's hard/soft classification, with
an unclassified bounce defaulting to soft (conservative - soft doesn't
block until the threshold, so a misclassification can't lock anyone
out); spam is a complaint. reject is ignored - SMTP2GO emits it when
refusing an already-flagged address, so it carries no new state.

SMTP2GO doesn't sign payloads, so the endpoint is guarded by a shared
secret in the URL (SMTP2GO_WEBHOOK_SECRET; 404 when unset, constant-time
compared). Accepts either webhook output type - JSON (single object or
array) or form-encoded (single event) - so a webhook left on the wrong
output type still works instead of silently rejecting every event.
Recipient is read from SMTP2GO's `rcpt` field.
@SiteRelEnby SiteRelEnby enabled auto-merge June 12, 2026 22:21
@SiteRelEnby SiteRelEnby merged commit 5c10f85 into main Jun 12, 2026
4 checks passed
@SiteRelEnby SiteRelEnby deleted the smtp2go-webhook branch June 12, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant