From 06d8062a5afc058b757ebada9650adaff1fdb7d1 Mon Sep 17 00:00:00 2001 From: Sami Date: Wed, 19 Aug 2026 12:49:44 -0700 Subject: [PATCH] Add email delivery for license keys (SMTP) --- src/email.js | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/email.js diff --git a/src/email.js b/src/email.js new file mode 100644 index 0000000..dcd5e39 --- /dev/null +++ b/src/email.js @@ -0,0 +1,107 @@ +import crypto from 'crypto'; +import nodemailer from 'nodemailer'; + +const LICENSE_SECRET = process.env.DASHCADDY_LICENSE_SECRET || ''; + +const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Crockford-ish (no I, O, 0, 1) + +/** + * Generate a DashCaddy license code in the format `DC-XXXX-XXXX-XXXX-XXXX`. + * Deterministic for the same (licenseKey, durationDays) tuple — if a customer + * pays again, regenerate the code with the same key + new duration, and the + * code stays the same. That's what "the same key gets more time" means. + */ +export function generateLicenseCode(licenseKey, durationDays) { + if (!LICENSE_SECRET) { + throw new Error('DASHCADDY_LICENSE_SECRET not set'); + } + const seed = crypto + .createHmac('sha256', LICENSE_SECRET) + .update(`${licenseKey}|${durationDays}`) + .digest(); + const segments = []; + for (let i = 0; i < seed.length && segments.length < 4; i += 2) { + const b1 = seed[i]; + const b2 = seed[i + 1] || 0; + segments.push( + ALPHABET[b1 % 32] + + ALPHABET[Math.floor(b1 / 32) % 32] + + ALPHABET[b2 % 32] + + ALPHABET[Math.floor(b2 / 32) % 32] + ); + } + return 'DC-' + segments.join('-'); +} + +let transporter = null; +function getTransporter() { + if (transporter) return transporter; + transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST || '127.0.0.1', + port: parseInt(process.env.SMTP_PORT || '25', 10), + secure: process.env.SMTP_SECURE === 'true', + auth: process.env.SMTP_USERNAME ? { + user: process.env.SMTP_USERNAME, + pass: process.env.SMTP_PASSWORD + } : undefined, + tls: { rejectUnauthorized: false } + }); + return transporter; +} + +function formatDate(iso) { + if (!iso) return 'N/A'; + return new Date(iso).toUTCString(); +} + +/** + * Send the license key email to the customer. Called after every payment + * (initial purchase or renewal extension). + */ +export async function sendLicenseEmail({ to, code, durationDays, planCode, expiresAt, extended = false }) { + const subject = extended + ? `Your DashCaddy Premium license has been extended (${durationDays} days added)` + : `Your DashCaddy Premium license key`; + const text = [ + extended ? 'Your DashCaddy Premium license has been extended.' : 'Thank you for purchasing DashCaddy Premium.', + '', + `License key: ${code}`, + `Plan: ${planCode}`, + `License valid until: ${formatDate(expiresAt)}`, + '', + 'To activate: paste this license key into your DashCaddy dashboard at Admin → License.', + '', + 'Need help? Reply to this email or visit https://dashcaddy.net/about' + ].join('\n'); + const html = ` +
+

${extended ? 'License extended' : 'Welcome to DashCaddy Premium'}

+

${extended + ? 'Your DashCaddy Premium license has been extended. Same key, more time.' + : 'Thank you for purchasing DashCaddy Premium. Your license key is below.'}

+
+ ${code} +
+

Plan: ${planCode}

+

License valid until: ${formatDate(expiresAt)}

+

To activate: paste this license key into your DashCaddy dashboard at Admin → License.

+
+

Need help? Reply to this email or visit dashcaddy.net/about

+
+ `; + + try { + const t = getTransporter(); + await t.sendMail({ + from: process.env.SMTP_FROM || 'licenses@dashcaddy.net', + to, + subject, + text, + html + }); + return { delivered: true, via: 'smtp' }; + } catch (err) { + console.error('Email delivery failed:', err.message); + return { delivered: false, error: err.message }; + } +}