feat(notifications): per-recipient mute + List-Unsubscribe (closes #297)#828
feat(notifications): per-recipient mute + List-Unsubscribe (closes #297)#828cristim wants to merge 2 commits into
Conversation
Migration 000061 adds muted_recipients table keyed on (email, scope). GET /api/notifications/unsubscribe verifies an HMAC-signed token and upserts the mute row (AuthPublic, mirrors approve/cancel pattern). SendPurchaseApprovalRequest checks the mute table before each send and attaches List-Unsubscribe + List-Unsubscribe-Post headers (RFC 8058) when an unsubscribe base URL is configured. Token signing uses HMAC-SHA256 over (lower(email)|scope) via NOTIFICATION_MUTE_SECRET.
|
Warning Review limit reached
More reviews will be available in 23 minutes and 33 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (20)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
…828) UpsertNotificationMute and IsNotificationMuted were added to config.StoreInterface (issue #297) but the stub implementations in five test files were not updated, causing go vet to fail with interface satisfaction errors. Affected mocks: - internal/analytics/collector_test.go (mockConfigStore) - internal/purchase/mocks_test.go (MockConfigStore) - internal/scheduler/scheduler_overrides_test.go (mockOverrideStore) - internal/scheduler/scheduler_suppressions_test.go (mockSuppressionStore) - internal/scheduler/scheduler_test.go (MockConfigStore) - internal/server/test_helpers_test.go (mockConfigStoreForHealth)
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Summary
muted_recipientstable keyed on(recipient_email, scope)) for per-recipient, per-scope opt-out persistence.GET /api/notifications/unsubscribe?token=...&email=...&scope=...(AuthPublic) that verifies an HMAC-SHA256 signed token, upserts the mute row, and returns a minimal HTML confirmation page (RFC 8058 one-click flow).List-Unsubscribe/List-Unsubscribe-Postheaders intoSendPurchaseApprovalRequest; the SES send is skipped silently when the recipient is muted, and the headers are attached whenunsubscribeBaseURLis configured.Design notes
HMAC-SHA256(NOTIFICATION_MUTE_SECRET, lower(email)+"|"+scope)inpkg/common.DeriveMuteToken; compared withhmac.Equal(constant time). Dev/test fallback key used when env var is unset.purchase_approvals/ri_exchange_approvals).us***@example.com(redacted).Test plan
go test github.com/LeanerCloud/CUDly/internal/email/... github.com/LeanerCloud/CUDly/internal/api/... github.com/LeanerCloud/CUDly/pkg/common/...passes (1867 tests)TestUnsubscribeHandler_Success- valid signed token stores mute row, returns HTML pageTestUnsubscribeHandler_ForgedToken_Returns401- bad token rejectedTestSendPurchaseApprovalRequest_MutedRecipient_NoSESCall- muted address skips SES callTestSendPurchaseApprovalRequest_NotMuted_SendsEmail- normal path unchangedTestSendPurchaseApprovalRequest_MuteCheckError_FailOpen- DB error doesn't block sendList-Unsubscribecarries angle-bracketed HTTPS URL;List-Unsubscribe-Post: List-Unsubscribe=One-Clickpresent when base URL configured