← back to George Gmail

lib/send-preflight.js

152 lines

'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']);

const MAILBOX = /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;

function parseMailbox(value, { allowDisplayName = false, allowBlank = false } = {}) {
  const raw = String(value ?? '');
  const trimmed = raw.trim();
  if (!trimmed) return { supplied: false, valid: allowBlank, email: '' };
  if (/[\r\n\0]/.test(raw) || /[,;]/.test(raw)) return { supplied: true, valid: false, email: '' };

  let address = trimmed;
  if (trimmed.includes('<') || trimmed.includes('>')) {
    if (!allowDisplayName) return { supplied: true, valid: false, email: '' };
    const match = trimmed.match(/^([^<>]+?)\s*<([^<>]+)>$/);
    if (!match || !match[1].trim()) return { supplied: true, valid: false, email: '' };
    address = match[2].trim();
  }
  if (!MAILBOX.test(address)) return { supplied: true, valid: false, email: '' };
  return { supplied: true, valid: true, email: address.toLowerCase() };
}

function emailOf(value) {
  const parsed = parseMailbox(value, { allowDisplayName: true, allowBlank: true });
  return parsed.valid ? parsed.email : '';
}

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 = parseMailbox(payload.from, { allowDisplayName: true, allowBlank: true });
  const compliance = payload.compliance || {};
  const coreChecks = [
    { id: 'accurate_from', ok: Boolean(sender) && (!requestedFrom.supplied || (requestedFrom.valid && requestedFrom.email === sender)), detail: `blank From may inherit; any supplied From must be exactly one valid mailbox resolving 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, parseMailbox, parsedVisibleContent, hasMailingAddress, hasWorkingOptOutLink, isCanonicalOptOutMailto, DEFAULT_MAILING_ADDRESS };