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

1 Commits

Author SHA1 Message Date
Claude (audit §9.1)
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>
2026-06-04 13:56:20 +03:00