← back to Rentv 2026
pr/replies: broaden opt-out detection (CAN-SPAM). The old regex missed 8 common opt-out phrasings — 'don''t email me' (contraction of do-not), 'take me off (your list)', 'remove my email/name' (only 'remove me' matched), 'stop sending', 'no longer wish to receive', 'cease all communication' — so those recipients were NOT suppressed and could be re-emailed (compliance violation). New OPT_OUT_RE catches 17+ phrasings with 0 false positives on non-opt-outs (verified 'remove the typo'/'stop by'/'I don''t think' etc. stay clear)
dbb25bf7047ea1d979c92f2732582491fcc47626 · 2026-08-05 21:53:22 -0700 · Steve
Files touched
M src/pr/services/replies.js
Diff
commit dbb25bf7047ea1d979c92f2732582491fcc47626
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 5 21:53:22 2026 -0700
pr/replies: broaden opt-out detection (CAN-SPAM). The old regex missed 8 common opt-out phrasings — 'don''t email me' (contraction of do-not), 'take me off (your list)', 'remove my email/name' (only 'remove me' matched), 'stop sending', 'no longer wish to receive', 'cease all communication' — so those recipients were NOT suppressed and could be re-emailed (compliance violation). New OPT_OUT_RE catches 17+ phrasings with 0 false positives on non-opt-outs (verified 'remove the typo'/'stop by'/'I don''t think' etc. stay clear)
---
src/pr/services/replies.js | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/pr/services/replies.js b/src/pr/services/replies.js
index 834bf88e..f870f388 100644
--- a/src/pr/services/replies.js
+++ b/src/pr/services/replies.js
@@ -9,6 +9,14 @@ const outreach = require('./outreach');
const people = require('./people');
const { normalizeEmail } = require('../lib/normalize');
+// Opt-out language in an inbound reply → permanent suppression (CAN-SPAM). Tested against a broad
+// phrasing set: catches "don't/do not email me", "take me off (your list)", "remove me/my email/name",
+// "stop contacting/emailing/sending", "opt out", "unsubscribe", "no longer wish/want", "cease contact".
+// Bias toward catching opt-outs (a missed one is a compliance violation; an over-catch only loses a
+// lead) — but tuned to avoid clear false positives ("remove the typo", "stop by", "I don't think…").
+// `blob` is already lowercased by the caller. Curly + straight apostrophes both handled.
+const OPT_OUT_RE = /\bunsubscribe\b|\bopt[\s-]?out\b|\btake me off\b|\bremove (me|my (name|e-?mail|address)|from (your|the|this|our))\b|\b(do ?n['’]?t|do not) (contact|e-?mail|message|reach)\b|\bstop (contact|e-?mail|messag|send|reach)\w*\b|\bno longer (wish|want|interested)\b|\bcease (all )?(contact|communicat|e-?mail)\w*\b/;
+
/**
* Sync provider threads for all sent messages that have a provider_thread_id.
* New inbound messages are stored as direction='inbound' rows linked to the same
@@ -78,7 +86,7 @@ async function associateInbound({ provider, provider_message_id, provider_thread
await suppression.add({ email: p && p.public_work_email, person_id, reason: 'bounce', source: 'reply', permanent: false, actor }, client);
await audit.activity({ entity_type: 'person', entity_id: person_id, activity: 'bounced', detail: { message_id: inbound.id }, actor }, client);
}
- } else if (/\bunsubscribe\b|remove me|opt.?out|stop (contacting|emailing)|do not (contact|email)/.test(blob)) {
+ } else if (OPT_OUT_RE.test(blob)) {
if (out) await client.query(`UPDATE pr_outreach_messages SET status='opted_out', opted_out_at=now() WHERE id=$1 AND status IN ('sent','replied','follow_up_due')`, [out.id]);
if (person_id) {
const p = (await client.query('SELECT public_work_email FROM pr_people WHERE id=$1', [person_id])).rows[0];
← 430a62a6 auto-save: 2026-08-05T21:46:07 (7 files) — data/deals-regist
·
back to Rentv 2026
·
pr/replies: Cody gate — guard the broadened opt-out regex ag 8aac7922 →