#!/usr/bin/env node import { ImapFlow } from 'imapflow'; import { readFileSync } from 'fs'; import { execFileSync } from 'child_process'; const CONFIG_PATH = process.env.MAIL_BUDDY_CONFIG ?? '/home/devilholk/.config/claude-mail-buddy/config.json'; 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.envelope.from?.[0]?.address ?? 'unknown'; const subject = message.envelope.subject ?? '(no subject)'; const body = message.text ?? '(no body)'; return [ `[mail-buddy] New email from ${from}`, `Subject: ${subject}`, ``, body.trim(), ``, `Please reply to this email using the send-email CCC action, addressed to ${from} with an appropriate subject.`, ].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 msg = await client.fetchOne(uid, { envelope: true, bodyStructure: true, source: true }); if (!msg) { 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 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 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 { // Process any unseen messages already waiting const unseen = await client.search({ unseen: true }); for (const uid of unseen) { await handle_message(client, uid, config); } console.log('Waiting for new mail (IDLE)...'); // 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']); } } } finally { lock.release(); await client.logout(); } } main().catch(err => { console.error(err); process.exit(1); });