← back to Designer Wallcoverings

mailers/send-test-via-george.js

100 lines

#!/usr/bin/env node
/*
 * send-test-via-george.js — send a CLEAN test preview of a mailer to a single
 * inbox via the LIVE LOCAL George (http://127.0.0.1:9850, basic-auth admin:"").
 *
 * WHY THIS EXISTS
 * ---------------
 * The Constant Contact source mailer (e.g. china-seas-launch.html) carries CC
 * footer tokens — {{ unsubscribe }} and {{update_preferences_url}} — that CC's
 * own compliance footer handles on a real campaign send. George is a plain
 * Gmail relay with NO merge engine, so on a self-test those tokens render
 * literally and the preview looks broken.
 *
 * This helper substitutes those tokens with working fallback links AT SEND TIME
 * ONLY. It NEVER writes the source file, so the CC-bound HTML stays byte-for-byte
 * identical and the real Constant Contact campaign is completely unaffected.
 *
 * USAGE
 *   node send-test-via-george.js <mailer.html> [--to addr] [--subject "..."] \
 *        [--account steve-office] [--dry]
 *
 * Defaults: to=steve@designerwallcoverings.com, account=steve-office,
 *           subject derived from the file's <title>.
 */

const fs = require('fs');
const path = require('path');

// ---- fallback links used ONLY in the test preview (cosmetic; CC merges the real ones) ----
const FALLBACKS = {
  // CAN-SPAM-reasonable: a real mailto so the test link is functional, not dead.
  '{{ unsubscribe }}':        'mailto:steve@designerwallcoverings.com?subject=Unsubscribe',
  '{{unsubscribe}}':          'mailto:steve@designerwallcoverings.com?subject=Unsubscribe',
  '{{update_preferences_url}}': 'https://designerwallcoverings.com',
  '{{ update_preferences_url }}': 'https://designerwallcoverings.com',
};

function parseArgs(argv) {
  const a = { _: [] };
  for (let i = 2; i < argv.length; i++) {
    const t = argv[i];
    if (t === '--dry') a.dry = true;
    else if (t === '--to') a.to = argv[++i];
    else if (t === '--subject') a.subject = argv[++i];
    else if (t === '--account') a.account = argv[++i];
    else a._.push(t);
  }
  return a;
}

function applyFallbacks(html) {
  let out = html, n = 0;
  for (const [tok, url] of Object.entries(FALLBACKS)) {
    const parts = out.split(tok);
    n += parts.length - 1;
    out = parts.join(url);
  }
  return { html: out, replaced: n };
}

function deriveSubject(html) {
  const m = html.match(/<title>([\s\S]*?)<\/title>/i);
  return m ? `[TEST] ${m[1].trim()}` : '[TEST] Mailer preview';
}

(async () => {
  const args = parseArgs(process.argv);
  const file = args._[0];
  if (!file) { console.error('usage: node send-test-via-george.js <mailer.html> [--to] [--subject] [--account] [--dry]'); process.exit(1); }

  const abs = path.resolve(file);
  const raw = fs.readFileSync(abs, 'utf8');
  const { html, replaced } = applyFallbacks(raw);

  const to      = args.to      || 'steve@designerwallcoverings.com';
  const account = args.account || 'steve-office';
  const subject = args.subject || deriveSubject(raw);

  console.log(`mailer:   ${abs} (${raw.length} bytes)`);
  console.log(`tokens:   ${replaced} CC token(s) swapped for test fallbacks (source file untouched)`);
  console.log(`to:       ${to}`);
  console.log(`account:  ${account}`);
  console.log(`subject:  ${subject}`);

  if (args.dry) { console.log('\n--dry: not sending.'); return; }

  const auth = 'Basic ' + Buffer.from('admin:').toString('base64');
  const res = await fetch('http://127.0.0.1:9850/api/send', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: auth },
    body: JSON.stringify({ to, subject, body: html, account }),
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok || !json.success) {
    console.error(`\nSEND FAILED (${res.status}):`, JSON.stringify(json));
    process.exit(1);
  }
  console.log(`\n✅ sent — messageId ${json.messageId} (thread ${json.threadId})`);
})().catch((e) => { console.error('FATAL', e.message); process.exit(1); });