diff --git a/mail-buddy.mjs b/mail-buddy.mjs index 23b75a9..852f1f6 100644 --- a/mail-buddy.mjs +++ b/mail-buddy.mjs @@ -1,9 +1,12 @@ #!/usr/bin/env node import { ImapFlow } from 'imapflow'; -import { readFileSync } from 'fs'; -import { execFileSync } from 'child_process'; +import { simpleParser } from 'mailparser'; +import { readFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { inspect } from 'node:util'; +import { setTimeout as sleep } from 'node:timers/promises'; -const CONFIG_PATH = process.env.MAIL_BUDDY_CONFIG ?? '/home/devilholk/.config/claude-mail-buddy/config.json'; +const CONFIG_PATH = process.env.MAIL_BUDDY_CONFIG ?? '/home/devilholk/.secrets/claude-mail-buddy.conf'; function load_config() { try { @@ -15,17 +18,17 @@ function load_config() { } function make_prompt(message) { - const from = message.envelope.from?.[0]?.address ?? 'unknown'; - const subject = message.envelope.subject ?? '(no subject)'; - const body = message.text ?? '(no body)'; + const from = message.from?.value?.[0]?.address ?? 'unknown'; + const subject = message.subject ?? '(no subject)'; + const body = message.html ?? message.text ?? '(no body)'; return [ - `[mail-buddy] New email from ${from}`, - `Subject: ${subject}`, + `[mail-buddy] You have received an email from ${from} with the subject "${subject}".`, + `Reply using ccc according to MEMORY:email_protocol.md`, ``, - body.trim(), - ``, - `Please reply to this email using the send-email CCC action, addressed to ${from} with an appropriate subject.`, + `--- Message body ---`, + body, + `--- End of message ---`, ].join('\n'); } @@ -39,29 +42,45 @@ function send_to_claude(text, config) { } async function handle_message(client, uid, config) { - const msg = await client.fetchOne(uid, { envelope: true, bodyStructure: true, source: true }); - if (!msg) { + const raw = await client.fetchOne(uid, { source: true }); + if (!raw?.source) { return; } - // Fetch plain text part - let text = ''; - for await (const part of client.fetch(uid, { bodyParts: ['text/plain'] })) { - text = part.bodyParts?.get('text/plain')?.toString('utf8') ?? ''; - } - msg.text = text; + const msg = await simpleParser(raw.source); + const sender = msg.from?.value?.[0]?.address ?? ''; - const sender = msg.envelope.from?.[0]?.address ?? ''; if (!sender.toLowerCase().includes(config.allowed_sender.toLowerCase())) { console.log(`Ignoring message from ${sender}`); return; } - console.log(`Dispatching message from ${sender}: ${msg.envelope.subject}`); const prompt = make_prompt(msg); send_to_claude(prompt, config); } +async function process_new_messages(client, config, last_uid) { + const search = last_uid + ? { uid: `${last_uid + 1}:*` } + : { unseen: true }; + + const uids = await client.search(search, { uid: true }); + let new_last_uid = last_uid; + + for (const uid of uids) { + if (uid <= last_uid) { + continue; + } + await handle_message(client, uid, config); + await client.messageFlagsAdd(uid, ['\\Seen'], { uid: true }); + if (uid > new_last_uid) { + new_last_uid = uid; + } + } + + return new_last_uid; +} + async function main() { const config = load_config(); @@ -82,23 +101,17 @@ async function main() { const lock = await client.getMailboxLock(config.imap.mailbox ?? 'INBOX'); try { - // Process any unseen messages already waiting - const unseen = await client.search({ unseen: true }); - for (const uid of unseen) { - await handle_message(client, uid, config); - } + // Get highest existing UID without processing old messages + const all_uids = await client.search({ all: true }, { uid: true }); + let last_uid = all_uids.length > 0 ? Math.max(...all_uids) : 0; + console.log(`Starting from UID ${last_uid}`); - console.log('Waiting for new mail (IDLE)...'); + const POLL_INTERVAL = 45_000; + console.log(`Polling every ${POLL_INTERVAL / 1000}s...`); - // IDLE loop — re-enters IDLE after each notification while (true) { - await client.idle(); - - const new_msgs = await client.search({ unseen: true }); - for (const uid of new_msgs) { - await handle_message(client, uid, config); - await client.messageFlagsAdd(uid, ['\\Seen']); - } + await sleep(POLL_INTERVAL); + last_uid = await process_new_messages(client, config, last_uid); } } finally { lock.release(); diff --git a/package.json b/package.json index 4fe29de..ad56dd9 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "type": "module", "dependencies": { - "imapflow": "^1.0.0" + "imapflow": "^1.0.0", + "mailparser": "^3.9.4" } -} \ No newline at end of file +}