feat(notifications): TR-only templates + name canonicalisation + MTA-STS + 2048-bit DKIM + unsubscribe (audit §9.3) #101
Reference in New Issue
Block a user
Delete Branch "fix/audit-9-3-tr-only-mta-sts-names"
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 the §9.3 compliance + brand tier of
postal/mailAudit.mdas one PR.What
Six audit items, all in the notifications surface or one hop away:
normalizeName()in @sase/shared + better-authuser.create.beforehook +scripts/backfill-user-names.tssuggestEmailFix()in @sase/shared + inline UI hint on register formnovu-*.htmltemplates + Novu Mongo workflow defs +NovuRecipient.localeremoveddomains.dkim_identifier_stringswapped to2k260604+ new TXT record at Cloudflaremta-sts.sase.tr/.well-known/mta-sts.txt+ 2 DNS TXT recordsemail_preferencestable +UnsubscribeController+ token sign/verify innovu.ts+ pre-flight skip inNovuServiceandlifecycle-email.processorEN i18n decision: user picked strip-EN. All 10 templates collapsed to TR-only (avg ~30 % smaller). EN can come back as a follow-up if/when there is an EN market, with a
users.localecolumn + UI switcher.Verification snapshot
raw_headersID 157 (post-rotation):s=postal-2k260604+ 256-byteb=…signature.dig +short TXT _mta-sts.sase.tr @1.1.1.1⇒ liveid=20260604111347.curl https://mta-sts.sase.tr/.well-known/mta-sts.txt⇒ 200 with the expected policy.Sibling changes already applied (NOT in this PR)
mailtrackredeployed withmta-sts.sase.trcustom domain.postal-2k260604._domainkey.sase.trTXT record live at Cloudflare._mta-sts.sase.tr+_smtp._tls.sase.trTXT records live at Cloudflare.messagetemplatesupdated for all 10 workflows in both Dev + Prod envs.UNSUBSCRIBE_SECRET(32-byte hex, distinct per env) +UNSUBSCRIBE_URL_BASE(=https://(dev.)sase.tr/api/email/unsubscribe).Deploy gotchas
pnpm db:generateto regenerate the drizzle snapshot for migration 0011 (added the journal entry manually because no drizzle-kit on this box).postal/novu-patches/apply-headers-patch.shmust be re-run after every Novu container redeploy — Novu OSS v3.15.0NodemailerProvider.createMailDatadropsoptions.headersbefore callingnodemailer.sendMail()(see audit §9.1 #3)./dashboard/settings/notificationsis deliberately NOT in this PR — the one-click endpoint alone is what Gmail/Yahoo bulk-sender enforcement requires. UI is a follow-up.lifecycle-email.processor.tsandnovu.tstoo — if it lands first, this PR's lifecycle filter will rebase cleanly on top of the test-user exclusion clause.🤖 Generated with Claude Code
Lands the user-facing half of the unsubscribe preferences work. The one-click endpoint already shipped in this PR's main commit; this adds the proactive self-service path at /dashboard/settings?tab=notifications so users don't have to wait for a mail to land before tuning their preferences. Backend ------- New EmailPreferencesController at /api/email/preferences: GET → returns one row per OPTIONAL_WORKFLOWS entry, each with current optedOut boolean (false when no DB row exists). POST → body {workflow, optedOut} flips the row; source='settings_page' captured for the audit trail. Auth+payment workflows are deliberately not exposed — the server's OPTIONAL_WORKFLOWS set stays the single source of truth. Frontend -------- Adds a 'notifications' tab to /dashboard/settings (between 'preferences' and 'security'). One toggle row per optional workflow with TR copy that explains what each mail is for. Optimistic update — switch flips instantly and reverts on failure; PostHog event captures accept/reject. Static footer note clarifies that auth + payment mail keeps coming regardless of the switches above (so users don't think they've unsubscribed from password-reset). i18n ---- Added settings.tabs.notifications + settings.notifications.{title, description} to both tr.json and en.json. Body copy is hard-coded TR (matches audit §9.3 #11 TR-only decision). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Pull request closed