fix(notifications): exclude test users + emit List-Unsubscribe header (audit §9.1) #99
Reference in New Issue
Block a user
Delete Branch "fix/audit-9-1-lifecycle-and-unsubscribe"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Lands mailAudit.md §9.1 #2 (test-user exclusion) + #3 (List-Unsubscribe header) — both touch the same Novu trigger path so they ship together.
What this PR does
1.
lifecycle-email.processor.ts— exclude test users from cronDaily 09:00 cron was firing for admin/QA/test fixtures left in prod (
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 (postal raw logs).Adds a shared
notInternal()WHERE-clause used by bothsendTrialEndingandsendWinBack. Patterns anchored to in-house domains (@sase.tr/@example.com/@otoyedekparca.co) so a legitimatetest@gmail.comuser isn't muted.2.
novu.ts— emitList-Unsubscribeheader on every triggerGmail/Yahoo Feb-2024 bulk-sender enforcement treats 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-calloverrides.email.headers:When
UNSUBSCRIBE_URL_BASEenv is set, also emits the HTTPS variant +List-Unsubscribe-Post: One-Clickfor RFC 8058 compliance (gated until the HTTPS endpoint ships in a follow-up PR).Auth flows (
email-verification,password-reset) opt out viaNO_UNSUBSCRIBE_WORKFLOWSso a privacy-proxy pre-fetching the unsub link can't consume the one-time token.Companion changes (already applied, not in this PR)
/home/s/postal/novu-patches/apply-headers-patch.sh— Novu OSS v3.15.0NodemailerProvider.createMailDatadropsoptions.headersbefore callingnodemailer.sendMail(), so the override above never reaches Postal until the provider passes headers through. Host-side patch re-injects them after each Novu redeploy. Idempotent. Verified end-to-end: DKIM signature now includeslist-unsubscribeinh=…and the header lands in Postal raw_headers.routestable now hasunsubscribe → SMTPEndpoint(dms-mailbox); docker-mailserver has aliasunsubscribe@sase.tr → destek@sase.tr. SMTP roundtrip confirmed (test mail landed in/var/mail/sase.tr/destek/new/).pct=25→pct=50, addedruf=mailto:dmarc@sase.tr; fo=1. Propagated.free → business— unblocks the 3-day delay step on the referral workflow (OSS free cap was 24h, verified infeature-tiers-constants.js). Test trigger now schedules correctly instead of failingDefer duration limit exceeded. The 178 historical referral failures stay missed (acceptable — those mails were going to never-converted users anyway); going forward day-3 invites fire as designed.Verification
lifecycle-email.processor.tsDrizzle WHERE clause rendersAND NOT (users.email IN (…)) AND NOT (users.email LIKE …) …on Postgres.overrides.email.headersconfirmed the patched provider now shipsList-Unsubscribein DKIM-signed mail (Postal raw_headers id 149 on 2026-06-04 10:53).digagainst authoritative + 1.1.1.1.{type:trigger,status:completed},{type:delay,status:delayed},{type:email,status:pending}instead of the previous all-failed.🤖 Generated with Claude Code
Pull request closed