email: skip recipients with bounces/unsubscribes in last 365d#14
Merged
Conversation
Wires the worker's audit-log forwarder to the email_events table that PR #54 of the api repo creates. Before every SendEvent, the forwarder asks "has this recipient told us to stop?" — bounces and spam complaints suppress sends for 365d (the inbox may be fixed by then), unsubscribes suppress permanently. Implementation: - New suppressionChecker interface (production: sqlSuppressionChecker backed by *sql.DB; tests: in-memory map). - Two separate range-scan queries rather than one OR'd query, so the composite index (email, event_type, created_at DESC) is hit cleanly for both the "no decay" unsubscribe path and the "365d window" bounce/complaint path. - Fail-OPEN on DB errors. A Postgres blip in the suppression check must NOT pin the forwarder queue or block sends — sending a duplicate to a bouncing inbox during a DB outage is preferable to no sends at all. - Suppressed rows advance the cursor + bump the skipped counter. No recipient address is logged (PII); audit_id is enough for forensic lookup. Tests: 4 new (suppressed → skip + cursor advances; not-suppressed → still sends; unsubscribe = permanent; DB error → fail-open). All 13 existing forwarder tests still pass under -race. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SendEvent, the audit-log forwarder now checks the newemail_eventstable (created by api#56) and skips recipients who have already told us "stop". Suppressed rows still advance the cursor — the queue doesn't pin on poisoned addresses.audit_id+kindso operators can correlate forensically.Architecture
suppressionCheckeris an interface. Production wiressqlSuppressionChecker(queriesemail_eventson the same*sql.DBthe forwarder already holds —email_eventslives in the platform DB alongsideaudit_log). Tests injectmemSuppressionso the spec stays hermetic.Test plan
go test ./... -race -count=1— all packages greenemail_eventstablePushback
SuppressionWindow=365d, the event-type strings). Keeping them in sync is on the developer; a regression test that pulls both via go generate would be tighter but adds module-coupling we deliberately don't have today (worker has no API-module import).🤖 Generated with Claude Code