diff --git a/src/lib/mail/imap.ts b/src/lib/mail/imap.ts index 4e9d638..a5783d9 100644 --- a/src/lib/mail/imap.ts +++ b/src/lib/mail/imap.ts @@ -38,6 +38,7 @@ function extractBody(parsed: ParsedMail): string { async function handleRawMessage(source: Buffer): Promise { const parsed = await simpleParser(source); const from = parsed.from?.value[0]; + console.log(`[mail] parsed message from=${from?.address ?? "(none)"} subject=${JSON.stringify(parsed.subject)}`); if (!from?.address) return; // no return address — nothing to reply to, skip const referencedIds = [ @@ -59,13 +60,28 @@ async function processUnseenMessages(client: ImapFlow): Promise { const lock = await client.getMailboxLock("INBOX"); try { const uids = await client.search({ seen: false }, { uid: true }); - if (!uids || uids.length === 0) return; + if (!uids || uids.length === 0) { + console.log("[mail] unseen check: 0 messages"); + return; + } + console.log(`[mail] unseen check: ${uids.length} message(s)`); - for await (const message of client.fetch(uids, { source: true, uid: true })) { - if (message.source) { - await handleRawMessage(message.source); + // One at a time, each in its own try/catch — a message that vanishes + // between search and fetch (seen once with a real mail server) must not + // abort processing of the rest of the batch. + for (const uid of uids) { + try { + const message = await client.fetchOne(uid, { source: true }, { uid: true }); + if (message && message.source) { + await handleRawMessage(message.source); + console.log(`[mail] processed message uid=${uid}`); + } else { + console.warn(`[mail] message uid=${uid} had no source (already gone?)`); + } + await client.messageFlagsAdd(uid, ["\\Seen"], { uid: true }); + } catch (err) { + console.error(`[mail] failed to process message uid=${uid}:`, err); } - await client.messageFlagsAdd(message.uid, ["\\Seen"], { uid: true }); } } finally { lock.release(); @@ -75,12 +91,20 @@ async function processUnseenMessages(client: ImapFlow): Promise { async function runIdleLoop(client: ImapFlow): Promise { try { await client.mailboxOpen("INBOX"); + console.log("[mail] IDLE listener connected, watching INBOX"); await processUnseenMessages(client); + // New mail is signaled by the 'exists' event while IDLE is active — it's + // independent of when idle() itself resolves (that happens on periodic + // re-idle / connection events, not per message). Handle new mail here; + // use the loop below purely to keep re-entering IDLE mode. + client.on("exists", () => { + console.log("[mail] 'exists' event fired — checking for new mail"); + processUnseenMessages(client).catch((err) => console.error("Error processing new mail:", err)); + }); + while (state.client === client) { await client.idle(); - if (state.client !== client) break; - await processUnseenMessages(client); } } catch (err) { if (state.client === client) {