import { ImapFlow } from 'imapflow'; export type Account = 'yahoo' | 'fetcherpay'; function getConfig(account: Account = 'yahoo') { if (account === 'fetcherpay') { return { host: process.env['FETCHERPAY_IMAP_HOST'] ?? 'mail.fetcherpay.com', port: parseInt(process.env['FETCHERPAY_IMAP_PORT'] ?? '30993'), secure: true, auth: { user: process.env['FETCHERPAY_EMAIL'] as string, pass: process.env['FETCHERPAY_PASSWORD'] as string, }, tls: { rejectUnauthorized: false }, // self-signed cert on self-hosted server }; } return { host: 'imap.mail.yahoo.com', port: 993, secure: true, auth: { user: process.env['YAHOO_EMAIL'] as string, pass: process.env['YAHOO_APP_PASSWORD'] as string, }, }; } async function withClient(account: Account, fn: (client: ImapFlow) => Promise): Promise { const client = new ImapFlow(getConfig(account)); await client.connect(); try { return await fn(client); } finally { await client.logout(); } } export interface MessageSummary { uid: number; messageId: string; subject: string; from: string; date: string; seen: boolean; size: number; } export interface FullMessage { uid: number; messageId: string; subject: string; from: string; to: string; date: string; body: string; seen: boolean; } export async function searchMessages(query: string, maxResults = 20, account: Account = 'yahoo'): Promise { return withClient(account, async (client) => { await client.mailboxOpen('INBOX'); const criteria = query ? { or: [{ subject: query }, { from: query }] } : { all: true }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const uids = await client.search(criteria as any, { uid: true }); const uidList: number[] = Array.isArray(uids) ? uids : []; const recentUids = uidList.slice(-maxResults).reverse(); if (recentUids.length === 0) return []; const messages: MessageSummary[] = []; for await (const msg of client.fetch(recentUids, { envelope: true, flags: true, size: true, }, { uid: true })) { const env = msg.envelope; messages.push({ uid: msg.uid, messageId: env?.messageId ?? '', subject: env?.subject ?? '(no subject)', from: env?.from?.[0] ? `${env.from[0].name ?? ''} <${env.from[0].address ?? ''}>`.trim() : '', date: env?.date?.toISOString() ?? '', seen: msg.flags?.has('\\Seen') ?? false, size: msg.size ?? 0, }); } return messages; }); } export async function readMessage(uid: number, account: Account = 'yahoo'): Promise { return withClient(account, async (client) => { console.log(`[imap] readMessage uid=${uid} account=${account}`); await client.mailboxOpen('INBOX'); console.log(`[imap] mailbox opened, fetching uid=${uid}`); let result: FullMessage | null = null; for await (const msg of client.fetch([uid], { envelope: true, flags: true, bodyParts: ['TEXT'], }, { uid: true })) { const env = msg.envelope; console.log(`[imap] got msg uid=${msg.uid} subject="${env?.subject}"`); const bpKeys = msg.bodyParts ? [...msg.bodyParts.keys()] : []; console.log(`[imap] bodyParts keys:`, JSON.stringify(bpKeys)); const textBuf = msg.bodyParts?.get('text') ?? msg.bodyParts?.get('TEXT') ?? msg.bodyParts?.get('1'); console.log(`[imap] textBuf length=${textBuf ? textBuf.length : 'null'}`); const rawBody = textBuf ? textBuf.toString('utf-8') : ''; const body = rawBody .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') .trim() .slice(0, 10000); console.log(`[imap] body length after strip=${body.length}`); result = { uid: msg.uid, messageId: env?.messageId ?? '', subject: env?.subject ?? '(no subject)', from: env?.from?.[0] ? `${env.from[0].name ?? ''} <${env.from[0].address ?? ''}>`.trim() : '', to: env?.to?.[0]?.address ?? '', date: env?.date?.toISOString() ?? '', body, seen: true, }; } if (!result) throw new Error(`Message UID ${uid} not found`); // Mark as seen AFTER the fetch loop fully completes — calling messageFlagsAdd // inside the for-await loop deadlocks because the FETCH command is still active. console.log(`[imap] marking uid=${uid} as seen`); await client.messageFlagsAdd([uid], ['\\Seen'], { uid: true }); console.log(`[imap] readMessage done uid=${uid}`); return result; }); } export async function getProfile(account: Account = 'yahoo'): Promise<{ email: string; name: string; account: string }> { const email = account === 'fetcherpay' ? (process.env['FETCHERPAY_EMAIL'] ?? '') : (process.env['YAHOO_EMAIL'] ?? ''); return { email, name: email.split('@')[0], account }; } export async function listFolders(account: Account = 'yahoo'): Promise { return withClient(account, async (client) => { const mailboxes = await client.list(); return mailboxes.map((m) => m.path); }); }