Member updates: paced + retried Sendy opt-in checks (unknowns never cached as declines), explicit recipient list for repairs, recipients recorded on the send log
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+13
-6
@@ -14,10 +14,14 @@ function init(refs) { R = refs; }
|
||||
const AUDIENCES = { optin: 'Newsletter opt-ins (ticked the box at sign-up)', optin30: 'Newsletter opt-ins active in the last 30 days', all: 'Every member, including those who declined the newsletter' };
|
||||
// the sign-up checkbox subscribes the member to the Sendy newsletter list, so Sendy is the record of who opted in
|
||||
const optinCache = new Map(); // email -> { t, v }
|
||||
const KNOWN = ['Subscribed', 'Unsubscribed', 'Unconfirmed', 'Bounced', 'Soft bounced', 'Complained', 'Email does not exist in list'];
|
||||
let unknownOptins = 0; // answers Sendy did not give (timeouts, rate limits): counted, never treated as a decline for long
|
||||
async function optedIn(email) {
|
||||
const c = optinCache.get(email); if (c && Date.now() - c.t < 6 * 3600000) return c.v;
|
||||
const st = R.sendy ? await R.sendy.status(email) : ''; const v = st === 'Subscribed';
|
||||
optinCache.set(email, { t: Date.now(), v }); return v;
|
||||
const c = optinCache.get(email); if (c && Date.now() - c.t < (c.known ? 6 * 3600000 : 60000)) return c.v;
|
||||
let st = ''; for (let i = 0; i < 3 && !KNOWN.includes(st); i++) { if (i) await new Promise(r => setTimeout(r, 800 * i)); st = R.sendy ? await R.sendy.status(email) : ''; }
|
||||
await new Promise(r => setTimeout(r, 250)); // Sendy's status endpoint throttles bursts (2026-09-14: an unpaced pass under-counted 126 opt-ins as 92)
|
||||
const known = KNOWN.includes(st); if (!known) unknownOptins += 1;
|
||||
const v = st === 'Subscribed'; optinCache.set(email, { t: Date.now(), v, known }); return v;
|
||||
}
|
||||
async function recipients(kind) {
|
||||
const all = await R.accounts.listAll(20000);
|
||||
@@ -60,16 +64,19 @@ async function send(input, opts) {
|
||||
}
|
||||
if (running) return { error: 'A send is already running (' + running.sent + ' of ' + running.total + '). Wait for it to finish.' };
|
||||
const kind = AUDIENCES[input.audience] ? input.audience : 'optin';
|
||||
const list = await recipients(kind);
|
||||
unknownOptins = 0;
|
||||
let list = await recipients(kind);
|
||||
if (Array.isArray(input.to) && input.to.length) { const want = new Set(input.to.map(e => String(e).toLowerCase())); const all = await R.accounts.listAll(20000); list = all.filter(a => a.email && want.has(String(a.email).toLowerCase())); } // explicit list (repairs)
|
||||
if (unknownOptins && !(input.to && input.to.length)) console.log('updates: Sendy gave no answer for', unknownOptins, 'members; they were left out of this send');
|
||||
if (!list.length) return { error: 'Nobody in that audience.' };
|
||||
const rec = { id: Date.now().toString(36), ts: Date.now(), subject: compose(input, null).subject, noteIds, audience: kind, total: list.length, sent: 0, skipped: 0, failed: 0, status: 'running' };
|
||||
const rec = { id: Date.now().toString(36), ts: Date.now(), subject: compose(input, null).subject, noteIds, audience: kind, total: list.length, sent: 0, skipped: 0, failed: 0, status: 'running', unknown: unknownOptins, recipients: list.map(a => a.email) };
|
||||
const l = log(); l.sends.unshift(rec); l.sends = l.sends.slice(0, 50); saveLog(l); running = rec;
|
||||
(async () => {
|
||||
for (const acct of list) {
|
||||
try {
|
||||
if (R.drip.isUnsubscribed && await R.drip.isUnsubscribed(acct.email)) { rec.skipped += 1; continue; }
|
||||
const m = compose(input, acct);
|
||||
await R.mailer.send(acct.email, m.subject, m.text); rec.sent += 1;
|
||||
await R.mailer.send(acct.email, m.subject, m.text); rec.sent += 1; (rec.delivered = rec.delivered || []).push(acct.email);
|
||||
} catch (e) { rec.failed += 1; console.error('updates send', acct.email, e.message); }
|
||||
if ((rec.sent + rec.failed + rec.skipped) % 10 === 0) { const l2 = log(); const k = l2.sends.find(x => x.id === rec.id); if (k) Object.assign(k, rec); saveLog(l2); }
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
|
||||
Reference in New Issue
Block a user