220 lines
5.9 KiB
JavaScript
220 lines
5.9 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 (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();
|