feat(campaigns): TARGO rebrand + Mustache sections + Mailjet webhook setup
- Template gift-email-fr.html: switch from Gigafibre indigo to TARGO green (#019547), use real Mailjet-hosted TARGO logo, adopt retention-offer layout from the latest mockup (tutoiement, Option 1/Option 2 split, prorata-refund disclaimer, "L'équipe TARGO" signature). Row 1 of the merchant grid uses real Mailjet logos (Amazon, IGA, Tim Hortons, $1 Plus); rows 2-3 are placehold.co until URLs are shared. - send_gift_campaign.js: add {{#var}}...{{/var}} Mustache section support to the renderer so the optional expiry block disappears cleanly when --expiry is omitted (was rendering literal tags before). Add new --commitment-months CLI flag (default 3) for the "Rester encore X mois ou +" wording. - setup_mailjet_webhook.js (new): one-shot Node script to register the Hub callback URL with Mailjet's /v3/REST/eventcallbackurl. Defaults to a safe event subset (open/click/spam/unsub) that doesn't conflict with the WP-Mail-SMTP integration already owning sent/bounce/blocked. --all forces full takeover with a conflict guard requiring --force-takeover to overwrite existing records. Supports --list and --delete for inspection / rollback. - package.json (new): nodemailer dependency for SMTP send. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9b06e2df30
commit
9f2b37939d
|
|
@ -99,7 +99,7 @@ domain level. The two known-validated senders on this account are:
|
|||
The default for gift campaigns:
|
||||
|
||||
```
|
||||
--from "Gigafibre Support <support@targointernet.com>"
|
||||
--from "TARGO <support@targointernet.com>"
|
||||
```
|
||||
|
||||
Reasoning for `support@` over `noreply@`: campaigns INVITE a reply
|
||||
|
|
@ -129,10 +129,11 @@ node send_gift_campaign.js \
|
|||
--gifts /path/to/giftbit-gifts.csv \
|
||||
--contacts /path/to/giftbit-contacts-A-first-email.csv \
|
||||
--template ./templates/gift-email-fr.html \
|
||||
--subject "🎁 Un cadeau pour vous, de la part de Gigafibre" \
|
||||
--amount "50 $" \
|
||||
--subject "🎁 Un cadeau pour vous, de la part de TARGO" \
|
||||
--amount "60 $" \
|
||||
--expiry "31 décembre 2026" \
|
||||
--from "Gigafibre Support <support@targointernet.com>" \
|
||||
--commitment-months 3 \
|
||||
--from "TARGO <support@targointernet.com>" \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
|
|
@ -151,10 +152,11 @@ node send_gift_campaign.js \
|
|||
--gifts /path/to/giftbit-gifts.csv \
|
||||
--contacts /path/to/giftbit-contacts-A-first-email.csv \
|
||||
--template ./templates/gift-email-fr.html \
|
||||
--subject "🎁 Un cadeau pour vous, de la part de Gigafibre" \
|
||||
--amount "50 $" \
|
||||
--subject "🎁 Un cadeau pour vous, de la part de TARGO" \
|
||||
--amount "60 $" \
|
||||
--expiry "31 décembre 2026" \
|
||||
--from "Gigafibre Support <support@targointernet.com>" \
|
||||
--commitment-months 3 \
|
||||
--from "TARGO <support@targointernet.com>" \
|
||||
--smtp-host in-v3.mailjet.com --smtp-port 587 \
|
||||
--smtp-user "$SMTP_USER" --smtp-pass "$SMTP_PASS" \
|
||||
--throttle-ms 600
|
||||
|
|
@ -185,11 +187,13 @@ Variables resolved at send time:
|
|||
| `{{gift_url}}` | matched from the gifts CSV |
|
||||
| `{{amount}}` | `--amount` CLI flag (e.g. `"50 $"`) |
|
||||
| `{{expiry}}` | `--expiry` CLI flag (e.g. `"31 décembre 2026"`) |
|
||||
| `{{commitment_months}}` | `--commitment-months` CLI flag (default `3`) — used in the "Condition" pill and prorata-refund disclaimer of the retention offer |
|
||||
|
||||
The template uses a vintage `{{#expiry}} ... {{/expiry}}` block for the
|
||||
optional expiry line — currently rendered as plain text (the script's
|
||||
simple `{{var}}` renderer doesn't strip the tags). If you don't want the
|
||||
expiry sentence, edit the template directly to remove that block.
|
||||
The template uses a Mustache-style `{{#expiry}} ... {{/expiry}}` block for
|
||||
the optional expiry line. The renderer keeps the contents when the
|
||||
matching variable is truthy and drops them entirely otherwise — so if you
|
||||
omit `--expiry` from the CLI, the "Le lien expire le …" sentence
|
||||
disappears cleanly with no orphan tags showing.
|
||||
|
||||
## Source data — the two CSVs
|
||||
|
||||
|
|
|
|||
25
scripts/campaigns/package-lock.json
generated
Normal file
25
scripts/campaigns/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "campaigns",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "campaigns",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"nodemailer": "^8.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.7",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
|
||||
"integrity": "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
scripts/campaigns/package.json
Normal file
16
scripts/campaigns/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "campaigns",
|
||||
"version": "1.0.0",
|
||||
"description": "One-shot tool to send Giftbit gift cards to a list of contacts with a branded French email, bypassing Giftbit's English-only built-in delivery.",
|
||||
"main": "create_giftbit_campaign.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"nodemailer": "^8.0.7"
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
* --subject "Votre cadeau Gigafibre" \
|
||||
* --amount "50 $" \
|
||||
* --expiry "31 décembre 2026" \
|
||||
* --commitment-months 3 \
|
||||
* --from "Gigafibre <noreply@gigafibre.ca>" \
|
||||
* --smtp-host in-v3.mailjet.com --smtp-port 587 \
|
||||
* --smtp-user $SMTP_USER --smtp-pass $SMTP_PASS \
|
||||
|
|
@ -75,6 +76,9 @@ const AMOUNT = args.amount || '50 $'
|
|||
const EXPIRY = args.expiry || ''
|
||||
const SUBJECT = args.subject
|
||||
const FROM = args.from
|
||||
// Retention commitment used in the template's "Condition" pill and prorata
|
||||
// disclaimer. Default 3 months. Override with --commitment-months 6 etc.
|
||||
const COMMITMENT_MONTHS = args['commitment-months'] || '3'
|
||||
|
||||
// ── CSV parsing ────────────────────────────────────────────────────────────
|
||||
// Minimal RFC-4180-ish parser. Handles quoted fields with embedded commas
|
||||
|
|
@ -164,7 +168,18 @@ function matchByEmail (gifts, contacts, urlCol) {
|
|||
}
|
||||
|
||||
// ── Template rendering ─────────────────────────────────────────────────────
|
||||
// Supports two constructs:
|
||||
// {{var}} simple substitution
|
||||
// {{#var}}...{{/var}} section block: kept if var is truthy, dropped otherwise
|
||||
//
|
||||
// The section pass runs FIRST so that variable expansion can fill in the
|
||||
// kept body. Non-greedy match with [\s\S] handles multi-line blocks (HTML
|
||||
// templates span many lines between the open/close tags).
|
||||
function render (tpl, vars) {
|
||||
// Pass 1: section blocks (truthy → keep body, falsy → drop everything)
|
||||
tpl = tpl.replace(/\{\{\s*#\s*(\w+)\s*\}\}([\s\S]*?)\{\{\s*\/\s*\1\s*\}\}/g,
|
||||
(_, k, body) => (vars[k] ? body : ''))
|
||||
// Pass 2: simple variable substitution
|
||||
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => {
|
||||
const v = vars[k]
|
||||
return v == null ? '' : String(v)
|
||||
|
|
@ -247,13 +262,14 @@ async function main () {
|
|||
for (let i = 0; i < matched.length; i++) {
|
||||
const { contact, gift_url } = matched[i]
|
||||
const vars = {
|
||||
firstname: contact.firstname || 'cher client',
|
||||
lastname: contact.lastname || '',
|
||||
email: contact.email,
|
||||
description: contact.description || '',
|
||||
firstname: contact.firstname || 'cher client',
|
||||
lastname: contact.lastname || '',
|
||||
email: contact.email,
|
||||
description: contact.description || '',
|
||||
gift_url,
|
||||
amount: AMOUNT,
|
||||
expiry: EXPIRY,
|
||||
amount: AMOUNT,
|
||||
expiry: EXPIRY,
|
||||
commitment_months: COMMITMENT_MONTHS,
|
||||
}
|
||||
const html = render(tpl, vars)
|
||||
const ts = new Date().toISOString()
|
||||
|
|
|
|||
200
scripts/campaigns/setup_mailjet_webhook.js
Executable file
200
scripts/campaigns/setup_mailjet_webhook.js
Executable file
|
|
@ -0,0 +1,200 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict'
|
||||
/**
|
||||
* setup_mailjet_webhook.js — Register the Hub's /campaigns/webhook URL with
|
||||
* Mailjet's Event API for every event type we care about.
|
||||
*
|
||||
* Mailjet's API uses ONE eventcallbackurl record PER event type. We want to
|
||||
* be notified about: sent, open, click, bounce, blocked, spam, unsub. So this
|
||||
* script idempotently registers (POST) or updates (PUT) one record per type.
|
||||
*
|
||||
* Auth: SMTP_USER + SMTP_PASS env vars (same creds work for the REST API on
|
||||
* Mailjet — they call them API_PUBLIC_KEY / API_PRIVATE_KEY but the values
|
||||
* are identical to the SMTP credentials).
|
||||
*
|
||||
* Usage:
|
||||
* export SMTP_USER=<MJ_APIKEY_PUBLIC>
|
||||
* export SMTP_PASS=<MJ_APIKEY_PRIVATE>
|
||||
* node setup_mailjet_webhook.js --url https://msg.gigafibre.ca/campaigns/webhook
|
||||
*
|
||||
* # Production-safe defaults:
|
||||
* # --is-backup false primary (not backup) callback URL
|
||||
* # --group-events true send events as a JSON array (recommended,
|
||||
* # minimizes hub load — one POST per ~50 events
|
||||
* # instead of one POST per event)
|
||||
*
|
||||
* To inspect / delete what's registered:
|
||||
* node setup_mailjet_webhook.js --list
|
||||
* node setup_mailjet_webhook.js --delete <id>
|
||||
*/
|
||||
|
||||
const https = require('https')
|
||||
|
||||
const ALL_EVENTS = ['sent', 'open', 'click', 'bounce', 'blocked', 'spam', 'unsub']
|
||||
// Safe defaults: only events that aren't typically already claimed by other
|
||||
// integrations (WP-Mail-SMTP on targo.ca currently owns sent/bounce/blocked
|
||||
// — see `--list` output). open + click are the events the gift campaign
|
||||
// actually needs for tracking; spam + unsub are nice-to-have signals.
|
||||
const SAFE_EVENTS = ['open', 'click', 'spam', 'unsub']
|
||||
|
||||
function parseArgs (argv) {
|
||||
const out = {}
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a.startsWith('--')) {
|
||||
const k = a.slice(2); const next = argv[i + 1]
|
||||
if (!next || next.startsWith('--')) out[k] = true
|
||||
else { out[k] = next; i++ }
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function mjApi (method, urlPath, body, { user, pass }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = body ? JSON.stringify(body) : null
|
||||
const auth = Buffer.from(`${user}:${pass}`).toString('base64')
|
||||
const req = https.request({
|
||||
host: 'api.mailjet.com',
|
||||
path: '/v3/REST' + urlPath,
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + auth,
|
||||
'Accept': 'application/json',
|
||||
...(data ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } : {}),
|
||||
},
|
||||
}, res => {
|
||||
let chunks = ''
|
||||
res.on('data', c => { chunks += c })
|
||||
res.on('end', () => {
|
||||
try { resolve({ status: res.statusCode, body: chunks ? JSON.parse(chunks) : {} }) }
|
||||
catch (e) { resolve({ status: res.statusCode, body: chunks }) }
|
||||
})
|
||||
})
|
||||
req.on('error', reject)
|
||||
if (data) req.write(data)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
async function listCallbacks (creds) {
|
||||
const r = await mjApi('GET', '/eventcallbackurl?Limit=100', null, creds)
|
||||
if (r.status !== 200) throw new Error(`GET eventcallbackurl ${r.status}: ${JSON.stringify(r.body)}`)
|
||||
return r.body.Data || []
|
||||
}
|
||||
|
||||
async function deleteCallback (id, creds) {
|
||||
const r = await mjApi('DELETE', `/eventcallbackurl/${id}`, null, creds)
|
||||
return r.status === 204 || r.status === 200
|
||||
}
|
||||
|
||||
async function upsert (eventType, url, isBackup, creds, existing) {
|
||||
// existing array is the result of GET — find a matching record (same
|
||||
// EventType + IsBackup combination). Mailjet only allows ONE primary +
|
||||
// ONE backup URL per event, so this combination is the unique key.
|
||||
const match = existing.find(r => r.EventType === eventType && Boolean(r.IsBackup) === isBackup)
|
||||
const payload = { EventType: eventType, IsBackup: isBackup, Url: url, Status: 'alive', Version: 2 }
|
||||
if (match) {
|
||||
const r = await mjApi('PUT', `/eventcallbackurl/${match.ID}`, payload, creds)
|
||||
return { action: 'updated', id: match.ID, status: r.status, ok: r.status === 200 }
|
||||
} else {
|
||||
const r = await mjApi('POST', '/eventcallbackurl', payload, creds)
|
||||
const id = r.body.Data?.[0]?.ID
|
||||
return { action: 'created', id, status: r.status, ok: r.status === 201 || r.status === 200 }
|
||||
}
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const args = parseArgs(process.argv)
|
||||
const user = process.env.SMTP_USER || process.env.MJ_APIKEY_PUBLIC
|
||||
const pass = process.env.SMTP_PASS || process.env.MJ_APIKEY_PRIVATE
|
||||
if (!user || !pass) {
|
||||
console.error('Set SMTP_USER + SMTP_PASS (or MJ_APIKEY_PUBLIC + MJ_APIKEY_PRIVATE).')
|
||||
process.exit(1)
|
||||
}
|
||||
const creds = { user, pass }
|
||||
|
||||
// --list — dump current config and exit
|
||||
if (args.list) {
|
||||
const callbacks = await listCallbacks(creds)
|
||||
console.log(`\n── Registered event callbacks: ${callbacks.length} ──`)
|
||||
for (const c of callbacks) {
|
||||
const flag = c.IsBackup ? '[BACKUP]' : '[PRIMARY]'
|
||||
console.log(` ${flag} id=${c.ID} event=${c.EventType.padEnd(10)} status=${c.Status} v${c.Version} → ${c.Url}`)
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// --delete <id>
|
||||
if (args.delete && args.delete !== true) {
|
||||
const ok = await deleteCallback(args.delete, creds)
|
||||
console.log(ok ? ` ✓ deleted callback ${args.delete}` : ` ✗ delete failed`)
|
||||
process.exit(ok ? 0 : 1)
|
||||
}
|
||||
|
||||
const url = args.url
|
||||
if (!url || url === true) {
|
||||
console.error('Missing --url <callback-url>')
|
||||
console.error('Example: --url https://msg.gigafibre.ca/campaigns/webhook')
|
||||
process.exit(1)
|
||||
}
|
||||
if (!url.startsWith('https://')) {
|
||||
console.error('Mailjet requires HTTPS. Got:', url)
|
||||
process.exit(1)
|
||||
}
|
||||
const isBackup = args['is-backup'] === 'true' || args['is-backup'] === true
|
||||
|
||||
// Resolve which events to configure
|
||||
let events
|
||||
if (args.all) {
|
||||
events = ALL_EVENTS
|
||||
} else if (args.events && args.events !== true) {
|
||||
events = args.events.split(',').map(e => e.trim()).filter(Boolean)
|
||||
} else {
|
||||
events = SAFE_EVENTS
|
||||
}
|
||||
|
||||
const existing = await listCallbacks(creds)
|
||||
|
||||
// Pre-flight: detect conflicts with existing PRIMARY records pointing
|
||||
// elsewhere. Refuse to overwrite unless --force-takeover is passed.
|
||||
const conflicts = events
|
||||
.map(ev => ({ ev, hit: existing.find(r => r.EventType === ev && Boolean(r.IsBackup) === isBackup) }))
|
||||
.filter(c => c.hit && c.hit.Url !== url)
|
||||
|
||||
console.log(`\n── Mailjet Event API webhook setup ──`)
|
||||
console.log(` callback URL: ${url}`)
|
||||
console.log(` type: ${isBackup ? 'BACKUP' : 'PRIMARY'}`)
|
||||
console.log(` events: ${events.join(', ')}`)
|
||||
console.log(` existing: ${existing.length} records on the account`)
|
||||
|
||||
if (conflicts.length && !args['force-takeover']) {
|
||||
console.log(`\n ⚠ Conflicts detected — these events already point elsewhere:`)
|
||||
for (const c of conflicts) {
|
||||
console.log(` • ${c.ev.padEnd(10)} → ${c.hit.Url} (id=${c.hit.ID})`)
|
||||
}
|
||||
console.log(`\n Refusing to overwrite without --force-takeover. Either:`)
|
||||
console.log(` • Exclude the conflicting events: --events open,click`)
|
||||
console.log(` • Or override the existing config: --force-takeover`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log()
|
||||
|
||||
let okCount = 0
|
||||
for (const ev of events) {
|
||||
process.stdout.write(` ${ev.padEnd(10)} ... `)
|
||||
const r = await upsert(ev, url, isBackup, creds, existing)
|
||||
if (r.ok) {
|
||||
okCount++
|
||||
console.log(`✓ ${r.action} (id=${r.id})`)
|
||||
} else {
|
||||
console.log(`✗ status=${r.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n ${okCount}/${events.length} events configured.`)
|
||||
console.log(`\n Verify with: node setup_mailjet_webhook.js --list`)
|
||||
console.log(` Mailjet dashboard: Account Settings → REST API → Event tracking\n`)
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('Fatal:', e); process.exit(1) })
|
||||
|
|
@ -3,97 +3,329 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Un cadeau de Gigafibre</title>
|
||||
<title>Une offre exclusive de TARGO</title>
|
||||
</head>
|
||||
<body style="margin:0; padding:0; background:#f5f6fa; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; color:#1f2937; line-height:1.5;">
|
||||
|
||||
<!-- Spacer above the card -->
|
||||
<div style="height:32px;"></div>
|
||||
<body style="margin:0; padding:0; background:#f7f8f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; color:#1f2937; line-height:1.5;">
|
||||
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<td align="center" style="padding:32px 16px;">
|
||||
|
||||
<!-- Main card -->
|
||||
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0"
|
||||
style="max-width:600px; background:#ffffff; border-radius:14px; overflow:hidden; box-shadow:0 6px 24px rgba(15,23,42,0.07);">
|
||||
style="max-width:600px; background:#ffffff; border:1px solid #e5e7eb; border-radius:14px; overflow:hidden;">
|
||||
|
||||
<!-- Header band -->
|
||||
<!-- Logo header (clean, no colored band) -->
|
||||
<tr>
|
||||
<td style="background:linear-gradient(135deg,#4f46e5 0%, #7c3aed 100%); padding:36px 32px 28px; text-align:center; color:#ffffff;">
|
||||
<div style="font-size:0.78rem; font-weight:700; letter-spacing:0.12em; text-transform:uppercase; opacity:0.85;">
|
||||
Gigafibre · Récompense
|
||||
</div>
|
||||
<div style="font-size:2.2rem; line-height:1.1; margin-top:10px; font-weight:800;">
|
||||
🎁 Un cadeau pour vous
|
||||
<td style="padding:28px 36px 22px; border-bottom:1px solid #eef0ee;">
|
||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content"
|
||||
alt="TARGO" width="140"
|
||||
style="display:block; border:0; outline:none; text-decoration:none; max-width:140px; height:auto;">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Greeting + hook -->
|
||||
<tr>
|
||||
<td style="padding:26px 36px 4px;">
|
||||
<p style="margin:0 0 14px; font-size:1rem; color:#374151;">Bonjour {{firstname}},</p>
|
||||
<p style="margin:0 0 10px; font-size:1.08rem; color:#1f2937; font-weight:500;">
|
||||
Tu choisis local, on veut te remercier.
|
||||
</p>
|
||||
<p style="margin:0; font-size:1rem; color:#374151;">
|
||||
Avec l'arrivée de l'été, voici ton
|
||||
<strong>offre exclusive pour un temps limité</strong> :
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Info pill: gift card amount -->
|
||||
<tr>
|
||||
<td style="padding:18px 36px 8px;">
|
||||
<div style="background:#f3f4f3; border-radius:10px; padding:14px 18px;">
|
||||
<div style="font-size:0.7rem; font-weight:700; letter-spacing:0.12em; color:#9ca3af; text-transform:uppercase; margin-bottom:4px;">
|
||||
Carte-cadeau numérique
|
||||
</div>
|
||||
<div style="font-size:1.05rem; font-weight:700; color:#1f2937;">
|
||||
🎁 {{amount}} chez des centaines de marques
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Body -->
|
||||
<!-- Two-column: ENVOI + CONDITION -->
|
||||
<tr>
|
||||
<td style="padding:36px 36px 12px;">
|
||||
<p style="margin:0 0 16px; font-size:1.05rem;">Bonjour {{firstname}},</p>
|
||||
|
||||
<p style="margin:0 0 16px;">
|
||||
Merci de faire partie de la famille Gigafibre. Pour vous remercier
|
||||
de votre fidélité, voici une carte-cadeau d'une valeur de
|
||||
<strong>{{amount}}</strong>, utilisable sur les marchands de votre choix.
|
||||
</p>
|
||||
|
||||
<!-- CTA button -->
|
||||
<div style="text-align:center; margin:32px 0 28px;">
|
||||
<a href="{{gift_url}}"
|
||||
style="display:inline-block; padding:16px 36px; background:#4f46e5; color:#ffffff;
|
||||
text-decoration:none; font-weight:700; font-size:1.05rem;
|
||||
border-radius:10px; box-shadow:0 4px 12px rgba(79,70,229,0.35);">
|
||||
Récupérer mon cadeau →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p style="margin:0 0 6px; font-size:0.9rem; color:#6b7280;">
|
||||
Le lien vous mène à une page sécurisée où vous pourrez choisir la
|
||||
marque qui vous fait plaisir (Amazon, Tim Hortons, SAQ, App Store,
|
||||
et plusieurs autres).
|
||||
</p>
|
||||
{{#expiry}}
|
||||
<p style="margin:6px 0 0; font-size:0.85rem; color:#9ca3af;">
|
||||
⏰ Le lien expire le <strong>{{expiry}}</strong>.
|
||||
</p>
|
||||
{{/expiry}}
|
||||
<td style="padding:6px 36px 18px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td width="50%" style="padding-right:5px; vertical-align:top;">
|
||||
<div style="background:#f3f4f3; border-radius:10px; padding:14px 16px;">
|
||||
<div style="font-size:0.7rem; font-weight:700; letter-spacing:0.12em; color:#9ca3af; text-transform:uppercase; margin-bottom:4px;">
|
||||
Envoi
|
||||
</div>
|
||||
<div style="font-size:0.95rem; font-weight:700; color:#1f2937;">
|
||||
⚡ Instantané à l'activation
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td width="50%" style="padding-left:5px; vertical-align:top;">
|
||||
<div style="background:#f3f4f3; border-radius:10px; padding:14px 16px;">
|
||||
<div style="font-size:0.7rem; font-weight:700; letter-spacing:0.12em; color:#9ca3af; text-transform:uppercase; margin-bottom:4px;">
|
||||
Condition
|
||||
</div>
|
||||
<div style="font-size:0.95rem; font-weight:700; color:#1f2937;">
|
||||
🤝 Rester encore {{commitment_months}} mois ou +
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Why this email -->
|
||||
<!-- Divider -->
|
||||
<tr><td style="padding:0 36px;"><div style="border-top:1px solid #eef0ee;"></div></td></tr>
|
||||
|
||||
<!-- Option 1 chip -->
|
||||
<tr>
|
||||
<td style="padding:0 36px 28px;">
|
||||
<div style="border-top:1px solid #e5e7eb; padding-top:20px; font-size:0.82rem; color:#6b7280;">
|
||||
Vous recevez ce cadeau parce que vous êtes client(e) Gigafibre à
|
||||
l'adresse <strong style="color:#374151;">{{description}}</strong>.
|
||||
Si vous avez la moindre question, écrivez-nous à
|
||||
<a href="mailto:facturation@targointernet.com" style="color:#4f46e5;">facturation@targointernet.com</a>
|
||||
ou appelez-nous au <a href="tel:5142421500" style="color:#4f46e5;">514 242-1500</a>.
|
||||
<td style="padding:22px 36px 10px;">
|
||||
<span style="display:inline-block; background:#dcf4e3; color:#019547; font-size:0.82rem; font-weight:700; padding:5px 12px; border-radius:6px;">
|
||||
✅ Option 1
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Big green CTA card -->
|
||||
<tr>
|
||||
<td style="padding:0 36px 8px;">
|
||||
<a href="{{gift_url}}" style="text-decoration:none; color:#ffffff; display:block;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"
|
||||
style="background:#019547; border-radius:14px;">
|
||||
<tr>
|
||||
<td style="padding:30px 24px; text-align:center;">
|
||||
<div style="font-size:2.1rem; font-weight:800; line-height:1; margin-bottom:14px; color:#ffffff;">
|
||||
🎁 {{amount}}
|
||||
</div>
|
||||
<div style="font-size:1.08rem; font-weight:700; color:#ffffff;">
|
||||
Activer ma carte-cadeau
|
||||
</div>
|
||||
<div style="font-size:0.85rem; opacity:0.9; margin-top:8px; color:#ffffff;">
|
||||
Choisir ma carte sur Giftbit →
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Prorata refund disclaimer -->
|
||||
<tr>
|
||||
<td style="padding:10px 36px 22px;">
|
||||
<div style="font-size:0.85rem; color:#6b7280;">
|
||||
🪂 En cas de départ avant {{commitment_months}} mois, le prorata du montant est remboursable.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer band -->
|
||||
<!-- Divider -->
|
||||
<tr><td style="padding:0 36px;"><div style="border-top:1px solid #eef0ee;"></div></td></tr>
|
||||
|
||||
<!-- Option 2 chip -->
|
||||
<tr>
|
||||
<td style="background:#f9fafb; padding:18px 32px; text-align:center; border-top:1px solid #e5e7eb;">
|
||||
<div style="font-size:0.75rem; color:#9ca3af;">
|
||||
Gigafibre — Internet fibre optique au Québec<br>
|
||||
<a href="https://www.gigafibre.ca" style="color:#9ca3af; text-decoration:underline;">www.gigafibre.ca</a>
|
||||
<td style="padding:22px 36px 8px;">
|
||||
<span style="display:inline-block; background:#f3f4f3; color:#6b7280; font-size:0.82rem; font-weight:700; padding:5px 12px; border-radius:6px;">
|
||||
⏭️ Option 2
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:0 36px 22px;">
|
||||
<div style="font-size:0.97rem; color:#4b5563; line-height:1.55;">
|
||||
Ne rien faire. Ton abonnement mensuel se poursuit normalement,
|
||||
sans engagement ni carte-cadeau.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{#expiry}}
|
||||
<!-- Optional expiry callout -->
|
||||
<tr><td style="padding:0 36px;"><div style="border-top:1px solid #eef0ee;"></div></td></tr>
|
||||
<tr>
|
||||
<td style="padding:18px 36px 0;">
|
||||
<div style="font-size:0.85rem; color:#9ca3af;">
|
||||
⏰ Cette offre expire le <strong style="color:#374151;">{{expiry}}</strong>.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/expiry}}
|
||||
|
||||
<!-- Divider -->
|
||||
<tr><td style="padding:18px 36px 0;"><div style="border-top:1px solid #eef0ee;"></div></td></tr>
|
||||
|
||||
<!-- Signature -->
|
||||
<tr>
|
||||
<td style="padding:22px 36px 28px;">
|
||||
<div style="font-size:0.97rem; color:#1f2937;">
|
||||
🤝 Merci de faire rouler l'économie de notre région avec nous !
|
||||
</div>
|
||||
<div style="font-size:0.9rem; color:#6b7280; margin-top:6px;">
|
||||
L'équipe TARGO
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<!-- Merchant brands grid — 4 cols × 3 rows = 12 logos
|
||||
TO SWAP TO MAILJET-HOSTED LOGOS:
|
||||
Replace each placeholder src URL below with the Mailjet CDN URL
|
||||
you already have (same format as the TARGO logo:
|
||||
https://xqy3m.mjt.lu/img2/xqy3m/<UUID>/content). The alt= attribute
|
||||
stays as-is (used by screen readers + shown when images blocked).
|
||||
Brand list in order: Amazon, IGA, Tim Hortons, $1 Plus (Dollarama),
|
||||
Pizza Pizza, Home Depot, Best Buy, Walmart,
|
||||
Petro-Canada, Esso, Home Hardware, Sobeys.
|
||||
-->
|
||||
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0"
|
||||
style="max-width:600px; margin-top:8px;">
|
||||
<tr>
|
||||
<td style="padding:24px 36px 12px; text-align:center;">
|
||||
<div style="font-size:1.02rem; font-weight:700; color:#019547;">
|
||||
Quelques exemples de choix pour votre carte cadeau :
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:0 28px 8px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<!-- Row 1 — real Mailjet-hosted logos -->
|
||||
<tr>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/31ffdf91-d2de-4ced-8b99-ad2221695abe/content" alt="Amazon" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/9c9dfa18-2a3a-414a-b5ad-16d490c961b5/content" alt="IGA" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/4b0b2a4a-5f99-416c-8873-8d3e4389b6f7/content" alt="Tim Hortons" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://xqy3m.mjt.lu/img2/xqy3m/162b988c-beb7-49b3-b85e-ccc12fa2c155/content" alt="$1 Plus" width="95" style="max-width:95px; height:auto; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2 -->
|
||||
<tr>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://placehold.co/160x90/ffffff/e7771a?text=Pizza+Pizza" alt="Pizza Pizza" width="120" style="max-width:120px; max-height:72px; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://placehold.co/160x90/ffffff/f96302?text=Home+Depot" alt="Home Depot" width="120" style="max-width:120px; max-height:72px; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://placehold.co/160x90/ffffff/000000?text=Best+Buy" alt="Best Buy" width="120" style="max-width:120px; max-height:72px; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://placehold.co/160x90/ffffff/0071ce?text=Walmart" alt="Walmart" width="120" style="max-width:120px; max-height:72px; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3 -->
|
||||
<tr>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://placehold.co/160x90/ffffff/e1140a?text=Petro-Canada" alt="Petro-Canada" width="120" style="max-width:120px; max-height:72px; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://placehold.co/160x90/ffffff/004b8d?text=Esso" alt="Esso" width="120" style="max-width:120px; max-height:72px; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://placehold.co/160x90/ffffff/e6332a?text=Home+Hardware" alt="Home Hardware" width="120" style="max-width:120px; max-height:72px; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="25%" style="padding:4px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#ffffff; border-radius:8px;">
|
||||
<tr><td align="center" valign="middle" height="92" style="height:92px;">
|
||||
<img src="https://placehold.co/160x90/ffffff/4caf50?text=Sobeys" alt="Sobeys" width="120" style="max-width:120px; max-height:72px; display:inline-block; border:0;">
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer (outside the card, small print) -->
|
||||
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0"
|
||||
style="max-width:600px;">
|
||||
<tr>
|
||||
<td style="padding:18px 36px 8px; text-align:center;">
|
||||
<div style="font-size:0.75rem; color:#9ca3af; line-height:1.55;">
|
||||
Tu reçois ce courriel parce que tu es client(e) TARGO à
|
||||
<strong style="color:#6b7280;">{{description}}</strong>.<br>
|
||||
Une question ? Écris-nous à
|
||||
<a href="mailto:facturation@targointernet.com" style="color:#019547;">facturation@targointernet.com</a>
|
||||
ou appelle au <a href="tel:5142421500" style="color:#019547;">514 242-1500</a>.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:0 36px 28px; text-align:center;">
|
||||
<div style="font-size:0.7rem; color:#9ca3af;">
|
||||
<strong style="color:#019547; letter-spacing:0.08em;">TARGO</strong>
|
||||
— Internet fibre optique au Québec — service <em>Gigafibre</em><br>
|
||||
<a href="https://www.targointernet.com" style="color:#9ca3af; text-decoration:underline;">targointernet.com</a>
|
||||
·
|
||||
<a href="https://www.gigafibre.ca" style="color:#9ca3af; text-decoration:underline;">gigafibre.ca</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Spacer below the card -->
|
||||
<div style="height:48px;"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user