Commit 80db5d9
feat: add List-Unsubscribe headers and throttle email sends (#2507)
* feat: add List-Unsubscribe headers and throttle email sends
- Add List-Unsubscribe and List-Unsubscribe-Post headers to all
outbound emails for Gmail/RFC 8058 one-click unsubscribe compliance
- Reduce email queue concurrency from 30 to 10
- Add 1s delay between sends to avoid email spikes that trigger
reputation systems
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use setTimeout instead of wait.for for email throttling
wait.for suspends execution and frees the concurrency slot,
defeating the throttling purpose. setTimeout holds the slot
occupied for 1s, actually spacing out sends.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use real recipient email for unsubscribe URL, not test override
When RESEND_TO_TEST is set, toAddress becomes the test email.
The unsubscribe URL should always reference the real recipient
(params.to) so the token validates correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove List-Unsubscribe-Post, add mailto fallback
The one-click POST handler doesn't exist yet (unsubscribe page is
GET only). Removed List-Unsubscribe-Post to avoid claiming RFC 8058
support we don't have. Added mailto fallback for broader client
compatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add RFC 8058 one-click unsubscribe POST endpoint
- New POST /v1/email/unsubscribe endpoint that accepts email+token
via query params, verifies HMAC token, and unsubscribes the user
- No auth required (token IS the auth, Gmail needs to POST directly)
- Re-add List-Unsubscribe-Post header now that the handler exists
- List-Unsubscribe URL points to API endpoint for one-click POST
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove dead import, use timing-safe token comparison
- Remove unused getUnsubscribeUrl import from send-email.ts
- Use crypto.timingSafeEqual for HMAC token verification in
unsubscribe endpoint
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: guard against type confusion on query/body params
CodeQL flagged that query params could be arrays. Explicitly
coerce to string before using.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: include findingNotifications in unsubscribe preferences
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>1 parent 56ba6ec commit 80db5d9
3 files changed
Lines changed: 88 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
7 | | - | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
7 | | - | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
54 | 64 | | |
55 | 65 | | |
56 | 66 | | |
57 | 67 | | |
58 | 68 | | |
59 | 69 | | |
| 70 | + | |
60 | 71 | | |
61 | 72 | | |
62 | 73 | | |
| |||
76 | 87 | | |
77 | 88 | | |
78 | 89 | | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
79 | 93 | | |
80 | 94 | | |
81 | 95 | | |
| |||
0 commit comments