← back to Sample Followup Sweep
Require visible opt-out link labels
900aa263ef44627228c7b33d2ffd8c58845155fe · 2026-08-28 16:34:24 -0700 · Steve Abrams
Files touched
M README.mdM lib/pre-send-gate.jsM test/pre-send-gate.test.jsM verification/e2e-proof.json
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: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 →