Files
claude-mail-buddy/mail-buddy.js

113 lines
2.9 KiB
JavaScript

#!/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);
});