364 lines
12 KiB
JavaScript
364 lines
12 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* TaskPing 邮件检查器
|
|||
|
|
* 更强大的邮件搜索和内容提取工具
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const Imap = require('node-imap');
|
|||
|
|
const { simpleParser } = require('mailparser');
|
|||
|
|
const { spawn } = require('child_process');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
class EmailChecker {
|
|||
|
|
constructor() {
|
|||
|
|
this.configPath = path.join(__dirname, 'config/channels.json');
|
|||
|
|
this.config = null;
|
|||
|
|
this.imap = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async start() {
|
|||
|
|
console.log('🔍 TaskPing 强化邮件检查器启动\n');
|
|||
|
|
|
|||
|
|
// 加载配置
|
|||
|
|
if (!this.loadConfig()) {
|
|||
|
|
console.log('❌ 请先配置邮件: npm run config');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`📧 邮箱: ${this.config.imap.auth.user}`);
|
|||
|
|
console.log(`📬 通知发送到: ${this.config.to}\n`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await this.connectToEmail();
|
|||
|
|
await this.comprehensiveEmailCheck();
|
|||
|
|
await this.startContinuousMonitoring();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 启动失败:', error.message);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loadConfig() {
|
|||
|
|
try {
|
|||
|
|
const data = fs.readFileSync(this.configPath, 'utf8');
|
|||
|
|
const config = JSON.parse(data);
|
|||
|
|
|
|||
|
|
if (!config.email?.enabled) {
|
|||
|
|
console.log('❌ 邮件功能未启用');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.config = config.email.config;
|
|||
|
|
return true;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.log('❌ 配置文件读取失败');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async connectToEmail() {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
this.imap = new Imap({
|
|||
|
|
user: this.config.imap.auth.user,
|
|||
|
|
password: this.config.imap.auth.pass,
|
|||
|
|
host: this.config.imap.host,
|
|||
|
|
port: this.config.imap.port,
|
|||
|
|
tls: this.config.imap.secure,
|
|||
|
|
connTimeout: 60000,
|
|||
|
|
authTimeout: 30000,
|
|||
|
|
keepalive: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.imap.once('ready', () => {
|
|||
|
|
console.log('✅ IMAP连接成功');
|
|||
|
|
resolve();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.imap.once('error', reject);
|
|||
|
|
this.imap.connect();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async comprehensiveEmailCheck() {
|
|||
|
|
console.log('\n🔍 开始全面邮件检查...\n');
|
|||
|
|
|
|||
|
|
await this.openInbox();
|
|||
|
|
|
|||
|
|
// 1. 检查最近24小时所有邮件(不仅是未读)
|
|||
|
|
console.log('📅 1. 检查最近24小时所有邮件...');
|
|||
|
|
await this.searchEmails('24h', false);
|
|||
|
|
|
|||
|
|
// 2. 检查最近1小时未读邮件
|
|||
|
|
console.log('\n📧 2. 检查最近1小时未读邮件...');
|
|||
|
|
await this.searchEmails('1h', true);
|
|||
|
|
|
|||
|
|
// 3. 检查主题包含特定关键词的邮件
|
|||
|
|
console.log('\n🎯 3. 检查TaskPing相关邮件...');
|
|||
|
|
await this.searchBySubject();
|
|||
|
|
|
|||
|
|
// 4. 检查来自特定发件人的邮件
|
|||
|
|
console.log('\n👤 4. 检查来自目标邮箱的邮件...');
|
|||
|
|
await this.searchByFrom();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async openInbox() {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
this.imap.openBox('INBOX', false, (err, box) => {
|
|||
|
|
if (err) {
|
|||
|
|
reject(err);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
console.log(`📫 收件箱: 总计${box.messages.total}封邮件`);
|
|||
|
|
resolve(box);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async searchEmails(timeRange, unseenOnly = false) {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
const since = new Date();
|
|||
|
|
if (timeRange === '1h') {
|
|||
|
|
since.setHours(since.getHours() - 1);
|
|||
|
|
} else if (timeRange === '24h') {
|
|||
|
|
since.setDate(since.getDate() - 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let searchCriteria = [['SINCE', since]];
|
|||
|
|
if (unseenOnly) {
|
|||
|
|
searchCriteria.push(['UNSEEN']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`🔍 搜索条件: ${timeRange}, ${unseenOnly ? '仅未读' : '全部'}`);
|
|||
|
|
|
|||
|
|
this.imap.search(searchCriteria, (err, results) => {
|
|||
|
|
if (err) {
|
|||
|
|
console.error('❌ 搜索失败:', err.message);
|
|||
|
|
resolve();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`📨 找到 ${results.length} 封邮件`);
|
|||
|
|
|
|||
|
|
if (results.length > 0) {
|
|||
|
|
this.analyzeEmails(results.slice(-5)); // 只分析最新5封
|
|||
|
|
}
|
|||
|
|
resolve();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async searchBySubject() {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
// 搜索主题包含Re:或TaskPing的邮件
|
|||
|
|
this.imap.search([['OR', ['SUBJECT', 'Re:'], ['SUBJECT', 'TaskPing']]], (err, results) => {
|
|||
|
|
if (err) {
|
|||
|
|
console.error('❌ 主题搜索失败:', err.message);
|
|||
|
|
resolve();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`📨 找到 ${results.length} 封相关邮件`);
|
|||
|
|
|
|||
|
|
if (results.length > 0) {
|
|||
|
|
this.analyzeEmails(results.slice(-3)); // 只分析最新3封
|
|||
|
|
}
|
|||
|
|
resolve();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async searchByFrom() {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
// 搜索来自目标邮箱的邮件
|
|||
|
|
const targetEmail = this.config.to;
|
|||
|
|
|
|||
|
|
this.imap.search([['FROM', targetEmail]], (err, results) => {
|
|||
|
|
if (err) {
|
|||
|
|
console.error('❌ 发件人搜索失败:', err.message);
|
|||
|
|
resolve();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`📨 找到来自 ${targetEmail} 的 ${results.length} 封邮件`);
|
|||
|
|
|
|||
|
|
if (results.length > 0) {
|
|||
|
|
this.analyzeEmails(results.slice(-3)); // 只分析最新3封
|
|||
|
|
}
|
|||
|
|
resolve();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
analyzeEmails(emailUids) {
|
|||
|
|
const fetch = this.imap.fetch(emailUids, {
|
|||
|
|
bodies: '',
|
|||
|
|
markSeen: false // 不标记为已读
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
fetch.on('message', (msg, seqno) => {
|
|||
|
|
console.log(`\n📧 分析邮件 ${seqno}:`);
|
|||
|
|
let buffer = '';
|
|||
|
|
|
|||
|
|
msg.on('body', (stream) => {
|
|||
|
|
stream.on('data', (chunk) => {
|
|||
|
|
buffer += chunk.toString('utf8');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
stream.once('end', async () => {
|
|||
|
|
try {
|
|||
|
|
const parsed = await simpleParser(buffer);
|
|||
|
|
await this.processEmailContent(parsed, seqno);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`❌ 解析邮件 ${seqno} 失败:`, error.message);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async processEmailContent(email, seqno) {
|
|||
|
|
console.log(` 📄 主题: ${email.subject || '(无主题)'}`);
|
|||
|
|
console.log(` 👤 发件人: ${email.from?.text || '(未知)'}`);
|
|||
|
|
console.log(` 📅 时间: ${email.date || '(未知)'}`);
|
|||
|
|
|
|||
|
|
// 判断是否是潜在的回复邮件
|
|||
|
|
const isPotentialReply = this.isPotentialReply(email);
|
|||
|
|
console.log(` 🎯 潜在回复: ${isPotentialReply ? '是' : '否'}`);
|
|||
|
|
|
|||
|
|
if (isPotentialReply) {
|
|||
|
|
const command = this.extractCommand(email);
|
|||
|
|
console.log(` 💬 邮件内容长度: ${(email.text || '').length} 字符`);
|
|||
|
|
console.log(` 📝 提取的内容:\n"${command.substring(0, 200)}${command.length > 200 ? '...' : ''}"`);
|
|||
|
|
|
|||
|
|
if (command && command.trim().length > 0) {
|
|||
|
|
console.log(`\n🎉 发现有效命令! (邮件 ${seqno})`);
|
|||
|
|
await this.handleCommand(command, seqno);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isPotentialReply(email) {
|
|||
|
|
const subject = email.subject || '';
|
|||
|
|
const from = email.from?.text || '';
|
|||
|
|
const targetEmail = this.config.to;
|
|||
|
|
|
|||
|
|
// 检查多种条件
|
|||
|
|
return (
|
|||
|
|
subject.includes('[TaskPing]') ||
|
|||
|
|
subject.match(/^(Re:|RE:|回复:)/i) ||
|
|||
|
|
from.includes(targetEmail) ||
|
|||
|
|
(email.text && email.text.length > 10) // 有实际内容
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extractCommand(email) {
|
|||
|
|
let text = email.text || '';
|
|||
|
|
const lines = text.split('\n');
|
|||
|
|
const commandLines = [];
|
|||
|
|
|
|||
|
|
for (const line of lines) {
|
|||
|
|
// 停止处理当遇到原始邮件标记
|
|||
|
|
if (line.includes('-----Original Message-----') ||
|
|||
|
|
line.includes('--- Original Message ---') ||
|
|||
|
|
(line.includes('在') && line.includes('写道:')) ||
|
|||
|
|
(line.includes('On') && line.includes('wrote:')) ||
|
|||
|
|
line.match(/^>\s*/) ||
|
|||
|
|
line.includes('会话ID:') ||
|
|||
|
|
line.includes('TaskPing <') ||
|
|||
|
|
line.match(/\d{4}年\d{1,2}月\d{1,2}日/)) {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 跳过签名和空行
|
|||
|
|
if (line.includes('--') ||
|
|||
|
|
line.includes('Sent from') ||
|
|||
|
|
line.includes('发自我的') ||
|
|||
|
|
line.trim() === '') {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
commandLines.push(line);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return commandLines.join('\n').trim();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleCommand(command, seqno) {
|
|||
|
|
console.log(`\n🚀 处理命令 (来自邮件 ${seqno}):`);
|
|||
|
|
console.log(`📝 命令内容: "${command}"`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 复制到剪贴板
|
|||
|
|
await this.copyToClipboard(command);
|
|||
|
|
console.log('✅ 命令已复制到剪贴板');
|
|||
|
|
|
|||
|
|
// 发送通知
|
|||
|
|
await this.sendNotification(command);
|
|||
|
|
console.log('✅ 通知已发送');
|
|||
|
|
|
|||
|
|
console.log('\n🎯 请在Claude Code中粘贴命令 (Cmd+V)');
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 处理命令失败:', error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async copyToClipboard(command) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const pbcopy = spawn('pbcopy');
|
|||
|
|
pbcopy.stdin.write(command);
|
|||
|
|
pbcopy.stdin.end();
|
|||
|
|
|
|||
|
|
pbcopy.on('close', (code) => {
|
|||
|
|
if (code === 0) {
|
|||
|
|
resolve();
|
|||
|
|
} else {
|
|||
|
|
reject(new Error('剪贴板复制失败'));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async sendNotification(command) {
|
|||
|
|
const shortCommand = command.length > 50 ? command.substring(0, 50) + '...' : command;
|
|||
|
|
|
|||
|
|
const script = `
|
|||
|
|
display notification "邮件命令已复制到剪贴板,请在Claude Code中粘贴" with title "TaskPing" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "Glass"
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
spawn('osascript', ['-e', script]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async startContinuousMonitoring() {
|
|||
|
|
console.log('\n👂 开始持续监控新邮件...');
|
|||
|
|
console.log('💌 现在可以回复邮件测试功能');
|
|||
|
|
console.log('🔍 每30秒检查一次新邮件\n');
|
|||
|
|
|
|||
|
|
setInterval(async () => {
|
|||
|
|
try {
|
|||
|
|
console.log('🔄 检查新邮件...');
|
|||
|
|
await this.searchEmails('1h', false); // 搜索所有邮件,不只是未读
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 监控检查失败:', error.message);
|
|||
|
|
}
|
|||
|
|
}, 30000); // 每30秒检查一次
|
|||
|
|
|
|||
|
|
// 设置优雅关闭
|
|||
|
|
process.on('SIGINT', () => {
|
|||
|
|
console.log('\n🛑 停止邮件监控...');
|
|||
|
|
if (this.imap) {
|
|||
|
|
this.imap.end();
|
|||
|
|
}
|
|||
|
|
console.log('✅ 服务已停止');
|
|||
|
|
process.exit(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
process.stdin.resume();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 启动检查器
|
|||
|
|
const checker = new EmailChecker();
|
|||
|
|
checker.start().catch(console.error);
|