Files
openclaw-backups/scripts/imap-idle-monitor.js

220 lines
6.0 KiB
JavaScript

#!/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 from skill directory
const Imap = require(path.resolve(__dirname, '../skills/imap-smtp-email/node_modules/imap'));
const { simpleParser } = require(path.resolve(__dirname, '../skills/imap-smtp-email/node_modules/mailparser'));
require('dotenv').config({ path: path.resolve(__dirname, '../skills/imap-smtp-email/.env.krillyclaw') });
const STATE_FILE = path.resolve(__dirname, '../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();