[object Object]

← back to Sample Followup Sweep

Require visible opt-out link labels

900aa263ef44627228c7b33d2ffd8c58845155fe · 2026-08-28 16:34:24 -0700 · Steve Abrams

Files touched

Diff

commit 900aa263ef44627228c7b33d2ffd8c58845155fe
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 28 16:34:24 2026 -0700

    Require visible opt-out link labels
---
 README.md                   |  8 +++++---
 lib/pre-send-gate.js        | 30 +++++++++++++++++++++---------
 test/pre-send-gate.test.js  | 10 ++++++++++
 verification/e2e-proof.json | 12 ++++++++----
 4 files changed, 44 insertions(+), 16 deletions(-)

diff --git a/README.md b/README.md
index 802de1f..8d5db58 100644
--- a/README.md
+++ b/README.md
@@ -25,9 +25,11 @@ blocks unless the sender resolves to `info@designerwallcoverings.com`, the body
 contains DW's physical postal address and a working opt-out link, and the subject
 is present without a misleading `Re:`/`Fwd:` prefix. A block performs no network
 request and returns `PRE_SEND_COMPLIANCE_BLOCKED` (HTTP 422 in the console API).
-Address text and opt-out anchors must be visibly rendered. The gate uses an HTML5
-parser and a rendered-tree walk, so comments, malformed script markup, and non-rendered
-elements (`script`, `style`, `template`, `noscript`, hidden blocks) never count.
+Address text and a meaningful opt-out anchor label must survive an HTML5 parsed-tree
+walk. Comments, malformed script markup, non-rendered elements (`script`, `style`,
+`template`, `noscript`), and elements hidden through HTML attributes or supported
+inline styles never count. This is not browser layout or a computed-stylesheet engine;
+class-based CSS hiding is outside the trusted-template gate and remains a hardening limit.
 The current approved opt-out is the `info@designerwallcoverings.com` mailto link.
 HTTPS opt-outs fail closed until a canonical DW-owned endpoint is explicitly registered.
 
