Mail MCP Tool

by shuakami
Verified
import nodemailer from 'nodemailer'; import IMAP from 'imap'; import { simpleParser, ParsedMail, AddressObject } from 'mailparser'; import { Readable } from 'stream'; import { promisify } from 'util'; // 邮件配置接口 export interface MailConfig { smtp: { host: string; port: number; secure: boolean; auth: { user: string; pass: string; } }, imap: { host: string; port: number; secure: boolean; auth: { user: string; pass: string; } }, defaults: { fromName: string; fromEmail: string; } } // 邮件信息接口 export interface MailInfo { to: string | string[]; cc?: string | string[]; bcc?: string | string[]; subject: string; text?: string; html?: string; attachments?: Array<{ filename: string; content: string | Buffer; contentType?: string; }>; } // 邮件查询选项 export interface MailSearchOptions { folder?: string; readStatus?: 'read' | 'unread' | 'all'; fromDate?: Date; toDate?: Date; from?: string; to?: string; subject?: string; hasAttachments?: boolean; limit?: number; } // 邮件项 export interface MailItem { id: string; uid: number; subject: string; from: { name?: string; address: string }[]; to: { name?: string; address: string }[]; cc?: { name?: string; address: string }[]; date: Date; isRead: boolean; hasAttachments: boolean; attachments?: { filename: string; contentType: string; size: number }[]; textBody?: string; htmlBody?: string; flags?: string[]; size: number; folder: string; } // 地址信息接口 interface EmailAddress { name?: string; address: string; } export class MailService { private smtpTransporter: nodemailer.Transporter; private imapClient: IMAP; private config: MailConfig; private isImapConnected = false; constructor(config: MailConfig) { this.config = config; // 创建SMTP传输器 this.smtpTransporter = nodemailer.createTransport({ host: config.smtp.host, port: config.smtp.port, secure: config.smtp.secure, auth: { user: config.smtp.auth.user, pass: config.smtp.auth.pass, }, }); // 创建IMAP客户端 this.imapClient = new IMAP({ user: config.imap.auth.user, password: config.imap.auth.pass, host: config.imap.host, port: config.imap.port, tls: config.imap.secure, tlsOptions: { rejectUnauthorized: false }, }); // 监听IMAP连接错误 this.imapClient.on('error', (err: Error) => { console.error('IMAP错误:', err); this.isImapConnected = false; }); } /** * 连接到IMAP服务器 */ async connectImap(): Promise<void> { if (this.isImapConnected) return; return new Promise((resolve, reject) => { this.imapClient.once('ready', () => { this.isImapConnected = true; resolve(); }); this.imapClient.once('error', (err: Error) => { reject(err); }); this.imapClient.connect(); }); } /** * 关闭IMAP连接 */ closeImap(): void { if (this.isImapConnected) { this.imapClient.end(); this.isImapConnected = false; } } /** * 发送邮件 */ async sendMail(mailInfo: MailInfo): Promise<{ success: boolean; messageId?: string; error?: string }> { try { const mailOptions = { from: { name: this.config.defaults.fromName, address: this.config.defaults.fromEmail, }, to: mailInfo.to, cc: mailInfo.cc, bcc: mailInfo.bcc, subject: mailInfo.subject, text: mailInfo.text, html: mailInfo.html, attachments: mailInfo.attachments, }; const info = await this.smtpTransporter.sendMail(mailOptions); return { success: true, messageId: info.messageId }; } catch (error) { console.error('发送邮件错误:', error); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * 获取邮箱文件夹列表 */ async getFolders(): Promise<string[]> { await this.connectImap(); return new Promise((resolve, reject) => { this.imapClient.getBoxes((err, boxes) => { if (err) { reject(err); return; } const folderNames: string[] = []; // 递归遍历所有邮件文件夹 const processBoxes = (boxes: IMAP.MailBoxes, prefix = '') => { for (const name in boxes) { folderNames.push(prefix + name); if (boxes[name].children) { processBoxes(boxes[name].children, `${prefix}${name}${boxes[name].delimiter}`); } } }; processBoxes(boxes); resolve(folderNames); }); }); } /** * 搜索邮件 */ async searchMails(options: MailSearchOptions = {}): Promise<MailItem[]> { await this.connectImap(); const folder = options.folder || 'INBOX'; const limit = options.limit || 20; return new Promise((resolve, reject) => { this.imapClient.openBox(folder, false, (err, box) => { if (err) { reject(err); return; } // 构建搜索条件 const criteria: any[] = []; if (options.readStatus === 'read') { criteria.push('SEEN'); } else if (options.readStatus === 'unread') { criteria.push('UNSEEN'); } if (options.fromDate) { criteria.push(['SINCE', options.fromDate]); } if (options.toDate) { criteria.push(['BEFORE', options.toDate]); } if (options.from) { criteria.push(['FROM', options.from]); } if (options.to) { criteria.push(['TO', options.to]); } if (options.subject) { criteria.push(['SUBJECT', options.subject]); } if (criteria.length === 0) { criteria.push('ALL'); } // 执行搜索 this.imapClient.search(criteria, (err, uids) => { if (err) { reject(err); return; } if (uids.length === 0) { resolve([]); return; } // 限制结果数量 const limitedUids = uids.slice(-Math.min(limit, uids.length)); // 获取邮件详情 const fetch = this.imapClient.fetch(limitedUids, { bodies: ['HEADER', 'TEXT'], struct: true, envelope: true, size: true, markSeen: false, }); const messages: MailItem[] = []; fetch.on('message', (msg, seqno) => { const message: Partial<MailItem> = { id: '', uid: 0, folder, flags: [], subject: '', from: [], to: [], date: new Date(), isRead: false, hasAttachments: false, size: 0, }; msg.on('body', (stream, info) => { let buffer = ''; stream.on('data', (chunk) => { buffer += chunk.toString('utf8'); }); stream.once('end', () => { if (info.which === 'HEADER') { const parsed = IMAP.parseHeader(buffer); message.subject = parsed.subject?.[0] || ''; message.from = this.parseAddressList(parsed.from); message.to = this.parseAddressList(parsed.to); message.cc = this.parseAddressList(parsed.cc); if (parsed.date && parsed.date[0]) { message.date = new Date(parsed.date[0]); } } else if (info.which === 'TEXT') { const readable = new Readable(); readable.push(buffer); readable.push(null); simpleParser(readable).then((parsed) => { message.textBody = parsed.text || undefined; message.htmlBody = parsed.html || undefined; message.attachments = parsed.attachments.map(att => ({ filename: att.filename || 'unknown', contentType: att.contentType, size: att.size, })); message.hasAttachments = parsed.attachments.length > 0; }).catch(err => { console.error('解析邮件内容错误:', err); }); } }); }); msg.once('attributes', (attrs) => { message.uid = attrs.uid; message.id = attrs.uid.toString(); message.flags = attrs.flags; message.isRead = attrs.flags.includes('\\Seen'); message.size = attrs.size || 0; // 检查是否有附件 if (attrs.struct) { message.hasAttachments = this.checkHasAttachments(attrs.struct); } }); msg.once('end', () => { messages.push(message as MailItem); }); }); fetch.once('error', (err) => { reject(err); }); fetch.once('end', () => { resolve(messages); }); }); }); }); } /** * 获取邮件详情 */ async getMailDetail(uid: number | string, folder: string = 'INBOX'): Promise<MailItem | null> { await this.connectImap(); // 确保 uid 为数字类型 const numericUid = typeof uid === 'string' ? parseInt(uid, 10) : uid; return new Promise((resolve, reject) => { this.imapClient.openBox(folder, false, (err) => { if (err) { reject(err); return; } const fetch = this.imapClient.fetch([numericUid], { bodies: '', struct: true, markSeen: false, }); let mailItem: MailItem | null = null; let attributes: any = null; let bodyParsed = false; let endReceived = false; // 检查是否所有处理都已完成并可以返回结果 const checkAndResolve = () => { if (bodyParsed && endReceived) { // 如果有属性数据但mailItem还没设置上,则现在设置 if (attributes && mailItem) { mailItem.flags = attributes.flags; mailItem.isRead = attributes.flags.includes('\\Seen'); mailItem.size = attributes.size || 0; } resolve(mailItem); } }; fetch.on('message', (msg) => { msg.on('body', (stream) => { // 创建一个可读流缓冲区 let buffer = ''; stream.on('data', (chunk) => { buffer += chunk.toString('utf8'); }); stream.once('end', () => { // 使用simpleParser解析邮件内容 const readable = new Readable(); readable.push(buffer); readable.push(null); simpleParser(readable).then((parsed: ParsedMail) => { // 处理发件人信息 const from: EmailAddress[] = []; if (parsed.from && 'value' in parsed.from) { from.push(...(parsed.from.value.map(addr => ({ name: addr.name || undefined, address: addr.address || '', })))); } // 处理收件人信息 const to: EmailAddress[] = []; if (parsed.to && 'value' in parsed.to) { to.push(...(parsed.to.value.map(addr => ({ name: addr.name || undefined, address: addr.address || '', })))); } // 处理抄送人信息 const cc: EmailAddress[] = []; if (parsed.cc && 'value' in parsed.cc) { cc.push(...(parsed.cc.value.map(addr => ({ name: addr.name || undefined, address: addr.address || '', })))); } mailItem = { id: numericUid.toString(), uid: numericUid, subject: parsed.subject || '', from, to, cc: cc.length > 0 ? cc : undefined, date: parsed.date || new Date(), isRead: false, // 将通过attributes更新 hasAttachments: parsed.attachments.length > 0, attachments: parsed.attachments.map(att => ({ filename: att.filename || 'unknown', contentType: att.contentType, size: att.size, })), textBody: parsed.text || undefined, htmlBody: parsed.html || undefined, size: 0, // 将通过attributes更新 folder, }; // 如果已经接收到属性,现在应用它们 if (attributes) { mailItem.flags = attributes.flags; mailItem.isRead = attributes.flags.includes('\\Seen'); mailItem.size = attributes.size || 0; } bodyParsed = true; checkAndResolve(); }).catch(err => { console.error('解析邮件详情错误:', err); reject(err); }); }); }); msg.once('attributes', (attrs) => { attributes = attrs; if (mailItem) { mailItem.flags = attrs.flags; mailItem.isRead = attrs.flags.includes('\\Seen'); mailItem.size = attrs.size || 0; } }); }); fetch.once('error', (err) => { reject(err); }); fetch.once('end', () => { endReceived = true; // 如果邮件没有内容,或者处理过程中出现问题,尝试确保至少返回空结果 if (!bodyParsed && !mailItem) { console.log(`没有找到UID为${numericUid}的邮件或邮件内容为空`); } checkAndResolve(); }); }); }); } /** * 将邮件标记为已读 */ async markAsRead(uid: number | string, folder: string = 'INBOX'): Promise<boolean> { await this.connectImap(); // 确保 uid 为数字类型 const numericUid = typeof uid === 'string' ? parseInt(uid, 10) : uid; return new Promise((resolve, reject) => { this.imapClient.openBox(folder, false, (err) => { if (err) { reject(err); return; } this.imapClient.addFlags(numericUid, '\\Seen', (err) => { if (err) { reject(err); return; } resolve(true); }); }); }); } /** * 将邮件标记为未读 */ async markAsUnread(uid: number | string, folder: string = 'INBOX'): Promise<boolean> { await this.connectImap(); // 确保 uid 为数字类型 const numericUid = typeof uid === 'string' ? parseInt(uid, 10) : uid; return new Promise((resolve, reject) => { this.imapClient.openBox(folder, false, (err) => { if (err) { reject(err); return; } this.imapClient.delFlags(numericUid, '\\Seen', (err) => { if (err) { reject(err); return; } resolve(true); }); }); }); } /** * 删除邮件 */ async deleteMail(uid: number | string, folder: string = 'INBOX'): Promise<boolean> { await this.connectImap(); // 确保 uid 为数字类型 const numericUid = typeof uid === 'string' ? parseInt(uid, 10) : uid; return new Promise((resolve, reject) => { this.imapClient.openBox(folder, false, (err) => { if (err) { reject(err); return; } this.imapClient.addFlags(numericUid, '\\Deleted', (err) => { if (err) { reject(err); return; } this.imapClient.expunge((err) => { if (err) { reject(err); return; } resolve(true); }); }); }); }); } /** * 移动邮件到其他文件夹 */ async moveMail(uid: number | string, sourceFolder: string, targetFolder: string): Promise<boolean> { await this.connectImap(); // 确保 uid 为数字类型 const numericUid = typeof uid === 'string' ? parseInt(uid, 10) : uid; return new Promise((resolve, reject) => { this.imapClient.openBox(sourceFolder, false, (err) => { if (err) { reject(err); return; } this.imapClient.move(numericUid, targetFolder, (err) => { if (err) { reject(err); return; } resolve(true); }); }); }); } /** * 关闭所有连接 */ async close(): Promise<void> { this.closeImap(); await promisify(this.smtpTransporter.close.bind(this.smtpTransporter))(); } // 辅助方法:解析地址列表 private parseAddressList(addresses?: string[]): EmailAddress[] { if (!addresses || addresses.length === 0) return []; return addresses.map(addr => { const match = addr.match(/(?:"?([^"]*)"?\s)?(?:<?(.+@[^>]+)>?)/); if (match) { const [, name, address] = match; return { name: name || undefined, address: address || '' }; } return { address: addr }; }); } // 辅助方法:检查是否有附件 private checkHasAttachments(struct: any[]): boolean { if (!struct || !Array.isArray(struct)) return false; if (struct[0] && struct[0].disposition && struct[0].disposition.type.toLowerCase() === 'attachment') { return true; } for (const item of struct) { if (Array.isArray(item)) { if (this.checkHasAttachments(item)) { return true; } } } return false; } /** * 高级搜索邮件 - 支持多个文件夹和更复杂的过滤条件 */ async advancedSearchMails(options: { folders?: string[]; // 要搜索的文件夹列表,默认为INBOX keywords?: string; // 全文搜索关键词 startDate?: Date; // 开始日期 endDate?: Date; // 结束日期 from?: string; // 发件人 to?: string; // 收件人 subject?: string; // 主题 hasAttachment?: boolean; // 是否有附件 maxResults?: number; // 最大结果数 includeBody?: boolean; // 是否包含邮件正文 }): Promise<MailItem[]> { const allResults: MailItem[] = []; const folders = options.folders || ['INBOX']; const maxResults = options.maxResults || 100; console.log(`执行高级搜索,文件夹: ${folders.join(', ')}, 关键词: ${options.keywords || '无'}`); // 对每个文件夹执行搜索 for (const folder of folders) { if (allResults.length >= maxResults) break; try { const folderResults = await this.searchMails({ folder, readStatus: 'all', fromDate: options.startDate, toDate: options.endDate, from: options.from, to: options.to, subject: options.subject, hasAttachments: options.hasAttachment, limit: maxResults - allResults.length }); // 如果包含关键词,执行全文匹配 if (options.keywords && options.keywords.trim() !== '') { const keywordLower = options.keywords.toLowerCase(); const filteredResults = folderResults.filter(mail => { // 在主题、发件人、收件人中搜索 const subjectMatch = mail.subject.toLowerCase().includes(keywordLower); const fromMatch = mail.from.some(f => (f.name?.toLowerCase() || '').includes(keywordLower) || f.address.toLowerCase().includes(keywordLower) ); const toMatch = mail.to.some(t => (t.name?.toLowerCase() || '').includes(keywordLower) || t.address.toLowerCase().includes(keywordLower) ); // 如果需要在正文中搜索,可能需要额外获取邮件详情 let bodyMatch = false; if (options.includeBody) { bodyMatch = (mail.textBody?.toLowerCase() || '').includes(keywordLower) || (mail.htmlBody?.toLowerCase() || '').includes(keywordLower); } return subjectMatch || fromMatch || toMatch || bodyMatch; }); allResults.push(...filteredResults); } else { allResults.push(...folderResults); } } catch (error) { console.error(`搜索文件夹 ${folder} 时出错:`, error); // 继续搜索其他文件夹 } } // 按日期降序排序(最新的邮件优先) allResults.sort((a, b) => b.date.getTime() - a.date.getTime()); // 限制结果数量 return allResults.slice(0, maxResults); } /** * 获取通讯录 - 基于邮件历史提取联系人信息 */ async getContacts(options: { maxResults?: number; // 最大结果数 includeGroups?: boolean; // 是否包含分组 searchTerm?: string; // 搜索词 } = {}): Promise<{ contacts: { name?: string; email: string; frequency: number; // 联系频率 lastContact?: Date; // 最后联系时间 }[]; }> { const maxResults = options.maxResults || 100; const searchTerm = options.searchTerm?.toLowerCase() || ''; // 从最近的邮件中提取联系人 const contactMap = new Map<string, { name?: string; email: string; frequency: number; lastContact?: Date; }>(); // 从收件箱和已发送邮件中收集联系人 const folders = ['INBOX', 'Sent Messages']; for (const folder of folders) { try { const emails = await this.searchMails({ folder, limit: 200, // 搜索足够多的邮件以收集联系人 }); emails.forEach(email => { // 处理收件箱中的发件人 if (folder === 'INBOX') { email.from.forEach(sender => { if (sender.address === this.config.defaults.fromEmail) return; // 跳过自己 const key = sender.address.toLowerCase(); if (!contactMap.has(key)) { contactMap.set(key, { name: sender.name, email: sender.address, frequency: 1, lastContact: email.date }); } else { const contact = contactMap.get(key)!; contact.frequency += 1; if (!contact.lastContact || email.date > contact.lastContact) { contact.lastContact = email.date; } } }); } // 处理已发送邮件中的收件人 if (folder === 'Sent Messages') { email.to.forEach(recipient => { if (recipient.address === this.config.defaults.fromEmail) return; // 跳过自己 const key = recipient.address.toLowerCase(); if (!contactMap.has(key)) { contactMap.set(key, { name: recipient.name, email: recipient.address, frequency: 1, lastContact: email.date }); } else { const contact = contactMap.get(key)!; contact.frequency += 1; if (!contact.lastContact || email.date > contact.lastContact) { contact.lastContact = email.date; } } }); // 如果有抄送人,也处理 if (email.cc) { email.cc.forEach(cc => { if (cc.address === this.config.defaults.fromEmail) return; // 跳过自己 const key = cc.address.toLowerCase(); if (!contactMap.has(key)) { contactMap.set(key, { name: cc.name, email: cc.address, frequency: 1, lastContact: email.date }); } else { const contact = contactMap.get(key)!; contact.frequency += 1; if (!contact.lastContact || email.date > contact.lastContact) { contact.lastContact = email.date; } } }); } } }); } catch (error) { console.error(`从文件夹 ${folder} 收集联系人时出错:`, error); // 继续处理其他文件夹 } } // 转换为数组并排序(频率优先) let contacts = Array.from(contactMap.values()); // 如果提供了搜索词,进行过滤 if (searchTerm) { contacts = contacts.filter(contact => (contact.name?.toLowerCase() || '').includes(searchTerm) || contact.email.toLowerCase().includes(searchTerm) ); } // 按联系频率排序 contacts.sort((a, b) => b.frequency - a.frequency); // 限制结果数 contacts = contacts.slice(0, maxResults); return { contacts }; } /** * 获取邮件附件 * @param uid 邮件UID * @param folder 文件夹名称 * @param attachmentIndex 附件索引 * @returns 附件数据,包括文件名、内容和内容类型 */ async getAttachment(uid: number, folder: string = 'INBOX', attachmentIndex: number): Promise<{ filename: string; content: Buffer; contentType: string } | null> { await this.connectImap(); console.log(`正在获取UID ${uid} 的第 ${attachmentIndex} 个附件...`); return new Promise((resolve, reject) => { this.imapClient.openBox(folder, true, (err) => { if (err) { console.error(`打开文件夹 ${folder} 失败:`, err); reject(err); return; } const f = this.imapClient.fetch(`${uid}`, { bodies: '', struct: true }); let attachmentInfo: { partID: string; filename: string; contentType: string } | null = null; f.on('message', (msg, seqno) => { msg.on('body', (stream, info) => { // 这个事件处理器只是为了确保消息体被处理 stream.on('data', () => {}); stream.on('end', () => {}); }); msg.once('attributes', (attrs) => { try { const struct = attrs.struct; const attachments = this.findAttachmentParts(struct); if (attachments.length <= attachmentIndex) { console.log(`附件索引 ${attachmentIndex} 超出范围,附件总数: ${attachments.length}`); resolve(null); return; } attachmentInfo = attachments[attachmentIndex]; console.log(`找到附件信息:`, attachmentInfo); } catch (error) { console.error(`解析附件结构时出错:`, error); reject(error); } }); msg.once('end', () => { if (!attachmentInfo) { console.log(`未找到附件或附件索引无效`); resolve(null); return; } // 获取附件内容 const attachmentFetch = this.imapClient.fetch(`${uid}`, { bodies: [attachmentInfo.partID], struct: true }); let buffer = Buffer.alloc(0); attachmentFetch.on('message', (msg, seqno) => { msg.on('body', (stream, info) => { stream.on('data', (chunk) => { buffer = Buffer.concat([buffer, chunk]); }); stream.once('end', () => { console.log(`附件内容下载完成,大小: ${buffer.length} 字节`); }); }); msg.once('end', () => { console.log(`附件消息处理完成`); }); }); attachmentFetch.once('error', (err) => { console.error(`获取附件内容时出错:`, err); reject(err); }); attachmentFetch.once('end', () => { console.log(`附件获取流程结束`); resolve({ filename: attachmentInfo!.filename, content: buffer, contentType: attachmentInfo!.contentType }); }); }); }); f.once('error', (err) => { console.error(`获取邮件时出错:`, err); reject(err); }); f.once('end', () => { if (!attachmentInfo) { console.log(`未找到附件或结构中没有附件`); resolve(null); } }); }); }); } /** * 辅助方法:查找邮件结构中的所有附件 */ private findAttachmentParts(struct: any[], prefix = ''): { partID: string; filename: string; contentType: string }[] { const attachments: { partID: string; filename: string; contentType: string }[] = []; if (!struct || !Array.isArray(struct)) return attachments; const processStruct = (s: any, partID = '') => { if (Array.isArray(s)) { // 多部分结构 if (s[0] && typeof s[0] === 'object' && s[0].partID) { // 这是一个具体的部分 if (s[0].disposition && (s[0].disposition.type.toLowerCase() === 'attachment' || s[0].disposition.type.toLowerCase() === 'inline')) { let filename = ''; if (s[0].disposition.params && s[0].disposition.params.filename) { filename = s[0].disposition.params.filename; } else if (s[0].params && s[0].params.name) { filename = s[0].params.name; } const contentType = s[0].type + '/' + s[0].subtype; if (filename) { attachments.push({ partID: s[0].partID, filename: filename, contentType: contentType }); } } } else { // 遍历数组中的每个元素 for (let i = 0; i < s.length; i++) { const newPrefix = partID ? `${partID}.${i + 1}` : `${i + 1}`; if (Array.isArray(s[i])) { processStruct(s[i], newPrefix); } else if (typeof s[i] === 'object') { // 可能是一个部分定义 if (s[i].disposition && (s[i].disposition.type.toLowerCase() === 'attachment' || s[i].disposition.type.toLowerCase() === 'inline')) { let filename = ''; if (s[i].disposition.params && s[i].disposition.params.filename) { filename = s[i].disposition.params.filename; } else if (s[i].params && s[i].params.name) { filename = s[i].params.name; } const contentType = s[i].type + '/' + s[i].subtype; if (filename) { attachments.push({ partID: newPrefix, filename: filename, contentType: contentType }); } } } } } } }; processStruct(struct, prefix); return attachments; } /** * 批量将邮件标记为已读 */ async markMultipleAsRead(uids: (number | string)[], folder: string = 'INBOX'): Promise<boolean> { await this.connectImap(); // 确保所有 uid 都是数字类型 const numericUids = uids.map(uid => typeof uid === 'string' ? parseInt(uid, 10) : uid); return new Promise((resolve, reject) => { this.imapClient.openBox(folder, false, (err) => { if (err) { reject(err); return; } this.imapClient.addFlags(numericUids, '\\Seen', (err) => { if (err) { reject(err); return; } resolve(true); }); }); }); } /** * 批量将邮件标记为未读 */ async markMultipleAsUnread(uids: (number | string)[], folder: string = 'INBOX'): Promise<boolean> { await this.connectImap(); // 确保所有 uid 都是数字类型 const numericUids = uids.map(uid => typeof uid === 'string' ? parseInt(uid, 10) : uid); return new Promise((resolve, reject) => { this.imapClient.openBox(folder, false, (err) => { if (err) { reject(err); return; } this.imapClient.delFlags(numericUids, '\\Seen', (err) => { if (err) { reject(err); return; } resolve(true); }); }); }); } /** * 等待新邮件回复 * 此方法使用轮询方式检测新邮件的到达。主要用于需要等待用户邮件回复的场景。 * * 工作原理: * 1. 首先检查是否有5分钟内的未读邮件,如果有,返回特殊状态提示需要先处理这些邮件 * 2. 如果没有最近的未读邮件,则: * - 连接到IMAP服务器并获取当前邮件数量 * - 每5秒检查一次邮件数量 * - 如果发现新邮件,获取最新的邮件内容 * - 如果超过指定时间仍未收到新邮件,则返回null * * @param folder 要监听的文件夹,默认为'INBOX'(收件箱) * @param timeout 超时时间(毫秒),默认为3小时。超时后返回null * @returns 如果在超时前收到新邮件,返回邮件详情;如果超时,返回null;如果有最近未读邮件,返回带有特殊标记的邮件列表 */ async waitForNewReply(folder: string = 'INBOX', timeout: number = 3 * 60 * 60 * 1000): Promise<MailItem | null | { type: 'unread_warning'; mails: MailItem[] }> { await this.connectImap(); // 检查5分钟内的未读邮件 const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); const existingMails = await this.searchMails({ folder, limit: 5, readStatus: 'unread', fromDate: fiveMinutesAgo }); // 如果有5分钟内的未读邮件,返回特殊状态 if (existingMails.length > 0) { console.log(`[waitForNewReply] 发现${existingMails.length}封最近5分钟内的未读邮件,需要先处理`); return { type: 'unread_warning', mails: existingMails }; } return new Promise((resolve, reject) => { let timeoutId: NodeJS.Timeout; let isResolved = false; let initialCount = 0; let checkInterval: NodeJS.Timeout; // 清理函数 const cleanup = () => { if (timeoutId) { clearTimeout(timeoutId); } if (checkInterval) { clearInterval(checkInterval); } }; // 设置超时 timeoutId = setTimeout(() => { if (!isResolved) { isResolved = true; cleanup(); resolve(null); } }, timeout); // 获取初始邮件数量并开始轮询 this.imapClient.openBox(folder, false, (err, mailbox) => { if (err) { cleanup(); reject(err); return; } // 记录初始邮件数量 initialCount = mailbox.messages.total; console.log(`[waitForNewReply] 初始邮件数量: ${initialCount},开始等待新邮件回复...`); // 每5秒检查一次新邮件 checkInterval = setInterval(async () => { if (isResolved) return; try { // 重新打开邮箱以获取最新状态 this.imapClient.openBox(folder, false, async (err, mailbox) => { if (err || isResolved) return; const currentCount = mailbox.messages.total; console.log(`[waitForNewReply] 当前邮件数量: ${currentCount},初始数量: ${initialCount}`); if (currentCount > initialCount) { // 有新邮件,获取最新的邮件 try { const messages = await this.searchMails({ folder, limit: 1 }); if (messages.length > 0 && !isResolved) { // 获取完整的邮件内容 const fullMail = await this.getMailDetail(messages[0].uid, folder); if (fullMail) { console.log(`[waitForNewReply] 收到新邮件回复,主题: "${fullMail.subject}"`); isResolved = true; cleanup(); resolve(fullMail); } } } catch (error) { console.error('[waitForNewReply] 获取新邮件失败:', error); } } }); } catch (error) { console.error('[waitForNewReply] 检查新邮件时出错:', error); } }, 5000); }); }); } }