[object Object]

← 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

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 →