diff --git a/lib/pre-send-gate.js b/lib/pre-send-gate.js
index 1545c87..65aba37 100644
--- a/lib/pre-send-gate.js
+++ b/lib/pre-send-gate.js
@@ -42,19 +42,30 @@ function isHiddenElement(node) {
 function renderedContent(body) {
   const fragment = parse5.parseFragment(String(body || ''));
   const text = [];
-  const hrefs = [];
-  function visit(node, hidden = false) {
+  const anchors = [];
+  function visit(node, hidden = false, activeAnchor = null) {
     const nowHidden = hidden || isHiddenElement(node);
-    if (!nowHidden && node.nodeName === '#text') text.push(node.value || '');
+    if (!nowHidden && node.nodeName === '#text') {
+      const value = node.value || '';
+      text.push(value);
+      if (activeAnchor) activeAnchor.text.push(value);
+    }
+    let childAnchor = activeAnchor;
     if (!nowHidden && String(node.tagName || '').toLowerCase() === 'a') {
       const href = attrs(node).href;
-      if (href) hrefs.push(href.trim());
+      if (href) {
+        childAnchor = { href: href.trim(), text: [] };
+        anchors.push(childAnchor);
+      }
     }
-    for (const child of node.childNodes || []) visit(child, nowHidden);
-    if (node.content) visit(node.content, true);
+    for (const child of node.childNodes || []) visit(child, nowHidden, childAnchor);
+    if (node.content) visit(node.content, true, null);
   }
   visit(fragment);
-  return { visibleText: text.join(' ').replace(/\s+/g, ' ').trim(), hrefs };
+  return {
+    visibleText: text.join(' ').replace(/\s+/g, ' ').trim(),
+    anchors: anchors.map(({ href, text: label }) => ({ href, visibleLabel: label.join(' ').replace(/\s+/g, ' ').trim() })),
+  };
 }
 
 function visibleTextFromHtml(body) {
@@ -62,9 +73,10 @@ function visibleTextFromHtml(body) {
 }
 
 function hasWorkingOptOutLink(body) {
-  const { hrefs } = renderedContent(body);
-  return hrefs.some((href) => {
+  const { anchors } = renderedContent(body);
+  return anchors.some(({ href, visibleLabel }) => {
     try {
+      if (!/(?:unsubscribe|opt\s*out|stop\s+receiving)/i.test(visibleLabel)) return false;
       const url = new URL(href);
       const optOutSignal = `${url.pathname} ${url.search} ${url.hash}`;
       if (!/(?:unsubscribe|opt[-_ ]?out)/i.test(optOutSignal)) return false;
diff --git a/test/pre-send-gate.test.js b/test/pre-send-gate.test.js
index f20a060..eaa9ae1 100644
--- a/test/pre-send-gate.test.js
+++ b/test/pre-send-gate.test.js
@@ -54,6 +54,16 @@ assert.throws(() => assertPreSendCompliance({ from: REQUIRED_FROM, subject: comp
 const attackerOwned = bodyWithoutLink + '<a href="https://attacker.example.org/unsubscribe">unsubscribe</a>';
 assert.throws(() => assertPreSendCompliance({ from: REQUIRED_FROM, subject: compliant.subject, body: attackerOwned }), { code: 'PRE_SEND_COMPLIANCE_BLOCKED' });
 
+for (const labelBypass of [
+  '<a href="mailto:info@designerwallcoverings.com?subject=Unsubscribe"></a>',
+  '<a href="mailto:info@designerwallcoverings.com?subject=Unsubscribe">   </a>',
+  '<a href="mailto:info@designerwallcoverings.com?subject=Unsubscribe">✉</a>',
+  '<a href="mailto:info@designerwallcoverings.com?subject=Unsubscribe"><span hidden>unsubscribe</span></a>',
+  '<a href="mailto:info@designerwallcoverings.com?subject=Unsubscribe"><span style="display:none">opt out</span></a>',
+]) {
+  assert.throws(() => assertPreSendCompliance({ from: REQUIRED_FROM, subject: compliant.subject, body: bodyWithoutLink + labelBypass }), { code: 'PRE_SEND_COMPLIANCE_BLOCKED' });
+}
+
 const validOptOut = compliant.html.match(/<a href="mailto:[\s\S]*?<\/a>/)[0];
 for (const parserBypass of [
   '<div style="display&#58;none">15442 Ventura Blvd. #102, Sherman Oaks, CA 91403</div>' + validOptOut,
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index 0eeb231..cb8dae9 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -1,9 +1,9 @@
 {
-  "intent": "Centralize fail-closed compliance enforcement and require visibly rendered, DW-owned compliance content",
+  "intent": "Centralize fail-closed compliance enforcement and require parsed-tree-visible, meaningfully labeled DW-owned compliance content",
   "risk_tier": "R3",
   "environment": "local repository only; injected HTTP spy; no George/Gmail/FileMaker/production network calls",
-  "build_identity": "Cycle 4 working tree after a3c2bbb; final commit recorded after verification",
-  "timestamp": "2026-08-28T16:30:24-07:00",
+  "build_identity": "Cycle 5 working tree based on 1f59efb; final commit recorded after verification",
+  "timestamp": "2026-08-28T16:34:16-07:00",
   "ticket": "TK-10940-centralize-sample-follow-up-pre-send-com",
   "baseline": "Cycle 1 commit c74ebf7 had an ungated /api/drafts call, an ungated gmail_create_draft artifact, and accepted hidden/comment/plain-text/reserved-domain opt-out tokens",
   "commands": [
@@ -22,7 +22,7 @@
     {
       "boundary": "compliance parser",
       "verdict": "PASS",
-      "evidence": "parse5 HTML5 tree parsing plus test/pre-send-gate.test.js reject comments, unclosed script, entity-encoded/CSS-comment display:none, script/template/hidden address text, script-only links, plain-text mailto tokens, invalid.example, and attacker.example.org while accepting lib/compose.js and reconciled-footer output"
+      "evidence": "parse5 HTML5 tree parsing plus test/pre-send-gate.test.js reject comments, unclosed script, entity-encoded/CSS-comment display:none, script/template/hidden address text, script-only links, empty/whitespace/icon-only/hidden-only opt-out labels, plain-text mailto tokens, invalid.example, and attacker.example.org while accepting lib/compose.js and reconciled-footer output"
     },
     {
       "boundary": "dependency integrity",
@@ -65,8 +65,12 @@
     "postal address present only in script/template",
     "postal address and opt-out present in malformed unclosed script",
     "opt-out link present only in script",
+    "approved opt-out destination with empty label",
+    "approved opt-out destination with whitespace or icon-only label",
+    "approved opt-out destination with label only in hidden descendant",
     "blocked transport makes zero requests"
   ],
+  "known_limit": "parse5 provides an HTML5 parsed tree, not browser layout or computed stylesheets; class-based CSS hiding is excluded by the trusted-template boundary and documented as a hardening limit",
   "cleanup": "Isolated local server stopped with Ctrl-C; no external or persistent test state created; repository verification artifact intentionally retained",
   "verdict": "PASS"
 }

← 1f59efb Parse compliance HTML before validating visibility  ·  back to Sample Followup Sweep  ·  Reject injected mailto opt-out headers f171037 →