Initial commit
Next.js + Express event management app for Hope Family Church.
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
// ─── Transport ────────────────────────────────────────────────────────────────
|
||||
// The SMTP transporter is built lazily and rebuilt whenever the settings cache
|
||||
// reports a different configuration (e.g. after an admin updates SMTP settings).
|
||||
|
||||
const { getSettingSync } = require('./settingsCache');
|
||||
|
||||
/** Read current SMTP config from settings cache, falling back to env vars. */
|
||||
function _smtpConfig() {
|
||||
return {
|
||||
host: getSettingSync('smtp_host', process.env.SMTP_HOST || process.env.EMAIL_HOST || ''),
|
||||
port: getSettingSync('smtp_port', process.env.SMTP_PORT || process.env.EMAIL_PORT || '587'),
|
||||
secure: getSettingSync('smtp_secure', process.env.SMTP_SECURE || process.env.EMAIL_SECURE || 'false'),
|
||||
user: getSettingSync('smtp_user', process.env.SMTP_USER || process.env.EMAIL_USER || ''),
|
||||
pass: getSettingSync('smtp_pass', process.env.SMTP_PASS || process.env.EMAIL_PASS || ''),
|
||||
from: getSettingSync('smtp_from', process.env.MAIL_FROM || process.env.EMAIL_FROM || ''),
|
||||
};
|
||||
}
|
||||
|
||||
function _configHash(c) {
|
||||
return [c.host, c.port, c.secure, c.user, c.pass].join('|');
|
||||
}
|
||||
|
||||
let _cachedTransporter = null;
|
||||
let _cachedConfigHash = null;
|
||||
|
||||
function _getTransporter() {
|
||||
const cfg = _smtpConfig();
|
||||
const hash = _configHash(cfg);
|
||||
|
||||
if (!_cachedTransporter || hash !== _cachedConfigHash) {
|
||||
_cachedConfigHash = hash;
|
||||
if (cfg.host) {
|
||||
_cachedTransporter = nodemailer.createTransport({
|
||||
host: cfg.host,
|
||||
port: parseInt(cfg.port, 10) || 587,
|
||||
secure: String(cfg.secure).toLowerCase() === 'true' || String(cfg.port) === '465',
|
||||
auth: cfg.user && cfg.pass ? { user: cfg.user, pass: cfg.pass } : undefined,
|
||||
});
|
||||
} else {
|
||||
_cachedTransporter = nodemailer.createTransport({ jsonTransport: true });
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.info('[email] SMTP not configured — using jsonTransport (dev). Configure SMTP via Admin → Site Settings or set SMTP_HOST env var.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { transporter: _cachedTransporter, cfg };
|
||||
}
|
||||
|
||||
async function sendMail({ to, subject, html, text, attachments }) {
|
||||
// Never send to anonymised/deleted accounts
|
||||
if (!to || String(to).endsWith('@deleted.invalid')) {
|
||||
console.info('[email] Skipping send to deleted account:', to);
|
||||
return;
|
||||
}
|
||||
const { transporter, cfg } = _getTransporter();
|
||||
const from = cfg.from || 'no-reply@hope-events.local';
|
||||
const info = await transporter.sendMail({ from, to, subject, html, text, ...(attachments ? { attachments } : {}) });
|
||||
|
||||
if (transporter.options && transporter.options.jsonTransport) {
|
||||
try {
|
||||
const payload = typeof info.message === 'string' ? JSON.parse(info.message) : info.message;
|
||||
console.info('[email][dev] simulated:', { to, subject, envelope: info.envelope });
|
||||
if (payload && payload.html) console.info('[email][dev] html (first 300 chars):', String(payload.html).slice(0, 300));
|
||||
} catch {
|
||||
console.info('[email][dev] simulated (raw):', info && info.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Shared template helpers ──────────────────────────────────────────────────
|
||||
|
||||
function getOrg() {
|
||||
const urlFallback = process.env.APP_BASE_URL || process.env.FRONTEND_URL || 'http://localhost:3001';
|
||||
return {
|
||||
name: getSettingSync('org_name', process.env.ORG_NAME || 'Hope Events'),
|
||||
tagline: getSettingSync('org_tagline', process.env.ORG_TAGLINE || 'Connecting community through events'),
|
||||
email: getSettingSync('smtp_from', process.env.EMAIL_FROM || process.env.EMAIL_USER || ''),
|
||||
url: getSettingSync('app_base_url', urlFallback).replace(/\/$/, ''),
|
||||
headerColor: getSettingSync('accent_color', process.env.EMAIL_HEADER_COLOR || '#1e3a5f'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps HTML content in a professional, responsive email shell.
|
||||
* @param {string} body - Inner HTML content
|
||||
* @param {{ preheader?: string }} options
|
||||
*/
|
||||
function emailWrapper(body, { preheader = '' } = {}) {
|
||||
const org = getOrg();
|
||||
const ff = `-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif`;
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<title>${org.name}</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background-color:#f1f5f9;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%">
|
||||
${preheader ? `<div style="display:none;font-size:1px;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;mso-hide:all">${preheader}‌ ‌ ‌ ‌ </div>` : ''}
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#f1f5f9">
|
||||
<tr><td align="center" style="padding:40px 16px">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px">
|
||||
|
||||
<!-- Header -->
|
||||
<tr><td style="background:linear-gradient(135deg,${org.headerColor} 0%,#2d5287 100%);border-radius:12px 12px 0 0;padding:40px 48px;text-align:center">
|
||||
<h1 style="color:#ffffff;font-size:28px;font-weight:800;margin:0;letter-spacing:-0.5px;font-family:${ff};text-shadow:0 1px 3px rgba(0,0,0,0.3)">${org.name}</h1>
|
||||
<p style="color:#ffffff;font-size:14px;font-weight:500;margin:8px 0 0 0;font-family:${ff};letter-spacing:0.3px;opacity:0.9">${org.tagline}</p>
|
||||
</td></tr>
|
||||
|
||||
<!-- Body -->
|
||||
<tr><td style="background:#ffffff;border-left:1px solid #e2e8f0;border-right:1px solid #e2e8f0;padding:48px 48px 40px 48px">
|
||||
<div style="font-family:${ff};color:#1e293b;font-size:15px;line-height:1.75">
|
||||
${body}
|
||||
</div>
|
||||
</td></tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr><td style="background:#f8fafc;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px;padding:24px 48px;text-align:center">
|
||||
<p style="color:#94a3b8;font-size:12px;margin:0 0 4px 0;font-family:${ff}">
|
||||
${org.name} •
|
||||
<a href="mailto:${org.email}" style="color:#94a3b8;text-decoration:underline">${org.email}</a> •
|
||||
<a href="${org.url}" style="color:#94a3b8;text-decoration:underline">${org.url}</a>
|
||||
</p>
|
||||
<p style="color:#cbd5e1;font-size:11px;margin:6px 0 0 0;font-family:${ff}">
|
||||
This email was sent because you have an account or registration with ${org.name}.
|
||||
</p>
|
||||
</td></tr>
|
||||
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/** Renders a prominent CTA button. */
|
||||
function ctaButton(label, url, { bg = '#2563eb', fg = '#ffffff' } = {}) {
|
||||
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:28px auto 8px auto">
|
||||
<tr><td align="center" style="border-radius:8px;background-color:${bg};mso-padding-alt:0px">
|
||||
<a href="${url}" target="_blank"
|
||||
style="display:inline-block;padding:14px 36px;font-size:15px;font-weight:700;color:${fg};text-decoration:none;border-radius:8px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;letter-spacing:0.2px;mso-hide:none"
|
||||
>${label}</a>
|
||||
</td></tr>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
/** Renders a fallback link below a CTA button. */
|
||||
function fallbackLink(url) {
|
||||
return `<p style="text-align:center;margin:4px 0 0 0;font-size:12px;color:#94a3b8;word-break:break-all">
|
||||
Or copy this link: <a href="${url}" style="color:#2563eb">${url}</a>
|
||||
</p>`;
|
||||
}
|
||||
|
||||
/** Horizontal rule. */
|
||||
function divider() {
|
||||
return `<div style="border-top:1px solid #f1f5f9;margin:32px 0"></div>`;
|
||||
}
|
||||
|
||||
/** Coloured callout box. type: info | success | warning | danger | neutral */
|
||||
function callout(content, type = 'info') {
|
||||
const map = {
|
||||
info: { bg: '#eff6ff', border: '#3b82f6', color: '#1e40af' },
|
||||
success: { bg: '#f0fdf4', border: '#22c55e', color: '#166534' },
|
||||
warning: { bg: '#fffbeb', border: '#f59e0b', color: '#92400e' },
|
||||
danger: { bg: '#fef2f2', border: '#ef4444', color: '#991b1b' },
|
||||
neutral: { bg: '#f8fafc', border: '#e2e8f0', color: '#475569' },
|
||||
};
|
||||
const s = map[type] || map.info;
|
||||
return `<div style="background:${s.bg};border-left:4px solid ${s.border};border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:${s.color};font-size:14px;line-height:1.6">
|
||||
${content}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Numbered payment option row. */
|
||||
function paymentOption(num, title, detail) {
|
||||
return `<tr>
|
||||
<td style="padding:14px 16px 14px 0;vertical-align:top;width:28px">
|
||||
<div style="width:26px;height:26px;border-radius:50%;background:#2563eb;color:#fff;font-size:13px;font-weight:700;text-align:center;line-height:26px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">${num}</div>
|
||||
</td>
|
||||
<td style="padding:14px 0;border-bottom:1px solid #f1f5f9;vertical-align:top">
|
||||
<p style="margin:0 0 4px 0;font-size:14px;font-weight:700;color:#1e293b;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">${title}</p>
|
||||
<div style="font-size:13px;color:#64748b;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;line-height:1.5">${detail}</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
// ─── Builder functions ────────────────────────────────────────────────────────
|
||||
|
||||
function buildPasswordResetEmail({ name, resetUrl }) {
|
||||
const org = getOrg();
|
||||
const preheader = `Reset your ${org.name} password. This link expires in 1 hour.`;
|
||||
const body = `
|
||||
<p style="font-size:22px;font-weight:800;color:#0f172a;margin:0 0 8px 0;letter-spacing:-0.3px">Password reset request</p>
|
||||
<p style="color:#64748b;font-size:14px;margin:0 0 32px 0">We received a request to reset your password.</p>
|
||||
|
||||
<p style="margin:0 0 8px 0;color:#374151">Hi <strong>${name || 'there'}</strong>,</p>
|
||||
<p style="margin:0 0 24px 0;color:#374151">Click the button below to choose a new password. This link will expire in <strong>1 hour</strong>.</p>
|
||||
|
||||
${ctaButton('Reset my password', resetUrl)}
|
||||
${fallbackLink(resetUrl)}
|
||||
|
||||
${divider()}
|
||||
<p style="margin:0;font-size:13px;color:#94a3b8">
|
||||
If you did not request a password reset, you can safely ignore this email — your password will not be changed.
|
||||
If you're concerned, contact us at <a href="mailto:${org.email}" style="color:#2563eb">${org.email}</a>.
|
||||
</p>`;
|
||||
|
||||
const text = `Hi ${name || 'there'},\n\nWe received a request to reset your ${org.name} password.\n\nReset link (expires in 1 hour):\n${resetUrl}\n\nIf you did not request this, ignore this email.\n\n${org.name} — ${org.email}`;
|
||||
return { text, html: emailWrapper(body, { preheader }) };
|
||||
}
|
||||
|
||||
function buildPasswordChangedEmail({ name, when, supportEmail }) {
|
||||
const org = getOrg();
|
||||
const contact = supportEmail || org.email;
|
||||
const whenText = when ? new Date(when).toLocaleString() : 'recently';
|
||||
const preheader = `Your ${org.name} password was changed. If this wasn't you, act immediately.`;
|
||||
const body = `
|
||||
<p style="font-size:22px;font-weight:800;color:#0f172a;margin:0 0 8px 0;letter-spacing:-0.3px">Password changed</p>
|
||||
<p style="color:#64748b;font-size:14px;margin:0 0 32px 0">Security notification for your account</p>
|
||||
|
||||
<p style="margin:0 0 8px 0;color:#374151">Hi <strong>${name || 'there'}</strong>,</p>
|
||||
<p style="margin:0 0 24px 0;color:#374151">
|
||||
Your <strong>${org.name}</strong> account password was successfully changed
|
||||
${when ? `on <strong>${whenText}</strong>` : 'recently'}.
|
||||
</p>
|
||||
|
||||
${callout(`<strong>This wasn't you?</strong><br/>
|
||||
If you did not make this change, your account may be compromised. Contact us immediately at
|
||||
<a href="mailto:${contact}" style="color:#991b1b;font-weight:600">${contact}</a>
|
||||
and reset your password right away.`, 'danger')}
|
||||
|
||||
<p style="margin:24px 0 0 0;font-size:13px;color:#94a3b8">
|
||||
If you made this change, no further action is needed. This is an automated security notification.
|
||||
</p>`;
|
||||
|
||||
const text = `Hi ${name || 'there'},\n\nYour ${org.name} password was changed ${when ? 'on ' + whenText : 'recently'}.\n\nIf you did NOT make this change, contact us immediately at ${contact}.\n\n${org.name} — ${org.email}`;
|
||||
return { text, html: emailWrapper(body, { preheader }) };
|
||||
}
|
||||
|
||||
function buildLoginNotificationEmail({ name, when, location, userAgent }) {
|
||||
const org = getOrg();
|
||||
const preheader = `New login to your ${org.name} account detected.`;
|
||||
const ff = `-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif`;
|
||||
const body = `
|
||||
<p style="font-size:22px;font-weight:800;color:#0f172a;margin:0 0 8px 0;letter-spacing:-0.3px">New login detected</p>
|
||||
<p style="color:#64748b;font-size:14px;margin:0 0 32px 0">A new session was opened on your account</p>
|
||||
|
||||
<p style="margin:0 0 24px 0;color:#374151">Hi <strong>${name || 'there'}</strong>,</p>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;margin:0 0 24px 0">
|
||||
<tr style="background:#f8fafc">
|
||||
<td style="padding:12px 16px;font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:0.5px;width:36%;font-family:${ff}">Detail</td>
|
||||
<td style="padding:12px 16px;font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:0.5px;font-family:${ff}">Value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px 16px;border-top:1px solid #f1f5f9;color:#64748b;font-size:13px;font-weight:600;font-family:${ff}">Time</td>
|
||||
<td style="padding:12px 16px;border-top:1px solid #f1f5f9;color:#1e293b;font-size:14px;font-weight:500;font-family:${ff}">${when}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px 16px;border-top:1px solid #f1f5f9;color:#64748b;font-size:13px;font-weight:600;font-family:${ff}">Location</td>
|
||||
<td style="padding:12px 16px;border-top:1px solid #f1f5f9;color:#1e293b;font-size:14px;font-weight:500;font-family:${ff}">${location}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px 16px;border-top:1px solid #f1f5f9;color:#64748b;font-size:13px;font-weight:600;font-family:${ff}">Device</td>
|
||||
<td style="padding:12px 16px;border-top:1px solid #f1f5f9;color:#1e293b;font-size:14px;font-weight:500;word-break:break-all;font-family:${ff}">${userAgent}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
${callout(`<strong>Not you?</strong> If you don't recognise this login, change your password immediately and contact us at <a href="mailto:${org.email}" style="color:#991b1b">${org.email}</a>.`, 'danger')}
|
||||
|
||||
<p style="margin:24px 0 0 0;font-size:13px;color:#94a3b8">If this was you, no action is needed.</p>`;
|
||||
|
||||
const text = `Hi ${name || 'there'},\n\nA new login to your ${org.name} account was detected.\n\nTime: ${when}\nLocation: ${location}\nDevice: ${userAgent}\n\nIf this was NOT you, change your password immediately and contact ${org.email}.\n\n${org.name}`;
|
||||
return { text, html: emailWrapper(body, { preheader }) };
|
||||
}
|
||||
|
||||
function buildWelcomeEmail({ name, events }) {
|
||||
const org = getOrg();
|
||||
const ff = `-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif`;
|
||||
const hasEvents = Array.isArray(events) && events.length > 0;
|
||||
const preheader = `Welcome to ${org.name}! Your account is ready.`;
|
||||
|
||||
const eventsBlock = hasEvents
|
||||
? `${divider()}
|
||||
<p style="font-size:16px;font-weight:700;color:#0f172a;margin:0 0 16px 0">Upcoming events</p>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||
${events.map(e => `<tr>
|
||||
<td style="padding:12px 0;border-bottom:1px solid #f1f5f9">
|
||||
<p style="margin:0 0 2px 0;font-size:14px;font-weight:700;color:#1e293b;font-family:${ff}">${e.title}</p>
|
||||
<p style="margin:0;font-size:13px;color:#64748b;font-family:${ff}">${new Date(e.startDate).toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}</p>
|
||||
</td>
|
||||
</tr>`).join('')}
|
||||
</table>
|
||||
${ctaButton('Browse all events', org.url + '/events')}`
|
||||
: `<p style="color:#64748b;font-size:14px;margin:0">Keep an eye on our website — new events are added regularly!</p>
|
||||
${ctaButton('View events', org.url + '/events')}`;
|
||||
|
||||
const body = `
|
||||
<p style="font-size:22px;font-weight:800;color:#0f172a;margin:0 0 8px 0;letter-spacing:-0.3px">Welcome to ${org.name}!</p>
|
||||
<p style="color:#64748b;font-size:14px;margin:0 0 32px 0">Your account has been created</p>
|
||||
|
||||
<p style="margin:0 0 16px 0;color:#374151">Hi <strong>${name || 'there'}</strong>,</p>
|
||||
<p style="margin:0 0 0 0;color:#374151">
|
||||
Your ${org.name} account is set up and ready to go. Use your account to register for events, manage your bookings, and view your tickets — all in one place.
|
||||
</p>
|
||||
|
||||
${eventsBlock}
|
||||
|
||||
${divider()}
|
||||
<p style="margin:0;font-size:13px;color:#94a3b8">We look forward to seeing you at our events!</p>`;
|
||||
|
||||
const eventsText = hasEvents
|
||||
? `Upcoming events:\n${events.map(e => `- ${e.title} (${new Date(e.startDate).toLocaleDateString()})`).join('\n')}`
|
||||
: 'Keep an eye on our website for upcoming events.';
|
||||
const text = `Welcome to ${org.name}, ${name || 'there'}!\n\nYour account has been created successfully.\n\n${eventsText}\n\n${org.url}\n\n${org.name} — ${org.email}`;
|
||||
return { text, html: emailWrapper(body, { preheader }) };
|
||||
}
|
||||
|
||||
function buildAccountActivationEmail({ name, activationUrl }) {
|
||||
const org = getOrg();
|
||||
const preheader = `Activate your ${org.name} account to get started.`;
|
||||
const body = `
|
||||
<p style="font-size:22px;font-weight:800;color:#0f172a;margin:0 0 8px 0;letter-spacing:-0.3px">Activate your account</p>
|
||||
<p style="color:#64748b;font-size:14px;margin:0 0 32px 0">One step to access your events and tickets</p>
|
||||
|
||||
<p style="margin:0 0 8px 0;color:#374151">Hi <strong>${name || 'there'}</strong>,</p>
|
||||
<p style="margin:0 0 24px 0;color:#374151">
|
||||
Your ${org.name} account is ready, but needs to be activated. Click below to set your password and access your registrations, tickets, and more.
|
||||
</p>
|
||||
|
||||
${ctaButton('Activate my account', activationUrl, { bg: '#059669' })}
|
||||
${fallbackLink(activationUrl)}
|
||||
|
||||
${callout('This activation link expires in <strong>24 hours</strong>.', 'warning')}
|
||||
|
||||
<p style="margin:24px 0 0 0;font-size:13px;color:#94a3b8">
|
||||
If you didn't expect this email, you can safely ignore it.
|
||||
</p>`;
|
||||
|
||||
const text = `Hi ${name || 'there'},\n\nActivate your ${org.name} account by visiting the link below:\n\n${activationUrl}\n\nThis link expires in 24 hours.\n\nIf you didn't expect this, ignore this email.\n\n${org.name} — ${org.email}`;
|
||||
return { text, html: emailWrapper(body, { preheader }) };
|
||||
}
|
||||
|
||||
function buildAccountClosedEmail({ name, dataDeleted }) {
|
||||
const org = getOrg();
|
||||
const preheader = `Your ${org.name} account has been closed.`;
|
||||
const body = `
|
||||
<p style="font-size:22px;font-weight:800;color:#0f172a;margin:0 0 8px 0;letter-spacing:-0.3px">Account closed</p>
|
||||
<p style="color:#64748b;font-size:14px;margin:0 0 32px 0">Confirmation of account closure</p>
|
||||
|
||||
<p style="margin:0 0 8px 0;color:#374151">Hi <strong>${name || 'there'}</strong>,</p>
|
||||
<p style="margin:0 0 16px 0;color:#374151">
|
||||
This email confirms that your <strong>${org.name}</strong> account has been successfully closed.
|
||||
</p>
|
||||
${dataDeleted
|
||||
? `<p style="margin:0 0 16px 0;color:#374151">All personal data associated with your account has been permanently deleted as requested.</p>`
|
||||
: `<p style="margin:0 0 16px 0;color:#374151">Your account has been deactivated. If you would also like your personal data permanently deleted, please contact us at <a href="mailto:${org.email}" style="color:#2563eb">${org.email}</a>.</p>`
|
||||
}
|
||||
<p style="margin:0 0 0 0;color:#374151">
|
||||
If you did not request this, please contact us immediately at
|
||||
<a href="mailto:${org.email}" style="color:#2563eb">${org.email}</a>.
|
||||
</p>`;
|
||||
|
||||
const dataNote = dataDeleted
|
||||
? 'All personal data has been permanently deleted.'
|
||||
: 'Your account has been deactivated. Contact us if you also want your data deleted.';
|
||||
const text = `Hi ${name || 'there'},\n\nYour ${org.name} account has been closed.\n\n${dataNote}\n\nIf you did not request this, contact us immediately at ${org.email}.\n\n${org.name}`;
|
||||
return { text, html: emailWrapper(body, { preheader }) };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendMail,
|
||||
emailWrapper,
|
||||
ctaButton,
|
||||
fallbackLink,
|
||||
divider,
|
||||
callout,
|
||||
paymentOption,
|
||||
buildPasswordResetEmail,
|
||||
buildPasswordChangedEmail,
|
||||
buildLoginNotificationEmail,
|
||||
buildWelcomeEmail,
|
||||
buildAccountActivationEmail,
|
||||
buildAccountClosedEmail,
|
||||
};
|
||||
Reference in New Issue
Block a user