import { ImapFlow } from 'imapflow'; import webpush from 'web-push'; import * as fs from 'fs'; import * as path from 'path'; const SUBS_FILE = path.resolve('/Users/ace/palacering/palaces/manglasabang/secretariat/keychain/push-subscriptions.json'); const SEEN_FILE = path.resolve('/Users/ace/palacering/palaceplatform/heartbeats/mail-push/.seen-uids.json'); const pass = process.env.PURELYMAIL_PASS_JUNWON; const vapidPublic = process.env.VAPID_PUBLIC_KEY; const vapidPrivate = process.env.VAPID_PRIVATE_KEY; if (!pass || !vapidPublic || !vapidPrivate) { console.error('Missing env: PURELYMAIL_PASS_JUNWON, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY'); process.exit(1); } webpush.setVapidDetails('mailto:ace@manglasabang.com', vapidPublic, vapidPrivate); const ACCOUNTS = [ 'junwon@manglasabang.com', 'junwon@palace.fund', 'junwon@palacering.com', ]; function loadSeen(): Record> { try { const raw = JSON.parse(fs.readFileSync(SEEN_FILE, 'utf8')); const result: Record> = {}; for (const [k, v] of Object.entries(raw)) result[k] = new Set(v as number[]); return result; } catch { return {}; } } function saveSeen(seen: Record>) { const raw: Record = {}; for (const [k, v] of Object.entries(seen)) raw[k] = [...v].slice(-500); fs.writeFileSync(SEEN_FILE, JSON.stringify(raw)); } function loadSubs(): object[] { try { return JSON.parse(fs.readFileSync(SUBS_FILE, 'utf8')); } catch { return []; } } async function sendPush(title: string, body: string, tag: string) { const subs = loadSubs(); if (subs.length === 0) return; const payload = JSON.stringify({ title, body, tag, url: '/mail/' }); const deadSubs: string[] = []; for (const sub of subs as any[]) { try { await webpush.sendNotification(sub, payload); } catch (e: any) { if (e.statusCode === 410 || e.statusCode === 404) deadSubs.push(sub.endpoint); } } if (deadSubs.length > 0) { const alive = subs.filter((s: any) => !deadSubs.includes(s.endpoint)); fs.writeFileSync(SUBS_FILE, JSON.stringify(alive, null, 2)); } } async function checkAccount(email: string, seen: Record>) { if (!seen[email]) seen[email] = new Set(); const client = new ImapFlow({ host: 'imap.purelymail.com', port: 993, secure: true, auth: { user: email, pass: pass! }, logger: false, }); try { await client.connect(); const lock = await client.getMailboxLock('INBOX'); try { const uids = (await client.search({ unseen: true }, { uid: true })) || []; const newUids = uids.filter(uid => !seen[email].has(uid)); if (newUids.length === 0) return; for (const uid of newUids) { seen[email].add(uid); try { const msg = await client.fetchOne(String(uid), { envelope: true }, { uid: true }); const subject = msg?.envelope?.subject || '(no subject)'; const from = msg?.envelope?.from?.[0]; const fromStr = from ? (from.name || `${from.mailbox}@${from.host}`) : 'Someone'; const domain = email.split('@')[1]; await sendPush( `New mail — ${domain}`, `${fromStr}: ${subject}`, `mail-${email}-${uid}` ); } catch { seen[email].add(uid); } } } finally { lock.release(); } } catch (e) { console.error(`[mail-push] ${email} error:`, e); } finally { await client.logout().catch(() => {}); } } async function run() { const seen = loadSeen(); for (const email of ACCOUNTS) { await checkAccount(email, seen); } saveSeen(seen); console.log(`[mail-push] done — ${new Date().toISOString()}`); } run().catch(e => { console.error('[mail-push] fatal:', e); process.exit(1); });