[object Object]

← back to George Gmail

Add staged outbound compliance preflight

ed923bcd01f2e1bb0d765f8bf0b2a948e5df1961 · 2026-08-28 20:07:51 -0700 · Steve Abrams

Files touched

Diff

commit ed923bcd01f2e1bb0d765f8bf0b2a948e5df1961
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 28 20:07:51 2026 -0700

    Add staged outbound compliance preflight
---
 lib/send-preflight.js       | 131 ++++++++++++++++++++++++++++++++++++++++++++
 server.js                   |  26 ++++++++-
 test/send-preflight.test.js |  77 ++++++++++++++++++++++++++
 3 files changed, 233 insertions(+), 1 deletion(-)

diff --git a/lib/send-preflight.js b/lib/send-preflight.js
new file mode 100644
index 0000000..7cd53db
--- /dev/null
+++ b/lib/send-preflight.js
@@ -0,0 +1,131 @@
+'use strict';
+
+const parse5 = require('parse5');
+
+const DEFAULT_MAILING_ADDRESS = '15442 Ventura Blvd. #102, Sherman Oaks, CA 91403';
+const MISLEADING_SUBJECT = /\b(?:re|fw|fwd)\s*:/i;
+const NON_RENDERED_ELEMENTS = new Set(['script', 'style', 'template', 'noscript', 'head', 'svg', 'canvas']);
+
+function emailOf(value) {
+  return String(value || '').match(/[\w.+-]+@[\w.-]+\.\w+/)?.[0]?.toLowerCase() || '';
+}
+
+function attrs(node) {
+  return Object.fromEntries((node.attrs || []).map(({ name, value }) => [name.toLowerCase(), value]));
+}
+
+function isHiddenElement(node) {
+  if (!node.tagName) return false;
+  const tag = node.tagName.toLowerCase();
+  if (NON_RENDERED_ELEMENTS.has(tag)) return true;
+  const attributes = attrs(node);
+  if (Object.hasOwn(attributes, 'hidden')) return true;
+  if (String(attributes['aria-hidden'] || '').trim().toLowerCase() === 'true') return true;
+  if (tag === 'input' && String(attributes.type || '').trim().toLowerCase() === 'hidden') return true;
+  const style = String(attributes.style || '')
+    .replace(/\/\*[\s\S]*?\*\//g, '')
+    .replace(/\s+/g, '')
+    .toLowerCase();
+  return /(?:^|;)display:none(?:!important)?(?:;|$)/.test(style)
+    || /(?:^|;)visibility:hidden(?:!important)?(?:;|$)/.test(style);
+}
+
+function parsedVisibleContent(body) {
+  const fragment = parse5.parseFragment(String(body || ''));
+  const text = [];
+  const anchors = [];
+  function visit(node, hidden = false, activeAnchor = null) {
+    const nowHidden = hidden || isHiddenElement(node);
+    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) {
+        childAnchor = { href: href.trim(), text: [] };
+        anchors.push(childAnchor);
+      }
+    }
+    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(),
+    anchors: anchors.map(({ href, text: label }) => ({ href, visibleLabel: label.join(' ').replace(/\s+/g, ' ').trim() })),
+  };
+}
+
+function hasMailingAddress(body, mailingAddress = DEFAULT_MAILING_ADDRESS) {
+  const wanted = String(mailingAddress || '').replace(/[^a-z0-9]/gi, '').toLowerCase();
+  const actual = parsedVisibleContent(body).visibleText.replace(/[^a-z0-9]/gi, '').toLowerCase();
+  return Boolean(wanted) && actual.includes(wanted);
+}
+
+function isCanonicalOptOutMailto(url, expectedFrom) {
+  if (url.protocol !== 'mailto:' || url.hash) return false;
+  let recipient;
+  try { recipient = decodeURIComponent(url.pathname); } catch { return false; }
+  if (recipient.toLowerCase() !== emailOf(expectedFrom)) return false;
+  const entries = [...url.searchParams.entries()];
+  if (entries.length !== 1 || entries[0][0] !== 'subject') return false;
+  const subject = entries[0][1];
+  return Boolean(subject) && !/[\r\n\0]/.test(subject) && /(?:unsubscribe|opt\s*out|stop\s+receiving)/i.test(subject);
+}
+
+function hasWorkingOptOutLink(body, expectedFrom) {
+  const { anchors } = parsedVisibleContent(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);
+      return isCanonicalOptOutMailto(url, expectedFrom);
+    } catch {
+      return false;
+    }
+  });
+}
+
+function classifySend(payload, { externalRecipients = [], endpointEvaluation = false } = {}) {
+  if (!externalRecipients.length) return { messageClass: 'internal', basis: 'server-derived-internal', enforcement: 'exempt' };
+  const explicit = String(payload.message_class || '').trim().toLowerCase();
+  if (explicit === 'commercial' || payload.compliance_required === true) {
+    return { messageClass: 'commercial', basis: explicit === 'commercial' ? 'caller-explicit-commercial' : 'caller-compliance-required', enforcement: 'block' };
+  }
+  if (explicit === 'transactional' || explicit === 'reply') {
+    return { messageClass: explicit, basis: `caller-explicit-${explicit}-unverified`, enforcement: 'report-only' };
+  }
+  if (endpointEvaluation) return { messageClass: 'commercial', basis: 'preflight-endpoint-conservative-default', enforcement: 'block' };
+  return { messageClass: 'unclassified', basis: explicit ? 'caller-unknown-class' : 'legacy-external-unclassified', enforcement: 'report-only' };
+}
+
+function sendPreflight(payload, options = {}) {
+  const { expectedFrom = '', externalRecipients = [], endpointEvaluation = false, mailingAddress = DEFAULT_MAILING_ADDRESS } = options;
+  const classification = classifySend(payload, { externalRecipients, endpointEvaluation });
+  if (classification.messageClass === 'internal') return { ok: true, shouldBlock: false, ...classification, checks: [], failed: [], external: [] };
+
+  const sender = emailOf(expectedFrom);
+  const requestedFrom = emailOf(payload.from);
+  const compliance = payload.compliance || {};
+  const coreChecks = [
+    { id: 'accurate_from', ok: Boolean(sender) && (!requestedFrom || requestedFrom === sender), detail: `sender must resolve to ${sender || '(configured account)'}` },
+    { id: 'honest_subject', ok: Boolean(String(payload.subject || '').trim()) && !MISLEADING_SUBJECT.test(String(payload.subject || '')), detail: 'subject is required and must not imply a reply or forward without server-verified context' },
+  ];
+  const commercialChecks = [
+    { id: 'physical_address', ok: hasMailingAddress(payload.body, mailingAddress), detail: 'body must contain the configured physical postal address' },
+    { id: 'working_opt_out', ok: hasWorkingOptOutLink(payload.body, sender), detail: 'body must contain a visible, valid opt-out anchor' },
+    { id: 'dnc_scrubbed', ok: compliance.dnc_scrubbed === true, detail: 'compliance.dnc_scrubbed must be true' },
+    { id: 'age_gate_excluded', ok: compliance.age_gated_excluded === true, detail: 'compliance.age_gated_excluded must be true' },
+  ];
+  const checks = classification.messageClass === 'transactional' || classification.messageClass === 'reply'
+    ? coreChecks
+    : [...coreChecks, ...commercialChecks];
+  const failed = checks.filter((check) => !check.ok);
+  const ok = failed.length === 0;
+  return { ok, shouldBlock: classification.enforcement === 'block' && !ok, ...classification, checks, failed, external: externalRecipients };
+}
+
+module.exports = { classifySend, sendPreflight, parsedVisibleContent, hasMailingAddress, hasWorkingOptOutLink, isCanonicalOptOutMailto, DEFAULT_MAILING_ADDRESS };
diff --git a/server.js b/server.js
index e8ecb97..50f8a36 100644
--- a/server.js
+++ b/server.js
@@ -9,6 +9,7 @@ const path = require('path');
 const fs = require('fs');
 const { google } = require('googleapis');
 const nodemailer = require('nodemailer');
