Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Lands the §9.4 operability tier of postal/mailAudit.md as one PR on top of the §9.3 stack (PR #101). Seven items, all independent of each other but sharing the same notifications surface. #15 Postal send_limit fuse — set per-hour cap (already applied LIVE in DB: servers.send_limit = 500). A runaway loop now hits Postal's own throttle long before flooding recipient ISPs into a rate-limit penalty. #16 noreply@sase.tr decommission — change default fromAddress in both email.service.ts and config/configuration.ts to destek@sase.tr. `noreply@` had no inbound route so user replies bounced; `destek@` lands in the SnappyMail destek inbox. Overridable via POSTAL_FROM_ADDRESS env per workflow that genuinely shouldn't accept replies. #21 Welcome CTA fallback — flip the {{else}} branch in novu-welcome.html from https://sase.tr to https://sase.tr/dashboard (the actual onboarding entry, not the marketing page). Already pushed live to Novu Mongo too. #17 Open-pixel embed — new buildTrackPixelUrl() in novu.ts; injected trackPixel payload into welcome/trial-ending/win-back/referral×3/ payment×2 NovuService methods + lifecycle-email.processor. Auth flows (email-verification, password-reset) deliberately skip the pixel. Templates updated with {{#if trackPixel}}<img ...>{{/if}} just before the footer; 8 templates touched, 2 (auth) skipped. Novu Mongo updated. #18 Signed-URL exp / replay-resistance — track.sase.tr Worker /c endpoint now expects `e=<unix-ms>` + `s=HMAC(MID|TARGET|EXP)`. Expired signatures return 410. Legacy signatures (no `e=`) still accepted while in-flight mail with old links drains; remove that branch ~30 days post-deploy. buildTrackedUrl() now mints exp=now+30d. #19 D1 retention cron — Cloudflare Cron Trigger added to mailtrack worker (`17 4 * * *` UTC, after Europe/Istanbul cron settles). `scheduled` handler DELETEs events older than RETENTION_DAYS (default 90). Both code and the cron schedule are LIVE on the production worker. #20 Lifecycle sent-flag idempotency — new `lifecycle_email_sent` table (migration 0012) keyed (user_id, workflow). Replaces the 1-day endDate window's at-most-once trick that lost cohorts on skipped days. Cron now LEFT JOINs and writes the row immediately after each successful trigger. Historical seed in scripts/backfill-lifecycle-sent.sql (19 trial-ending + 5 win-back users — generated from postal-server-1.messages) so the first post-deploy cron doesn't re-send to users we already mailed. ## Live infrastructure (deploy-independent) - Postal MariaDB: `UPDATE servers SET send_limit = 500`. - Cloudflare Worker mailtrack redeployed with new /c logic + scheduled handler. - Cloudflare Worker: cron `17 4 * * *` registered on production env. - Cloudflare Worker: RETENTION_DAYS=90 plain_text binding. - Novu Mongo: 16 messagetemplates updated with pixel + Welcome /dashboard. ## Companion deploy steps post-merge 1. `pnpm db:generate` to refresh drizzle snapshots for 0011 + 0012. 2. Run `scripts/backfill-lifecycle-sent.sql` against prod + dev BEFORE the first cron tick post-deploy. 3. Apply host-side novu-patches/apply-headers-patch.sh again if Novu container rolled (idempotent). ## Verification curl /c?…e=<future>… ⇒ 302 new-style signature accepted curl /c?…(no e)… ⇒ 302 legacy signature still accepted (drain) curl /c?…e=<past>… ⇒ 410 expired signature rejected curl /c?…s=bad… ⇒ 403 bad signature rejected CF API schedules ⇒ `17 4 * * *` live on mailtrack worker. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
27 lines
3.4 KiB
SQL
27 lines
3.4 KiB
SQL
-- Historical seed (generated 2026-06-04 from postal-server-1)
|
|
-- Run AFTER migration 0012, BEFORE tomorrow's lifecycle cron.
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('a988f621-f3b4-4086-aee8-38583bc0aaca', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('107ed4f1-2bb1-4730-9f71-1a33c4d75504', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('664cd1fa-4ce3-4f2b-9184-bbaca0f8b020', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('d03d711a-6235-4f45-ae80-65bf0b815ce0', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('844a47b8-640d-42a3-910c-68a23e14a8f6', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('6e4c13b2-1e35-4e9c-a737-55dd6075b130', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('ee0b8dfa-ae02-41b8-9886-3247a0a2c471', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('08baff23-9754-4762-a665-ce8251b81333', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('9d966aa6-8033-4c67-98bb-e84c00c68157', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('9faea025-6ed2-4897-a503-7d5697d6ab7e', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('2f70a2b6-e3df-4419-b688-7488a1a69515', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('9112fa82-61ee-4b89-b815-e50abb78544f', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('9c41b4c4-3b67-401d-9563-ea9d97763513', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('16ed8cb3-9bdf-4918-a1c0-253008d112e0', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('0e69e85f-3cfb-4d2e-abec-de086bd6e592', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('fd469c94-377a-4148-a280-a1cf361b7b6d', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('419b51f1-14c8-4ef0-a221-f6ffff93f6b1', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('fa2bd88e-c256-4a7f-8ee6-51e06a785c7a', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('5b1eda7f-e7bd-4238-9964-9d3707eb2e39', 'trial-ending') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('e457416c-f406-47ed-8e23-4a832c2fc965', 'win-back') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('89607516-7061-432e-bd40-971ccbf4e02b', 'win-back') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('15390f23-6d87-4be1-a644-df7602ee58bc', 'win-back') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('21191b23-b327-4239-a0fc-18974b97e35c', 'win-back') ON CONFLICT DO NOTHING;
|
|
INSERT INTO lifecycle_email_sent (user_id, workflow) VALUES ('83d9628a-faa5-40d3-a75c-8ae12629ecdf', 'win-back') ON CONFLICT DO NOTHING;
|