← 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
A mailers/send-test-via-george.js
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 →