0374b04f0d
This is a test area on a testnet contract, but it was seeded on 15 Sep with a copy of InstantAdPay's live member list and it carried a working SendGrid key. On 18 Sep the coach's stall nudges fired on schedule and emailed 95 real people from it. One of them clicked through and opened a fresh account two hours later. Nothing was misconfigured. Every send site checked mailer.hasKey(), the key was there, so every send site got a yes. The default was wrong, not the plumbing. OUTBOUND=on is now required before anything can leave: hasKey() is false without it, send() rejects outright, and the two mailing loops (coach nudges, lead drip) never start. SIGNUPS=closed shuts the three registration doors and reports signupsOpen:false, which also drops the email-code card from the UI. Members already here keep their dashboards. A missing env var means silence now, not delivery. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
53 lines
2.3 KiB
JavaScript
53 lines
2.3 KiB
JavaScript
// Outbound mail via SendGrid v3 (domain-authenticated linkspin-test.saasy.top).
|
|
// Key sources: SENDGRID_KEY env, else DATA_DIR/sendgrid.key in the volume.
|
|
// No key = email sign-in stays feature-flagged off in production.
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const https = require('https');
|
|
|
|
let DATA_DIR = null;
|
|
const FROM = { email: 'no-reply@linkspin-test.saasy.top', name: 'LinkSpin' };
|
|
|
|
function init(opts) { DATA_DIR = opts.dataDir; }
|
|
function key() {
|
|
if (process.env.SENDGRID_KEY) return process.env.SENDGRID_KEY.trim();
|
|
try { return fs.readFileSync(path.join(DATA_DIR, 'sendgrid.key'), 'utf8').trim(); } catch (e) { return ''; }
|
|
}
|
|
// A deployment must opt IN to sending mail. Default-deny, on purpose: on 2026-09-18 this
|
|
// test area mailed 95 real people because a seeded member list and a live key were both
|
|
// present and every send site dutifully checked hasKey() and got a yes.
|
|
const OUTBOUND = process.env.OUTBOUND === 'on';
|
|
function hasKey() { return OUTBOUND && !!key(); }
|
|
|
|
function send(to, subject, text) {
|
|
if (!OUTBOUND) return Promise.reject(new Error('outbound mail is off on this deployment (set OUTBOUND=on to allow it)'));
|
|
return new Promise((resolve, reject) => {
|
|
const body = JSON.stringify({
|
|
personalizations: [{ to: [{ email: to }] }],
|
|
from: FROM,
|
|
subject,
|
|
content: [{ type: 'text/plain', value: text }]
|
|
});
|
|
const req = https.request({ hostname: 'api.sendgrid.com', path: '/v3/mail/send', method: 'POST',
|
|
headers: { Authorization: 'Bearer ' + key(), 'Content-Type': 'application/json',
|
|
'Content-Length': Buffer.byteLength(body) }, timeout: 15000 },
|
|
res => {
|
|
let d = '';
|
|
res.on('data', c => d += c);
|
|
res.on('end', () => res.statusCode < 300 ? resolve(true) : reject(new Error('sendgrid ' + res.statusCode + ': ' + d.slice(0, 200))));
|
|
});
|
|
req.on('error', reject);
|
|
req.on('timeout', () => req.destroy(new Error('sendgrid timeout')));
|
|
req.end(body);
|
|
});
|
|
}
|
|
|
|
function sendCode(to, code) {
|
|
return send(to, code + ' is your LinkSpin sign-in code',
|
|
'Your sign-in code is: ' + code + '\n\n'
|
|
+ 'It works for 15 minutes. If you did not request it, ignore this email.\n\n'
|
|
+ 'LinkSpin\nAdvertise and earn instantly. Locked in code, not promises.\nhttps://linkspin-test.saasy.top');
|
|
}
|
|
|
|
module.exports = { init, hasKey, send, sendCode };
|