修复邮件监听服务的IMAP问题
- 解决client.idle()不兼容的问题 - 改用定期检查机制监听新邮件 - 修复邮件下载和解析方法 - 改善错误处理和日志记录 - 配置使用飞书邮箱接收回复 现在可以: 1. 成功连接飞书IMAP服务器 2. 读取和解析邮件内容 3. 检测TaskPing相关邮件 4. 等待用户回复测试 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
be9eb49734
commit
a920a41c90
|
|
@ -18,12 +18,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"imap": {
|
"imap": {
|
||||||
"host": "imap.gmail.com",
|
"host": "imap.feishu.cn",
|
||||||
"port": 993,
|
"port": 993,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"auth": {
|
"auth": {
|
||||||
"user": "jiaxicui446@gmail.com",
|
"user": "noreply@pandalla.ai",
|
||||||
"pass": "your-gmail-app-password"
|
"pass": "kKgS3tNReRTL3RQC"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"from": "TaskPing 通知系统 <noreply@pandalla.ai>",
|
"from": "TaskPing 通知系统 <noreply@pandalla.ai>",
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,12 @@
|
||||||
"expiresAt": 1753559287,
|
"expiresAt": 1753559287,
|
||||||
"cwd": "/Users/jessytsui/dev/TaskPing",
|
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||||
"description": "测试会话"
|
"description": "测试会话"
|
||||||
|
},
|
||||||
|
"TESTMDKM0Q3M": {
|
||||||
|
"type": "pty",
|
||||||
|
"createdAt": 1753556089,
|
||||||
|
"expiresAt": 1753559689,
|
||||||
|
"cwd": "/Users/jessytsui/dev/TaskPing",
|
||||||
|
"description": "测试会话"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"id": "ace02d65-154a-4c95-9616-cb6e66ad06ec",
|
||||||
|
"created": "2025-07-26T18:49:27.369Z",
|
||||||
|
"expires": "2025-07-27T18:49:27.369Z",
|
||||||
|
"notification": {
|
||||||
|
"type": "completed",
|
||||||
|
"project": "TaskPing",
|
||||||
|
"message": "[TaskPing] 任务已完成,Claude正在等待下一步指令"
|
||||||
|
},
|
||||||
|
"status": "waiting",
|
||||||
|
"commandCount": 0,
|
||||||
|
"maxCommands": 10
|
||||||
|
}
|
||||||
|
|
@ -165,9 +165,9 @@ function stripReply(text = '') {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理邮件消息
|
// 处理邮件消息
|
||||||
async function handleMailMessage(source, uid) {
|
async function handleMailMessage(source, uid, parsedEmail = null) {
|
||||||
try {
|
try {
|
||||||
const parsed = await simpleParser(source);
|
const parsed = parsedEmail || await simpleParser(source);
|
||||||
|
|
||||||
// 检查是否已处理过
|
// 检查是否已处理过
|
||||||
const messageId = parsed.messageId;
|
const messageId = parsed.messageId;
|
||||||
|
|
@ -365,45 +365,132 @@ async function startImap() {
|
||||||
log.info(`Found ${messages.length} unread messages`);
|
log.info(`Found ${messages.length} unread messages`);
|
||||||
|
|
||||||
for (const uid of messages) {
|
for (const uid of messages) {
|
||||||
const { source } = await client.download(uid, '1', { uid: true });
|
try {
|
||||||
const chunks = [];
|
log.debug({ uid }, 'Downloading message');
|
||||||
for await (const chunk of source) {
|
const message = await client.fetchOne(uid, {
|
||||||
chunks.push(chunk);
|
bodyText: true,
|
||||||
}
|
bodyStructure: true,
|
||||||
await handleMailMessage(Buffer.concat(chunks), uid);
|
envelope: true
|
||||||
|
}, { uid: true });
|
||||||
|
|
||||||
// 标记为已读
|
if (!message) {
|
||||||
await client.messageFlagsAdd(uid, ['\\Seen'], { uid: true });
|
log.warn({ uid }, 'Could not fetch message');
|
||||||
}
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
// 监听新邮件
|
|
||||||
log.info('Starting IMAP monitor...');
|
|
||||||
|
|
||||||
for await (const msg of client.idle()) {
|
|
||||||
if (msg.path === 'INBOX' && msg.type === 'exists') {
|
|
||||||
log.debug({ count: msg.count }, 'New message notification');
|
|
||||||
|
|
||||||
// 获取最新的邮件
|
|
||||||
const messages = await client.search({ seen: false });
|
|
||||||
for (const uid of messages) {
|
|
||||||
const { source } = await client.download(uid, '1', { uid: true });
|
|
||||||
const chunks = [];
|
|
||||||
for await (const chunk of source) {
|
|
||||||
chunks.push(chunk);
|
|
||||||
}
|
}
|
||||||
await handleMailMessage(Buffer.concat(chunks), uid);
|
|
||||||
|
// 使用简单的方式获取邮件内容
|
||||||
|
const emailText = message.bodyText?.value || '';
|
||||||
|
const envelope = message.envelope;
|
||||||
|
|
||||||
|
// 构造简单的邮件对象用于解析
|
||||||
|
const simpleEmail = {
|
||||||
|
from: envelope.from?.[0] ? {
|
||||||
|
text: `${envelope.from[0].name || ''} <${envelope.from[0].address}>`,
|
||||||
|
value: envelope.from
|
||||||
|
} : null,
|
||||||
|
subject: envelope.subject,
|
||||||
|
text: emailText,
|
||||||
|
date: envelope.date,
|
||||||
|
messageId: envelope.messageId,
|
||||||
|
headers: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
await handleMailMessage(null, uid, simpleEmail);
|
||||||
|
|
||||||
// 标记为已读
|
// 标记为已读
|
||||||
await client.messageFlagsAdd(uid, ['\\Seen'], { uid: true });
|
await client.messageFlagsAdd(uid, ['\\Seen'], { uid: true });
|
||||||
|
} catch (downloadError) {
|
||||||
|
log.error({
|
||||||
|
uid,
|
||||||
|
error: downloadError.message,
|
||||||
|
code: downloadError.code
|
||||||
|
}, 'Failed to download message');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用定期检查替代IDLE监听(更稳定)
|
||||||
|
log.info('Starting periodic email check...');
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const newMessages = await client.search({ seen: false });
|
||||||
|
if (newMessages.length > 0) {
|
||||||
|
log.debug({ count: newMessages.length }, 'Found new messages');
|
||||||
|
|
||||||
|
for (const uid of newMessages) {
|
||||||
|
// 检查是否已处理过这个UID
|
||||||
|
if (PROCESSED_MESSAGES.has(`uid_${uid}`)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.debug({ uid }, 'Processing new message');
|
||||||
|
const message = await client.fetchOne(uid, {
|
||||||
|
bodyText: true,
|
||||||
|
bodyStructure: true,
|
||||||
|
envelope: true
|
||||||
|
}, { uid: true });
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
log.warn({ uid }, 'Could not fetch new message');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用简单的方式获取邮件内容
|
||||||
|
const emailText = message.bodyText?.value || '';
|
||||||
|
const envelope = message.envelope;
|
||||||
|
|
||||||
|
// 构造简单的邮件对象用于解析
|
||||||
|
const simpleEmail = {
|
||||||
|
from: envelope.from?.[0] ? {
|
||||||
|
text: `${envelope.from[0].name || ''} <${envelope.from[0].address}>`,
|
||||||
|
value: envelope.from
|
||||||
|
} : null,
|
||||||
|
subject: envelope.subject,
|
||||||
|
text: emailText,
|
||||||
|
date: envelope.date,
|
||||||
|
messageId: envelope.messageId,
|
||||||
|
headers: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
await handleMailMessage(null, uid, simpleEmail);
|
||||||
|
|
||||||
|
// 标记为已处理
|
||||||
|
PROCESSED_MESSAGES.add(`uid_${uid}`);
|
||||||
|
|
||||||
|
// 标记为已读
|
||||||
|
await client.messageFlagsAdd(uid, ['\\Seen'], { uid: true });
|
||||||
|
} catch (processError) {
|
||||||
|
log.error({
|
||||||
|
uid,
|
||||||
|
error: processError.message,
|
||||||
|
code: processError.code
|
||||||
|
}, 'Failed to process new message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (checkError) {
|
||||||
|
log.error({ error: checkError.message }, 'Error during periodic check');
|
||||||
|
}
|
||||||
|
}, 10000); // 每10秒检查一次
|
||||||
|
|
||||||
|
// 保持连接活跃
|
||||||
|
log.info('Email monitoring active, checking every 10 seconds...');
|
||||||
|
|
||||||
|
// 无限等待(保持进程运行)
|
||||||
|
await new Promise(resolve => {
|
||||||
|
// 进程会一直运行直到被终止
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
lock.release();
|
lock.release();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({ error }, 'IMAP error');
|
log.error({
|
||||||
|
error: error.message,
|
||||||
|
code: error.code,
|
||||||
|
stack: error.stack
|
||||||
|
}, 'IMAP error');
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
await client.logout();
|
await client.logout();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试 IMAP 连接
|
||||||
|
* 验证邮箱服务器连接和邮件读取
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { ImapFlow } = require('imapflow');
|
||||||
|
const { simpleParser } = require('mailparser');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
async function testImapConnection() {
|
||||||
|
console.log('🔍 测试 IMAP 连接...\n');
|
||||||
|
|
||||||
|
const client = new ImapFlow({
|
||||||
|
host: process.env.IMAP_HOST,
|
||||||
|
port: Number(process.env.IMAP_PORT || 993),
|
||||||
|
secure: process.env.IMAP_SECURE === 'true',
|
||||||
|
auth: {
|
||||||
|
user: process.env.IMAP_USER,
|
||||||
|
pass: process.env.IMAP_PASS
|
||||||
|
},
|
||||||
|
logger: false
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`连接到: ${process.env.IMAP_HOST}:${process.env.IMAP_PORT}`);
|
||||||
|
console.log(`账号: ${process.env.IMAP_USER}`);
|
||||||
|
console.log(`SSL: ${process.env.IMAP_SECURE}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 连接
|
||||||
|
await client.connect();
|
||||||
|
console.log('✅ IMAP 连接成功');
|
||||||
|
|
||||||
|
// 打开收件箱
|
||||||
|
const lock = await client.getMailboxLock('INBOX');
|
||||||
|
console.log('✅ 收件箱已打开');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取邮箱状态
|
||||||
|
const status = await client.status('INBOX', {
|
||||||
|
messages: true,
|
||||||
|
unseen: true,
|
||||||
|
recent: true
|
||||||
|
});
|
||||||
|
console.log(`📧 邮箱状态:`);
|
||||||
|
console.log(` 总邮件数: ${status.messages}`);
|
||||||
|
console.log(` 未读邮件: ${status.unseen}`);
|
||||||
|
console.log(` 新邮件: ${status.recent}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 搜索未读邮件
|
||||||
|
const unseenMessages = await client.search({ seen: false });
|
||||||
|
console.log(`🔍 找到 ${unseenMessages.length} 封未读邮件`);
|
||||||
|
|
||||||
|
if (unseenMessages.length > 0) {
|
||||||
|
console.log('📋 未读邮件列表:');
|
||||||
|
|
||||||
|
// 只处理最近的5封邮件
|
||||||
|
const recentMessages = unseenMessages.slice(-5);
|
||||||
|
|
||||||
|
for (const uid of recentMessages) {
|
||||||
|
try {
|
||||||
|
console.log(`\n📧 处理邮件 UID: ${uid}`);
|
||||||
|
|
||||||
|
// 获取邮件头信息
|
||||||
|
const envelope = await client.fetchOne(uid, {
|
||||||
|
envelope: true,
|
||||||
|
flags: true
|
||||||
|
}, { uid: true });
|
||||||
|
|
||||||
|
console.log(` 发件人: ${envelope.envelope?.from?.[0]?.address || 'unknown'}`);
|
||||||
|
console.log(` 主题: ${envelope.envelope?.subject || 'no subject'}`);
|
||||||
|
console.log(` 日期: ${envelope.envelope?.date?.toLocaleString('zh-CN') || 'unknown'}`);
|
||||||
|
console.log(` 标志: ${Array.isArray(envelope.flags) ? envelope.flags.join(', ') : 'none'}`);
|
||||||
|
|
||||||
|
// 下载并解析邮件
|
||||||
|
const message = await client.fetchOne(uid, {
|
||||||
|
source: true
|
||||||
|
}, { uid: true });
|
||||||
|
|
||||||
|
if (!message || !message.source) {
|
||||||
|
console.log(` ⚠️ 无法获取邮件内容`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of message.source) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = await simpleParser(Buffer.concat(chunks));
|
||||||
|
|
||||||
|
// 检查是否是 TaskPing 相关邮件
|
||||||
|
const isTaskPingReply = parsed.subject?.includes('TaskPing') ||
|
||||||
|
parsed.subject?.includes('[TaskPing') ||
|
||||||
|
parsed.text?.includes('TaskPing') ||
|
||||||
|
parsed.headers?.get('x-taskping-session-id');
|
||||||
|
|
||||||
|
if (isTaskPingReply) {
|
||||||
|
console.log(` 🎯 这是 TaskPing 相关邮件!`);
|
||||||
|
console.log(` 正文预览: ${(parsed.text || '').substring(0, 100)}...`);
|
||||||
|
|
||||||
|
// 尝试提取 Token
|
||||||
|
const tokenMatch = parsed.subject?.match(/\[TaskPing\s+#([A-Za-z0-9_-]+)\]/);
|
||||||
|
if (tokenMatch) {
|
||||||
|
console.log(` 🔑 找到 Token: ${tokenMatch[1]}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` ℹ️ 非 TaskPing 邮件,跳过`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (messageError) {
|
||||||
|
console.log(` ❌ 处理邮件失败: ${messageError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
lock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✅ IMAP 测试完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ IMAP 连接失败:', error.message);
|
||||||
|
console.log('\n可能的原因:');
|
||||||
|
console.log('1. 邮箱服务器地址或端口错误');
|
||||||
|
console.log('2. 用户名或密码错误');
|
||||||
|
console.log('3. IMAP 服务未开启');
|
||||||
|
console.log('4. 网络连接问题');
|
||||||
|
console.log('5. SSL/TLS 配置错误');
|
||||||
|
|
||||||
|
if (error.code) {
|
||||||
|
console.log(`\n错误代码: ${error.code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
await client.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
async function main() {
|
||||||
|
console.log('╔══════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ TaskPing IMAP Connection Test ║');
|
||||||
|
console.log('╚══════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
// 检查配置
|
||||||
|
if (!process.env.IMAP_HOST || !process.env.IMAP_USER || !process.env.IMAP_PASS) {
|
||||||
|
console.error('❌ 缺少 IMAP 配置,请检查 .env 文件');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await testImapConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行
|
||||||
|
main().catch(console.error);
|
||||||
Loading…
Reference in New Issue