[object Object]

← back to Majilite Onboard

Draft Constant Contact campaign for Majilite Metallic Specialties I (email HTML + spec)

8fafdafb27ff3d51b425cc3f8c4d545932b51375 · 2026-08-10 11:54:52 -0700 · Steve Abrams

Files touched

Diff

commit 8fafdafb27ff3d51b425cc3f8c4d545932b51375
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 10 11:54:52 2026 -0700

    Draft Constant Contact campaign for Majilite Metallic Specialties I (email HTML + spec)
---
 campaign/campaign.json       | 19 ++++++++++++
 campaign/majilite_email.html | 71 ++++++++++++++++++++++++++++++++++++++++++++
 scripts/cc_probe.js          | 29 ++++++++++++++++++
 3 files changed, 119 insertions(+)

diff --git a/campaign/campaign.json b/campaign/campaign.json
new file mode 100644
index 0000000..ddbb214
--- /dev/null
+++ b/campaign/campaign.json
@@ -0,0 +1,19 @@
+{
+  "platform": "Constant Contact",
+  "campaign_name": "Majilite Metallic Specialties I — New Collection",
+  "subject": "New: Majilite Metallic Specialties I — 160 luxe metallic finishes",
+  "preheader": "Breathable, durable Nytek® faux leather for walls & seating. Order swatches.",
+  "from_name": "Designer Wallcoverings & Fabrics",
+  "from_email": "TBD — must be a CONFIRMED sender in the CC account (e.g. info@designerwallcoverings.com)",
+  "reply_to_email": "info@designerwallcoverings.com",
+  "html_file": "majilite_email.html",
+  "cta_url": "https://www.designerwallcoverings.com/collections/majilite-metallic-specialties-i",
+  "target_list": "TBD — Steve picks the CC contact list (e.g. Trade / All Customers)",
+  "status_intended": "DRAFT (create only; scheduling/sending is Steve-gated send-to-list)",
+  "compliance": {
+    "can_spam_footer": "physical address + unsubscribe via CC merge tokens [[physical_address]] / [[UNSUBSCRIBE]]",
+    "subject_screen": "no ALL-CAPS, no RE:/FW:, no $ in subject (price kept in body) — passes hard+soft screen",
+    "note": "CC auto-injects the account's registered physical address + unsubscribe at send"
+  },
+  "how_to_create": "Via Connie (cc-agent :9820) which holds the live rotating CC token, or the marketing-command-center CC module. Local master CC_REFRESH_TOKEN is expired by design (CC rotates it; Connie holds the live one)."
+}
diff --git a/campaign/majilite_email.html b/campaign/majilite_email.html
new file mode 100644
index 0000000..0206d21
--- /dev/null
+++ b/campaign/majilite_email.html
@@ -0,0 +1,71 @@
+<!-- Constant Contact campaign — Majilite Metallic Specialties I (email-safe tables + inline styles) -->
+<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#0d0f12;margin:0;padding:0">
+<tr><td align="center" style="padding:0">
+  <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#14171c;font-family:Helvetica,Arial,sans-serif;color:#e9ecf1">
+
+    <!-- wordmark -->
+    <tr><td align="center" style="padding:22px 24px 8px">
+      <div style="font-size:13px;letter-spacing:.28em;color:#b7bcc2;text-transform:uppercase">Designer Wallcoverings &amp; Fabrics</div>
+    </td></tr>
+
+    <!-- hero -->
+    <tr><td style="padding:0">
+      <img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/DWMJ-600158.png" width="600" alt="Majilite Metallic Specialties I" style="display:block;width:100%;height:auto;border:0"/>
+    </td></tr>
+
+    <!-- headline -->
+    <tr><td align="center" style="padding:26px 30px 6px">
+      <div style="font-size:12px;letter-spacing:.22em;color:#5fd08a;text-transform:uppercase;margin-bottom:10px">New Collection · The Nytek® Collection</div>
+      <h1 style="margin:0;font-size:30px;line-height:1.15;font-weight:400;letter-spacing:.01em;color:#ffffff;font-style:italic">Majilite Metallic Specialties&nbsp;I</h1>
+    </td></tr>
+
+    <!-- body -->
+    <tr><td align="center" style="padding:14px 40px 6px">
+      <p style="margin:0;font-size:15px;line-height:1.6;color:#c9ced6">
+        <strong>160 luxurious metallic &amp; neutral finishes</strong> in Majilite's engineered
+        <em>Nytek®</em> faux leather — breathable, remarkably durable (300,000+ double-rubs), and
+        <strong>PFAS-, PVC- &amp; plasticizer-free</strong>. Ideal for wall coverings, seating,
+        flat surfaces &amp; displays.
+      </p>
+    </td></tr>
+
+    <!-- swatch trio -->
+    <tr><td style="padding:22px 30px 6px">
+      <table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>
+        <td width="33%" style="padding:4px"><img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/DWMJ-600007.png" width="100%" alt="Reflection Aluminum" style="display:block;width:100%;height:auto;border-radius:6px;border:0"/></td>
+        <td width="33%" style="padding:4px"><img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/DWMJ-600120.png" width="100%" alt="Rose Gold" style="display:block;width:100%;height:auto;border-radius:6px;border:0"/></td>
+        <td width="33%" style="padding:4px"><img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/DWMJ-600160.png" width="100%" alt="Hammered Metal Midnight" style="display:block;width:100%;height:auto;border-radius:6px;border:0"/></td>
+      </tr></table>
+    </td></tr>
+
+    <!-- CTA -->
+    <tr><td align="center" style="padding:22px 30px 8px">
+      <a href="https://www.designerwallcoverings.com/collections/majilite-metallic-specialties-i"
+         style="display:inline-block;background:#5b6cff;color:#ffffff;text-decoration:none;font-size:15px;font-weight:600;letter-spacing:.02em;padding:14px 34px;border-radius:8px">
+        Shop the Collection →
+      </a>
+      <div style="margin-top:12px;font-size:13px;color:#8a929e">Order any swatch as a <strong style="color:#c9ced6">$4.25 sample</strong>.</div>
+    </td></tr>
+
+    <!-- reassurance line -->
+    <tr><td align="center" style="padding:16px 40px 8px">
+      <div style="font-size:12px;line-height:1.6;color:#8a929e;border-top:1px solid #232830;padding-top:18px">
+        54&quot; wide · sold by the yard · inherent flame resistance (NFPA-260 Class 1) · made in Massachusetts.<br>
+        Now stocked at Designer Wallcoverings &amp; Fabrics.
+      </div>
+    </td></tr>
+
+    <!-- CAN-SPAM footer: CC injects the account's registered physical address + a working
+         unsubscribe link at send time. Tokens below satisfy the pre-flight gate. -->
+    <tr><td align="center" style="padding:0 40px 26px">
+      <div style="font-size:11px;line-height:1.6;color:#6b7079">
+        Designer Wallcoverings &amp; Fabrics &nbsp;·&nbsp; (888) 373-4564 &nbsp;·&nbsp; DesignerWallcoverings.com<br>
+        [[physical_address]]<br>
+        You received this because you're a Designer Wallcoverings customer or trade contact.
+        &nbsp;<a href="[[UNSUBSCRIBE]]" style="color:#8a929e">Unsubscribe</a> or update your preferences.
+      </div>
+    </td></tr>
+
+  </table>
+</td></tr>
+</table>
diff --git a/scripts/cc_probe.js b/scripts/cc_probe.js
new file mode 100644
index 0000000..7d42a6f
--- /dev/null
+++ b/scripts/cc_probe.js
@@ -0,0 +1,29 @@
+// Constant Contact v3 — verify creds, refresh token (save rotated), read senders/address/lists.
+const fs=require('fs'),path=require('path');
+const ENV=path.join(process.env.HOME,'Projects/secrets-manager/.env');
+const env=fs.readFileSync(ENV,'utf8');
+const g=k=>((env.match(new RegExp('^'+k+'=(.+)$','m'))||[])[1]||'').replace(/["']/g,'').trim();
+const CID=g('CC_CLIENT_ID'), CSEC=g('CC_CLIENT_SECRET'), RTOK=g('CC_REFRESH_TOKEN');
+(async()=>{
+  if(!CID||!CSEC||!RTOK){console.log('missing CC creds', {CID:!!CID,CSEC:!!CSEC,RTOK:!!RTOK});return;}
+  const basic=Buffer.from(CID+':'+CSEC).toString('base64');
+  const body=new URLSearchParams({grant_type:'refresh_token',refresh_token:RTOK});
+  const tr=await fetch('https://authz.constantcontact.com/oauth2/default/v1/token',{method:'POST',headers:{'Authorization':'Basic '+basic,'Content-Type':'application/x-www-form-urlencoded'},body});
+  const tj=await tr.json();
+  if(!tj.access_token){console.log('TOKEN FAIL', JSON.stringify(tj).slice(0,200));return;}
+  const AT=tj.access_token;
+  // persist rotated refresh token immediately
+  if(tj.refresh_token && tj.refresh_token!==RTOK){
+    let e=fs.readFileSync(ENV,'utf8'); e=e.replace(/^CC_REFRESH_TOKEN=.*$/m,'CC_REFRESH_TOKEN='+tj.refresh_token); fs.writeFileSync(ENV,e);
+    console.log('rotated CC_REFRESH_TOKEN saved (…'+tj.refresh_token.slice(-6)+')');
+  }
+  fs.writeFileSync('/tmp/majilite/cc_at.txt', AT);
+  const api=(p)=>fetch('https://api.cc.email/v3'+p,{headers:{'Authorization':'Bearer '+AT,'Accept':'application/json'}}).then(r=>r.json());
+  const sum=await api('/account/summary?extra_fields=physical_address');
+  console.log('ACCOUNT:', sum.organization_name||sum.email||'?', '| addr:', JSON.stringify(sum.physical_address||{}).slice(0,120));
+  const emails=await api('/account/emails');
+  const verified=(Array.isArray(emails)?emails:[]).filter(e=>e.confirm_status==='CONFIRMED');
+  console.log('VERIFIED SENDERS:', verified.map(e=>e.email_address).join(', ')||JSON.stringify(emails).slice(0,150));
+  const lists=await api('/contact_lists?include_count=true');
+  console.log('LISTS:', (lists.lists||[]).map(l=>l.name+' ('+(l.membership_count||0)+') ['+l.list_id+']').join(' | ').slice(0,400));
+})();

← 8889c9b Publish all 160 Majilite + collection to ALL 13 sales channe  ·  back to Majilite Onboard  ·  chore: session-close quality gate (lint ✓ 0-fix, refactor ✓ ee8bbbf →