[object Object]

← back to Designer Wallcoverings

mailers: add George test-send helper with CC-token fallback (clean self-test previews, CC source untouched)

19239652384e35e5485787d68d169de3d3554590 · 2026-06-24 14:22:35 -0700 · Steve

Files touched

Diff

commit 19239652384e35e5485787d68d169de3d3554590
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jun 24 14:22:35 2026 -0700

    mailers: add George test-send helper with CC-token fallback (clean self-test previews, CC source untouched)
---
 mailers/send-test-via-george.js | 99 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 99 insertions(+)

diff --git a/mailers/send-test-via-george.js b/mailers/send-test-via-george.js
new file mode 100644
index 00000000..a556b46a
--- /dev/null
+++ b/mailers/send-test-via-george.js
@@ -0,0 +1,99 @@
+#!/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); });

← 1bd894df China Seas mailer: China Seas logo at top + bottom, room-set  ·  back to Designer Wallcoverings  ·  China Seas mailer: DW banner restored at top, China Seas log 29700f1b →