fix(notifications): exclude test users + emit List-Unsubscribe header (audit §9.1) #99
Closed
root
wants to merge 1 commits from
fix/audit-9-1-lifecycle-and-unsubscribe into main
pull from: fix/audit-9-1-lifecycle-and-unsubscribe
merge into: root:main
root:main
root:dev
root:fix/lifecycle-founder-uuid
root:revert/build-cache-web
root:fix/build-cache-web
root:feat/fiat-birlesik-canonical
root:feat/fiat-eper-simplify
root:feat/fiat-eper-default
root:fix/activate-userbrands-conflict
root:feature/vin-ocr
root:fix/vinpin-query-log-accounting
root:feature/widget-canonical
root:feature/public-canonical-categories
root:feature/pcat-jwt-resilience
root:feature/public-api
root:feature/discount-codes
root:fix/backfill-inprocess-scan
root:fix/backfill-lifo-fast-lane
root:promote-expert-program
root:blog-cms-assets
root:blog-directus-main
root:legacy
1 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6fbbd753d9 |
fix(notifications): exclude test users + emit List-Unsubscribe header
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
mailAudit.md §9.1 items #2 and #3 — landing two related changes in one PR because they share the same audit findings and touch overlapping code. Test-user exclusion (lifecycle-email.processor.ts) ================================================== The daily 09:00 cron was firing for internal/QA accounts left in the prod DB (test1@, testyeni@, dialogtest@, pending-test-*@example.com, stripe-test-*@otoyedekparca.co, admin@, adm1in@). On 2026-06-04 09:00 the last 5 trial-ending mails went to test users alone. Adds a shared notInternal() WHERE-clause used by both sendTrialEnding + sendWinBack so the cron sees the same exclusion in both cohorts. Patterns are anchored to in-house domains (@sase.tr / @example.com / @otoyedekparca.co) so a legitimate `test@gmail.com` user isn't accidentally muted. List-Unsubscribe header (novu.ts) ================================= Gmail/Yahoo Feb-2024 bulk-sender rules treat the absence of this header as a spam signal; we ship none today (verified by reading raw_headers in postal-server-1.raw-2026-06-04). triggerNovu() now attaches a per-call `overrides.email.headers` containing: List-Unsubscribe: <mailto:unsubscribe@sase.tr?subject=unsubscribe:WORKFLOW> (+ `<https://…>` and `List-Unsubscribe-Post: One-Click` when UNSUBSCRIBE_URL_BASE env is set — gated until the HTTPS endpoint ships) Auth flows (email-verification, password-reset) opt out via NO_UNSUBSCRIBE_WORKFLOWS so a privacy-proxy pre-fetching the unsub link can't consume the one-time token. Companion runtime patch (NOT part of this PR — lives at postal/novu-patches/apply-headers-patch.sh on the host): Novu OSS v3.15.0 NodemailerProvider.createMailData drops options.headers before calling nodemailer.sendMail(), so the override above never reaches Postal until the provider passes headers through. The host-side patch re-injects them after each Novu redeploy. Already applied — verified end-to-end (DKIM signature now includes list-unsubscribe in h=… and the header lands in Postal raw_headers). Sibling out-of-PR changes done today (in mailAudit.md §9.1): - DMARC pct=25 → pct=50, +ruf=mailto:dmarc@sase.tr, +fo=1 (Cloudflare) - Novu org tier free → business (unblocks 3-day delay step on referral workflow; OSS free cap was 24h — verified by reading feature-tiers-constants.js in the Novu container; test trigger now schedules correctly instead of failing 'Defer duration limit exceeded') Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |