Skip to content

feat: add List-Unsubscribe headers and throttle email sends#2507

Merged
Marfuen merged 8 commits intomainfrom
feat/email-best-practices
Apr 10, 2026
Merged

feat: add List-Unsubscribe headers and throttle email sends#2507
Marfuen merged 8 commits intomainfrom
feat/email-best-practices

Conversation

@claudfuen
Copy link
Copy Markdown
Contributor

Summary

  • Add List-Unsubscribe and List-Unsubscribe-Post headers to all outbound emails (Gmail/RFC 8058 compliance)
  • Reduce email queue concurrency from 30 to 10
  • Add 1s delay between sends to spread email volume over time

Why

Part of domain reputation remediation (P0 SURBL incident). Gmail requires List-Unsubscribe for bulk senders. Email spikes from high concurrency can trigger reputation systems.

Impact

  • With concurrency 10 + 1s delay: ~1000 emails takes ~100s instead of instant blast
  • Unsubscribe header uses existing HMAC-signed URL generator

🤖 Generated with Claude Code

- 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>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
comp-framework-editor Ready Ready Preview, Comment Apr 10, 2026 7:37pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
app Skipped Skipped Apr 10, 2026 7:37pm
portal Skipped Skipped Apr 10, 2026 7:37pm

Request Review

@vercel vercel bot temporarily deployed to Preview – app April 10, 2026 18:57 Inactive
@vercel vercel bot temporarily deployed to Preview – portal April 10, 2026 18:57 Inactive
@cursor
Copy link
Copy Markdown

cursor bot commented Apr 10, 2026

PR Summary

Medium Risk
Adds a new public v1/email/unsubscribe endpoint that mutates user notification settings and changes outbound email delivery rate/headers, which could impact unsubscribe behavior and email throughput if misconfigured.

Overview
Adds RFC 8058/Gmail one-click unsubscribe support. All outbound emails now include List-Unsubscribe and List-Unsubscribe-Post headers pointing at a new POST /v1/email/unsubscribe endpoint that validates an HMAC token and, if the user exists, disables all email notification preferences.

Throttles email delivery. The send-email Trigger.dev queue concurrency is reduced from 30 → 10 and each send holds its slot for ~1s to smooth out burst volume.

Reviewed by Cursor Bugbot for commit 173863c. Bugbot is set up for automated code reviews on this repo. Configure here.

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>
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>
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>
- 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>
- 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>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 173863c. Configure here.

@Marfuen Marfuen merged commit 80db5d9 into main Apr 10, 2026
11 checks passed
@Marfuen Marfuen deleted the feat/email-best-practices branch April 10, 2026 19:46
claudfuen pushed a commit that referenced this pull request Apr 10, 2026
# [3.21.0](v3.20.2...v3.21.0) (2026-04-10)

### Bug Fixes

* **onboarding:** add initialize-organization trigger task and recover… ([#2512](#2512)) ([082501f](082501f))
* **onboarding:** disable Complete button while server action is running ([8e53a10](8e53a10))
* **onboarding:** don't delete org after session activation succeeds ([a9cb9c5](a9cb9c5))
* **onboarding:** fix org creation timeout and improve error handling ([726760d](726760d))
* **onboarding:** harden cancel action — guard completed orgs, switch before delete ([b1dec0e](b1dec0e))
* **onboarding:** hide cancel button while onboarding submission is in-flight ([887dfa9](887dfa9))
* **onboarding:** require fallback org before allowing cancel ([03452e3](03452e3))
* **onboarding:** rollback active org switch if delete fails ([9b884f0](9b884f0))
* **onboarding:** sanitize error messages shown to users ([14a35df](14a35df))
* use barrel import for email package (Trigger build fix) ([b165a18](b165a18))

### Features

* add List-Unsubscribe headers and throttle email sends ([#2507](#2507)) ([80db5d9](80db5d9))
* **onboarding:** add cancel button to abandon onboarding and return to previous org ([7d990c2](7d990c2))
@claudfuen
Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 3.21.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants