#!/usr/bin/env node import { ImapFlow } from 'imapflow'; 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/.secrets/claude-mail-buddy.conf'; function load_config() { try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf8')); } catch (err) { console.error(`Failed to load config from ${CONFIG_PATH}: ${err.message}`); process.exit(1); } } function make_prompt(message) { 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] You have received an email from ${from} with the subject "${subject}".`, `Reply using ccc according to MEMORY:email_protocol.md`, ``, `--- Message body ---`, body, `--- End of message ---`, ].join('\n'); } function send_to_claude(text, config) { const window_id = String(config.claude_window_id); execFileSync('node', [config.claude_remote_path, '--window-id', window_id], { input: text, encoding: 'utf8', stdio: ['pipe', 'inherit', 'inherit'], }); } async function handle_message(client, uid, config) { const raw = await client.fetchOne(uid, { source: true }); if (!raw?.source) { return; } const msg = await simpleParser(raw.source); const sender = msg.from?.value?.[0]?.address ?? ''; if (!sender.toLowerCase().includes(config.allowed_sender.toLowerCase())) { console.log(`Ignoring message from ${sender}`); return; } 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(); const client = new ImapFlow({ host: config.imap.host, port: config.imap.port ?? 993, secure: config.imap.secure ?? true, auth: { user: config.imap.user, pass: config.imap.pass, }, logger: false, }); await client.connect(); console.log(`Connected to ${config.imap.host}`); const lock = await client.getMailboxLock(config.imap.mailbox ?? 'INBOX'); try { // 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}`); const POLL_INTERVAL = 45_000; console.log(`Polling every ${POLL_INTERVAL / 1000}s...`); while (true) { await sleep(POLL_INTERVAL); last_uid = await process_new_messages(client, config, last_uid); } } finally { lock.release(); await client.logout(); } } main().catch(err => { console.error(err); process.exit(1); });