#!/usr/bin/env node /** * IMAP IDLE Monitor for krillyclaw@gmail.com * Uses IMAP IDLE (RFC 2177) for real-time push notifications */ const path = require('path'); const https = require('https'); // Load imap modules (installed in this directory) const Imap = require('imap'); const { simpleParser } = require('mailparser'); require('dotenv').config({ path: path.resolve(__dirname, '../.env.krillyclaw') }); const STATE_FILE = path.resolve(__dirname, '../../../workspace/memory/.krillyclaw-imap-state.json'); const GATEWAY_URL = 'http://127.0.0.1:18789/api/message/send'; let lastUid = 0; function loadState() { try { const fs = require('fs'); if (fs.existsSync(STATE_FILE)) { const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); lastUid = state.last_uid || 0; } } catch (e) { console.error('Error loading state:', e.message); } } function saveState() { try { const fs = require('fs'); fs.writeFileSync(STATE_FILE, JSON.stringify({ last_uid: lastUid, last_check: Date.now() })); } catch (e) { console.error('Error saving state:', e.message); } } function sendAlert(subject, from) { const message = `šŸ“¬ **New email in krillyclaw@gmail.com**:\n\n• **${subject}**\n From: ${from}\n\n— Krilly šŸ¦€`; const data = JSON.stringify({ channel: 'telegram', to: 'telegram:1793951355', message: message }); const http = require('http'); const req = http.request(GATEWAY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => { console.log('Alert sent:', res.statusCode); }); req.on('error', (e) => console.error('Alert failed:', e.message)); req.write(data); req.end(); } function createImap() { return new Imap({ user: process.env.IMAP_USER, password: process.env.IMAP_PASS, host: process.env.IMAP_HOST, port: parseInt(process.env.IMAP_PORT), tls: process.env.IMAP_TLS === 'true', tlsOptions: { rejectUnauthorized: false }, connTimeout: 60000, authTimeout: 10000 }); } function fetchNewMessages(imap, box) { return new Promise((resolve, reject) => { const searchCriteria = ['UNSEEN']; const fetchOptions = { bodies: ['HEADER.FIELDS (FROM SUBJECT)'], struct: true }; imap.search(searchCriteria, (err, results) => { if (err) { reject(err); return; } if (!results || results.length === 0) { resolve([]); return; } const fetch = imap.fetch(results, fetchOptions); const messages = []; fetch.on('message', (msg, seqno) => { let header = {}; let uid = 0; msg.on('body', (stream) => { let buffer = ''; stream.on('data', (chunk) => buffer += chunk); stream.on('end', () => { header = Imap.parseHeader(buffer); }); }); msg.once('attributes', (attrs) => { uid = attrs.uid; }); msg.once('end', () => { messages.push({ uid, subject: header.subject ? header.subject[0] : 'No subject', from: header.from ? header.from[0] : 'Unknown' }); }); }); fetch.once('error', reject); fetch.once('end', () => resolve(messages)); }); }); } async function monitor() { loadState(); console.log(`[${new Date().toISOString()}] Starting IMAP IDLE monitor for ${process.env.IMAP_USER}`); console.log(`[${new Date().toISOString()}] Last seen UID: ${lastUid}`); const imap = createImap(); imap.once('ready', () => { imap.openBox('INBOX', false, async (err, box) => { if (err) { console.error('Error opening inbox:', err); return; } console.log(`[${new Date().toISOString()}] Connected to INBOX, watching for new emails...`); // Fetch initial unread messages try { const messages = await fetchNewMessages(imap, box); let newCount = 0; for (const msg of messages) { if (msg.uid > lastUid) { newCount++; lastUid = msg.uid; console.log(`[${new Date().toISOString()}] New message: ${msg.subject} (UID: ${msg.uid})`); sendAlert(msg.subject, msg.from); } } if (newCount > 0) { saveState(); } } catch (e) { console.error('Error fetching messages:', e); } // Set up IDLE mode imap.on('mail', async (numNewMsgs) => { console.log(`[${new Date().toISOString()}] ${numNewMsgs} new message(s) received`); try { const messages = await fetchNewMessages(imap, box); for (const msg of messages) { if (msg.uid > lastUid) { lastUid = msg.uid; console.log(`[${new Date().toISOString()}] New message: ${msg.subject} (UID: ${msg.uid})`); sendAlert(msg.subject, msg.from); saveState(); } } } catch (e) { console.error('Error fetching new messages:', e); } }); imap.on('update', (seqno, info) => { console.log(`[${new Date().toISOString()}] Update: seqno=${seqno}`); }); imap.on('expunge', (seqno) => { console.log(`[${new Date().toISOString()}] Expunge: seqno=${seqno}`); }); }); }); imap.once('error', (err) => { console.error(`[${new Date().toISOString()}] IMAP error:`, err.message); // Reconnect after 30 seconds setTimeout(monitor, 30000); }); imap.once('end', () => { console.log(`[${new Date().toISOString()}] Connection ended, reconnecting in 30s...`); setTimeout(monitor, 30000); }); imap.connect(); } // Handle graceful shutdown process.on('SIGINT', () => { console.log('\nShutting down...'); process.exit(0); }); process.on('SIGTERM', () => { console.log('\nShutting down...'); process.exit(0); }); monitor();