+const { sendPreflight } = require('./lib/send-preflight');
 
 const PORT = process.env.PORT || 9850;
 const AGENT_NAME = 'George';
@@ -1194,13 +1195,28 @@ function externalSendGuard(req) {
     + 'External recipients: ' + external.join(', ') };
 }
 
+function runSendPreflight(req, payload, expectedFrom, externalRecipients) {
+  const result = sendPreflight(payload, { expectedFrom, externalRecipients });
+  const action = result.shouldBlock ? 'send-PREFLIGHT-BLOCKED' : (!result.ok && result.enforcement === 'report-only' ? 'send-PREFLIGHT-REPORT' : null);
+  if (action) try { audit(req.body?.account || '?', action, { to: payload.to, messageClass: result.messageClass, basis: result.basis, failed: result.failed.map(c => c.id) }); } catch {}
+  return result;
+}
+
+app.post('/api/send-preflight', (req, res) => {
+  const account = resolveAccount(req);
+  if (!account.gmail) return res.status(400).json({ error: `unknown account: ${account.key}` });
+  const external = _recips(req.body?.to, req.body?.cc, req.body?.bcc).filter(a => !_isInternal(a));
+  const result = sendPreflight(req.body || {}, { expectedFrom: account.label, externalRecipients: external, endpointEvaluation: true });
+  return res.status(result.ok ? 200 : 422).json(result);
+});
+
 app.post('/api/send', async (req, res) => {
   try {
     const { from, to, subject, body, cc, bcc, threadId: bodyThreadId, inReplyTo: bodyInReplyTo, replyToMessageId, references } = req.body;
     if (!to || !body) return res.status(400).json({ error: 'to and body required' });
     { const _g = externalSendGuard(req); if (!_g.ok) { try { audit(req.body.account || '?', 'send-BLOCKED', { to, external: _g.external, reason: _g.reason }); } catch (e) {} return res.status(403).json({ error: _g.reason, blocked: true, external: _g.external }); } }
 
-    const { gmail: g, key: account } = resolveAccount(req);
+    const { gmail: g, key: account, label: accountLabel } = resolveAccount(req);
     if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
 
     let threadId = bodyThreadId || null;
@@ -1218,6 +1234,10 @@ app.post('/api/send', async (req, res) => {
     }
     if (!finalSubject) return res.status(400).json({ error: 'subject required (or pass replyToMessageId)' });
 
+    const external = _recips(to, cc, bcc).filter(a => !_isInternal(a));
+    const preflight = runSendPreflight(req, { ...req.body, subject: finalSubject }, accountLabel, external);
+    if (preflight.shouldBlock) return res.status(422).json({ error: 'send compliance preflight failed', blocked: true, messageClass: preflight.messageClass, basis: preflight.basis, failed: preflight.failed, checks: preflight.checks });
+
     const source = inferSource(req.body, req);
     const taggedBody = withSourceFooter(body, source);
     const encoded = buildRawMessage({ from, to, cc, bcc, subject: finalSubject, body: taggedBody, inReplyTo, references: refsArr });
@@ -1369,6 +1389,10 @@ app.post('/api/send-with-attachment', async (req, res) => {
     if (!authClient || !gmailClient) {
       return res.status(500).json({ error: `account not configured: ${acctKey}` });
     }
+    const accountInfo = resolveAccount(req);
+    const external = _recips(to, cc, bcc).filter(a => !_isInternal(a));
+    const preflight = runSendPreflight(req, req.body, accountInfo.label, external);
+    if (preflight.shouldBlock) return res.status(422).json({ error: 'send compliance preflight failed', blocked: true, messageClass: preflight.messageClass, basis: preflight.basis, failed: preflight.failed, checks: preflight.checks });
 
     // Build MIME with nodemailer (handles UTF-8 subjects, multipart, base64 chunking correctly)
     const profile = await gmailClient.users.getProfile({ userId: 'me' });
diff --git a/test/send-preflight.test.js b/test/send-preflight.test.js
new file mode 100644
index 0000000..4f2bb75
--- /dev/null
+++ b/test/send-preflight.test.js
@@ -0,0 +1,77 @@
+'use strict';
+
+const assert = require('node:assert/strict');
+const { classifySend, sendPreflight } = require('../lib/send-preflight');
+
+const external = ['customer@real-domain.test'];
+const options = { expectedFrom: 'info@designerwallcoverings.com', externalRecipients: external };
+const compliant = {
+  account: 'info', from: 'Designer Wallcoverings <info@designerwallcoverings.com>', to: external[0], message_class: 'commercial',
+  subject: 'Fall wallcovering edit',
+  body: '<p>Designer Wallcoverings<br>15442 Ventura Blvd. #102, Sherman Oaks, CA 91403</p><a href="mailto:info@designerwallcoverings.com?subject=unsubscribe">Unsubscribe</a>',
+  compliance: { dnc_scrubbed: true, age_gated_excluded: true },
+};
+
+assert.deepEqual(classifySend(compliant, options), { messageClass: 'commercial', basis: 'caller-explicit-commercial', enforcement: 'block' });
+assert.equal(sendPreflight(compliant, options).ok, true);
+assert.equal(sendPreflight(compliant, options).shouldBlock, false);
+
+const missingControls = { account: 'info', to: external[0], subject: 'Hello', body: '<p>Hello</p>' };
+const legacy = sendPreflight(missingControls, options);
+assert.equal(legacy.messageClass, 'unclassified');
+assert.equal(legacy.enforcement, 'report-only');
+assert.equal(legacy.ok, false);
+assert.equal(legacy.shouldBlock, false);
+
+const explicitCommercial = sendPreflight({ ...missingControls, message_class: 'commercial' }, options);
+assert.equal(explicitCommercial.enforcement, 'block');
+assert.equal(explicitCommercial.shouldBlock, true);
+
+const endpointDefault = sendPreflight(missingControls, { ...options, endpointEvaluation: true });
+assert.equal(endpointDefault.messageClass, 'commercial');
+assert.equal(endpointDefault.basis, 'preflight-endpoint-conservative-default');
+assert.equal(endpointDefault.enforcement, 'block');
+assert.equal(endpointDefault.shouldBlock, true);
+
+const transactional = sendPreflight({ ...missingControls, message_class: 'transactional' }, options);
+assert.equal(transactional.enforcement, 'report-only');
+assert.equal(transactional.shouldBlock, false);
+
+const forgedReply = sendPreflight({ ...compliant, subject: 'Re: prior note', threadId: 'caller-controlled' }, options);
+assert.ok(forgedReply.failed.some((check) => check.id === 'honest_subject'));
+assert.equal(forgedReply.shouldBlock, true);
+
+const internal = sendPreflight({ ...missingControls, message_class: 'commercial' }, { expectedFrom: options.expectedFrom, externalRecipients: [] });
+assert.equal(internal.messageClass, 'internal');
+assert.equal(internal.basis, 'server-derived-internal');
+assert.equal(internal.enforcement, 'exempt');
+
+for (const fakeOptOut of [
+  '<!-- <a href="mailto:info@designerwallcoverings.com?subject=unsubscribe">unsubscribe</a> -->',
+  'mailto:info@designerwallcoverings.com?subject=unsubscribe',
+  '<a href="https://invalid.example/unsubscribe">unsubscribe</a>',
+]) {
+  const result = sendPreflight({ ...compliant, body: '<p>15442 Ventura Blvd. #102, Sherman Oaks, CA 91403</p>' + fakeOptOut }, options);
+  assert.ok(result.failed.some((check) => check.id === 'working_opt_out'));
+}
+
+for (const parserBypass of [
+  '<div hidden>15442 Ventura Blvd. #102, Sherman Oaks, CA 91403</div><a href="mailto:info@designerwallcoverings.com?subject=unsubscribe">Unsubscribe</a>',
+  '<div style="display&#58;none">15442 Ventura Blvd. #102, Sherman Oaks, CA 91403</div><a href="mailto:info@designerwallcoverings.com?subject=unsubscribe">Unsubscribe</a>',
+  '<script>15442 Ventura Blvd. #102, Sherman Oaks, CA 91403 <a href="mailto:info@designerwallcoverings.com?subject=unsubscribe">Unsubscribe</a>',
+]) {
+  const result = sendPreflight({ ...compliant, body: parserBypass }, options);
+  assert.ok(result.failed.some((check) => check.id === 'physical_address' || check.id === 'working_opt_out'));
+}
+
+for (const optOutBypass of [
+  '<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&amp;bcc=attacker@example.org">Unsubscribe</a>',
+  '<a href="mailto:info@designerwallcoverings.com,attacker@example.org?subject=unsubscribe">Unsubscribe</a>',
+]) {
+  const result = sendPreflight({ ...compliant, body: '<p>15442 Ventura Blvd. #102, Sherman Oaks, CA 91403</p>' + optOutBypass }, options);
+  assert.ok(result.failed.some((check) => check.id === 'working_opt_out'));
+}
+
+console.log('George send preflight classification: PASS');

← 615c560 auto-data-snapshot: 2026-08-28T20:07:34 (3 data files) — pac  ·  back to George Gmail  ·  Reject malformed explicit From identities 9cfb00a →