OpenClaw Skills 邮件管理(imap-smtp-email)技能使用参考手册
概述
IMAP/SMTP Email是用于通过IMAP/SMTP协议管理邮件的OpenClaw技能,支持邮件的读取、发送、搜索、标记为已读/未读、附件下载等功能,适用于多种标准IMAP/SMTP邮件服务器,包括Gmail、Outlook、163.com、vip.163.com、126.com、vip.126.com、188.com、vip.188.com等。
该技能能够让用户在OpenClaw中直接管理邮件,适合需要在AI助手中集成邮件管理功能的场景,例如邮件内容搜索、自动邮件回复、邮件分类管理等,帮助用户高效处理邮件事务。
技能信息
- 名称:imap-smtp-email
- 描述:通过IMAP/SMTP协议读取和发送邮件,支持检查新邮件/未读邮件、获取邮件内容、搜索邮箱、标记邮件为已读/未读,以及发送带附件的邮件,适用于多种IMAP/SMTP服务器。
- 版本:0.0.9
- 作者:gzlicanyi
- 依赖:
- 需要Node.js环境
- 需要安装相关npm依赖(dotenv、imap、imap-simple、mailparser、nodemailer)
- 触发词:"邮件管理"、"邮件读取"、"邮件发送"、"邮件搜索"、"邮件标记"
👤 作者:gzlicanyi
🦞 官方地址:https://clawhub.ai/gzlicanyi/imap-smtp-email
👉 Skills 下载地址:imap-smtp-email-0.0.9.zip
配置设置
环境变量配置
在技能文件夹中创建.env文件或设置环境变量,配置示例如下:
## IMAP配置(接收邮件)
IMAP_HOST=imap.gmail.com # 邮件服务器主机名
IMAP_PORT=993 # 邮件服务器端口
IMAP_USER=your@email.com # 邮箱地址
IMAP_PASS=your_password # 邮箱密码/应用密码/授权码
IMAP_TLS=true # 使用TLS/SSL连接
IMAP_REJECT_UNAUTHORIZED=true # 对于自签名证书,设置为false
IMAP_MAILBOX=INBOX # 默认邮箱文件夹
## SMTP配置(发送邮件)
SMTP_HOST=smtp.gmail.com # SMTP服务器主机名
SMTP_PORT=587 # SMTP端口(587用于STARTTLS,465用于SSL)
SMTP_SECURE=false # true表示使用SSL(465端口),false表示使用STARTTLS(587端口)
SMTP_USER=your@gmail.com # 邮箱地址
SMTP_PASS=your_password # 邮箱密码/应用密码/授权码
SMTP_FROM=your@gmail.com # 默认发件人邮箱(可选)
SMTP_REJECT_UNAUTHORIZED=true # 对于自签名证书,设置为false
常见邮件服务器配置
| 服务商 | IMAP主机 | IMAP端口 | SMTP主机 | SMTP端口 |
|---|---|---|---|---|
| 163.com | imap.163.com | 993 | smtp.163.com | 465 |
| vip.163.com | imap.vip.163.com | 993 | smtp.vip.163.com | 465 |
| 126.com | imap.126.com | 993 | smtp.126.com | 465 |
| vip.126.com | imap.vip.126.com | 993 | smtp.vip.126.com | 465 |
| 188.com | imap.188.com | 993 | smtp.188.com | 465 |
| vip.188.com | imap.vip.188.com | 993 | smtp.vip.188.com | 465 |
| yeah.net | imap.yeah.net | 993 | smtp.yeah.net | 465 |
| Gmail | imap.gmail.com | 993 | smtp.gmail.com | 587 |
| Outlook | outlook.office365.com | 993 | smtp.office365.com | 587 |
| QQ邮箱 | imap.qq.com | 993 | smtp.qq.com | 587 |
特殊说明
- Gmail:不接受普通账户密码,必须生成应用密码:
https://myaccount.google.com/apppasswords,使用生成的16位应用密码作为IMAP_PASS和SMTP_PASS,且需要启用两步验证。 - 163.com等网易邮箱:使用授权码而非账户密码,需要先在网页设置中启用IMAP/SMTP功能。
安装方法
1. 安装依赖
在技能目录下运行以下命令安装所需的npm依赖:
npm install
2. 配置邮箱信息
可以手动创建.env文件,也可以运行setup.sh脚本进行交互式配置:
bash setup.sh
该脚本会引导你选择邮箱提供商,输入邮箱地址、密码/授权码等信息,自动生成.env文件,并测试IMAP和SMTP连接。
使用方法
IMAP命令(接收邮件)
check - 检查新邮件/未读邮件
node scripts/imap.js check [--limit 10] [--mailbox INBOX] [--recent 2h]
选项:
--limit <n>:最大结果数(默认:10)--mailbox <name>:要检查的邮箱文件夹(默认:INBOX)--recent <time>:只显示最近X时间内的邮件(例如:30m、2h、7d)
fetch - 根据UID获取完整邮件内容
node scripts/imap.js fetch <uid> [--mailbox INBOX]
<uid>:邮件的唯一标识符
download - 下载邮件附件
node scripts/imap.js download <uid> [--mailbox INBOX] [--dir <path>] [--file <filename>]
选项:
--mailbox <name>:邮箱文件夹(默认:INBOX)--dir <path>:输出目录(默认:当前目录)--file <filename>:只下载指定名称的附件(默认:下载所有附件)
search - 搜索邮件
node scripts/imap.js search [options]
选项:
--unseen:只显示未读邮件--seen:只显示已读邮件--from <email>:发件人地址包含指定内容--subject <text>:主题包含指定内容--recent <time>:最近X时间内的邮件(例如:30m、2h、7d)--since <date>:指定日期之后的邮件(YYYY-MM-DD)--before <date>:指定日期之前的邮件(YYYY-MM-DD)--limit <n>:最大结果数(默认:20)--mailbox <name>:要搜索的邮箱文件夹(默认:INBOX)
mark-read / mark-unread - 标记邮件为已读/未读
node scripts/imap.js mark-read <uid> [uid2 uid3...]
node scripts/imap.js mark-unread <uid> [uid2 uid3...]
<uid>:邮件的唯一标识符,可以指定多个UID
list-mailboxes - 列出所有可用的邮箱文件夹
node scripts/imap.js list-mailboxes
SMTP命令(发送邮件)
send - 发送邮件
node scripts/smtp.js send --to <email> --subject <text> [options]
必填选项:
--to <email>:收件人地址,多个收件人用逗号分隔--subject <text>:邮件主题,也可以使用--subject-file <file>从文件读取主题
可选选项:
--body <text>:纯文本邮件正文--html:将正文作为HTML发送--body-file <file>:从文件读取邮件正文--html-file <file>:从文件读取HTML内容--cc <email>:抄送收件人--bcc <email>:密送收件人--attach <file>:附件,多个附件用逗号分隔--from <email>:覆盖默认发件人地址
示例:
## 简单文本邮件
node scripts/smtp.js send --to recipient@example.com --subject "Hello" --body "World"
## HTML邮件
node scripts/smtp.js send --to recipient@example.com --subject "Newsletter" --html --body "<h1>Welcome</h1>"
## 带附件的邮件
node scripts/smtp.js send --to recipient@example.com --subject "Report" --body "Please find attached" --attach report.pdf
## 多个收件人
node scripts/smtp.js send --to "a@example.com,b@example.com" --cc "c@example.com" --subject "Update" --body "Team update"
test - 测试SMTP连接
发送测试邮件到自己的邮箱,测试SMTP连接是否正常:
node scripts/smtp.js test
工具代码说明
smtp.js(SMTP邮件发送脚本)
#!/usr/bin/env node
/**
* SMTP Email CLI
* 通过SMTP协议发送邮件,支持Gmail、Outlook、163.com等标准SMTP服务器
* 支持附件、HTML内容和多个收件人
*/
const nodemailer = require('nodemailer');
const path = require('path');
const os = require('os');
const fs = require('fs');
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
/**
* 验证文件读取路径是否在允许的目录内
* @param {string} inputPath - 要验证的文件路径
* @returns {string} - 解析后的真实路径
* @throws {Error} - 如果路径不在允许的目录内则抛出错误
*/
function validateReadPath(inputPath) {
let realPath;
try {
realPath = fs.realpathSync(inputPath);
} catch {
realPath = path.resolve(inputPath);
}
const allowedDirsStr = process.env.ALLOWED_READ_DIRS;
if (!allowedDirsStr) {
throw new Error('ALLOWED_READ_DIRS not set in .env. File read operations are disabled.');
}
const allowedDirs = allowedDirsStr.split(',').map(d =>
path.resolve(d.trim().replace(/^~/, os.homedir()))
);
const allowed = allowedDirs.some(dir =>
realPath === dir || realPath.startsWith(dir + path.sep)
);
if (!allowed) {
throw new Error(`Access denied: '${inputPath}' is outside allowed read directories`);
}
return realPath;
}
/**
* 解析命令行参数
* @returns {object} - 包含命令、选项和位置参数的对象
*/
function parseArgs() {
const args = process.argv.slice(2);
const command = args[0];
const options = {};
const positional = [];
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const value = args[i + 1];
options[key] = value || true;
if (value && !value.startsWith('--')) i++;
} else {
positional.push(arg);
}
}
return { command, options, positional };
}
/**
* 创建SMTP传输器
* @returns {object} - nodemailer传输器对象
* @throws {Error} - 如果缺少SMTP配置则抛出错误
*/
function createTransporter() {
const config = {
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === 'true', // true表示使用465端口,false表示使用其他端口
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
tls: {
rejectUnauthorized: process.env.SMTP_REJECT_UNAUTHORIZED !== 'false',
},
};
if (!config.host || !config.auth.user || !config.auth.pass) {
throw new Error('Missing SMTP configuration. Please set SMTP_HOST, SMTP_USER, and SMTP_PASS in .env');
}
return nodemailer.createTransport(config);
}
/**
* 发送邮件
* @param {object} options - 邮件选项
* @returns {object} - 发送结果对象
* @throws {Error} - 如果SMTP连接失败则抛出错误
*/
async function sendEmail(options) {
const transporter = createTransporter();
// 验证连接
try {
await transporter.verify();
console.error('SMTP server is ready to send');
} catch (err) {
throw new Error(`SMTP connection failed: ${err.message}`);
}
const mailOptions = {
from: options.from || process.env.SMTP_FROM || process.env.SMTP_USER,
to: options.to,
cc: options.cc || undefined,
bcc: options.bcc || undefined,
subject: options.subject || '(no subject)',
text: options.text || undefined,
html: options.html || undefined,
attachments: options.attachments || [],
};
// 如果既没有文本也没有HTML内容,使用默认文本
if (!mailOptions.text && !mailOptions.html) {
mailOptions.text = options.body || '';
}
const info = await transporter.sendMail(mailOptions);
return {
success: true,
messageId: info.messageId,
response: info.response,
to: mailOptions.to,
};
}
/**
* 读取附件文件
* @param {string} filePath - 附件文件路径
* @returns {object} - 附件对象
* @throws {Error} - 如果文件不存在则抛出错误
*/
function readAttachment(filePath) {
validateReadPath(filePath);
if (!fs.existsSync(filePath)) {
throw new Error(`Attachment file not found: ${filePath}`);
}
return {
filename: path.basename(filePath),
path: path.resolve(filePath),
};
}
/**
* 发送带内容的邮件
* @param {object} options - 邮件选项
* @returns {object} - 发送结果对象
*/
async function sendEmailWithContent(options) {
// 处理附件
if (options.attach) {
const attachFiles = options.attach.split(',').map(f => f.trim());
options.attachments = attachFiles.map(f => readAttachment(f));
}
return await sendEmail(options);
}
/**
* 测试SMTP连接
* @returns {object} - 测试结果对象
* @throws {Error} - 如果测试失败则抛出错误
*/
async function testConnection() {
const transporter = createTransporter();
try {
await transporter.verify();
const info = await transporter.sendMail({
from: process.env.SMTP_FROM || process.env.SMTP_USER,
to: process.env.SMTP_USER, // 发送给自己
subject: 'SMTP Connection Test',
text: 'This is a test email from the IMAP/SMTP email skill.',
html: '<p>This is a <strong>test email</strong> from the IMAP/SMTP email skill.</p>',
});
return {
success: true,
message: 'SMTP connection successful',
messageId: info.messageId,
};
} catch (err) {
throw new Error(`SMTP test failed: ${err.message}`);
}
}
/**
* 主CLI处理函数
*/
async function main() {
const { command, options, positional } = parseArgs();
try {
let result;
switch (command) {
case 'send':
if (!options.to) {
throw new Error('Missing required option: --to <email>');
}
if (!options.subject && !options['subject-file']) {
throw new Error('Missing required option: --subject <text> or --subject-file <file>');
}
// 如果指定了主题文件,读取主题内容
if (options['subject-file']) {
validateReadPath(options['subject-file']);
options.subject = fs.readFileSync(options['subject-file'], 'utf8').trim();
}
// 如果指定了正文文件,读取正文内容
if (options['body-file']) {
validateReadPath(options['body-file']);
const content = fs.readFileSync(options['body-file'], 'utf8');
if (options['body-file'].endsWith('.html') || options.html) {
options.html = content;
} else {
options.text = content;
}
} else if (options['html-file']) {
validateReadPath(options['html-file']);
options.html = fs.readFileSync(options['html-file'], 'utf8');
} else if (options.body) {
options.text = options.body;
}
result = await sendEmailWithContent(options);
break;
case 'test':
result = await testConnection();
break;
default:
console.error('Unknown command:', command);
console.error('Available commands: send, test');
console.error('\nUsage:');
console.error(' send --to <email> --subject <text> [--body <text>] [--html] [--cc <email>] [--bcc <email>] [--attach <file>]');
console.error(' send --to <email> --subject <text> --body-file <file> [--html-file <file>] [--attach <file>]');
console.error(' test Test SMTP connection');
process.exit(1);
}
console.log(JSON.stringify(result, null, 2));
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}
}
main();
imap.js(IMAP邮件接收脚本)
/**
* IMAP Email CLI
* 适用于任何标准IMAP服务器(Gmail、ProtonMail Bridge、Fastmail等)
* 支持IMAP ID扩展(RFC 2971),兼容163.com等服务器
*/
const Imap = require('imap');
const { simpleParser } = require('mailparser');
const path = require('path');
const fs = require('fs');
const os = require('os');
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
/**
* 验证文件写入路径是否在允许的目录内
* @param {string} dirPath - 要验证的目录路径
* @returns {string} - 解析后的真实路径
* @throws {Error} - 如果路径不在允许的目录内则抛出错误
*/
function validateWritePath(dirPath) {
const allowedDirsStr = process.env.ALLOWED_WRITE_DIRS;
if (!allowedDirsStr) {
throw new Error('ALLOWED_WRITE_DIRS not set in .env. Attachment download is disabled.');
}
const resolved = path.resolve(dirPath.replace(/^~/, os.homedir()));
const allowedDirs = allowedDirsStr.split(',').map(d =>
path.resolve(d.trim().replace(/^~/, os.homedir()))
);
const allowed = allowedDirs.some(dir =>
resolved === dir || resolved.startsWith(dir + path.sep)
);
if (!allowed) {
throw new Error(`Access denied: '${dirPath}' is outside allowed write directories`);
}
return resolved;
}
/**
* 清理文件名,防止路径遍历攻击
* @param {string} filename - 要清理的文件名
* @returns {string} - 清理后的文件名
*/
function sanitizeFilename(filename) {
return path.basename(filename).replace(/\.\./g, '').replace(/^[./\\]/, '') || 'attachment';
}
// 用于163.com兼容性的IMAP ID信息
const IMAP_ID = {
name: 'openclaw',
version: '0.0.1',
vendor: 'netease',
'support-email': 'kefu@188.com'
};
const DEFAULT_MAILBOX = process.env.IMAP_MAILBOX || 'INBOX';
/**
* 解析命令行参数
* @returns {object} - 包含命令、选项和位置参数的对象
*/
function parseArgs() {
const args = process.argv.slice(2);
const command = args[0];
const options = {};
const positional = [];
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const value = args[i + 1];
options[key] = value || true;
if (value && !value.startsWith('--')) i++;
} else {
positional.push(arg);
}
}
return { command, options, positional };
}
/**
* 创建IMAP配置
* @returns {object} - IMAP配置对象
*/
function createImapConfig() {
return {
user: process.env.IMAP_USER,
password: process.env.IMAP_PASS,
host: process.env.IMAP_HOST || '127.0.0.1',
port: parseInt(process.env.IMAP_PORT) || 1143,
tls: process.env.IMAP_TLS === 'true',
tlsOptions: {
rejectUnauthorized: process.env.IMAP_REJECT_UNAUTHORIZED !== 'false',
},
connTimeout: 10000,
authTimeout: 10000,
};
}
/**
* 连接到IMAP服务器
* @returns {object} - IMAP连接对象
* @throws {Error} - 如果缺少IMAP用户或密码则抛出错误
*/
async function connect() {
const config = createImapConfig();
if (!config.user || !config.password) {
throw new Error('Missing IMAP_USER or IMAP_PASS environment variables');
}
return new Promise((resolve, reject) => {
const imap = new Imap(config);
imap.once('ready', () => {
// 发送IMAP ID命令以兼容163.com
if (typeof imap.id === 'function') {
imap.id(IMAP_ID, (err) => {
if (err) {
console.warn('Warning: IMAP ID command failed:', err.message);
}
resolve(imap);
});
} else {
// 如果不支持ID命令,直接继续
resolve(imap);
}
});
imap.once('error', (err) => {
reject(new Error(`IMAP connection failed: ${err.message}`));
});
imap.connect();
});
}
/**
* 打开邮箱文件夹
* @param {object} imap - IMAP连接对象
* @param {string} mailbox - 邮箱文件夹名称
* @param {boolean} readOnly - 是否以只读模式打开
* @returns {object} - 邮箱文件夹对象
*/
function openBox(imap, mailbox, readOnly = false) {
return new Promise((resolve, reject) => {
imap.openBox(mailbox, readOnly, (err, box) => {
if (err) reject(err);
else resolve(box);
});
});
}
/**
* 搜索邮件
* @param {object} imap - IMAP连接对象
* @param {array} criteria - 搜索条件
* @param {object} fetchOptions - 获取选项
* @returns {array} - 邮件结果数组
*/
function searchMessages(imap, criteria, fetchOptions) {
return new Promise((resolve, reject) => {
imap.search(criteria, (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) => {
const parts = [];
msg.on('body', (stream, info) => {
let buffer = '';
stream.on('data', (chunk) => {
buffer += chunk.toString('utf8');
});
stream.once('end', () => {
parts.push({ which: info.which, body: buffer });
});
});
msg.once('attributes', (attrs) => {
parts.forEach((part) => {
part.attributes = attrs;
});
});
msg.once('end', () => {
if (parts.length > 0) {
messages.push(parts[0]);
}
});
});
fetch.once('error', (err) => {
reject(err);
});
fetch.once('end', () => {
resolve(messages);
});
});
});
}
/**
* 解析原始邮件内容
* @param {string} bodyStr - 原始邮件内容
* @param {boolean} includeAttachments - 是否包含附件内容
* @returns {object} - 解析后的邮件对象
*/
async function parseEmail(bodyStr, includeAttachments = false) {
const parsed = await simpleParser(bodyStr);
return {
from: parsed.from?.text || 'Unknown',
to: parsed.to?.text,
subject: parsed.subject || '(no subject)',
date: parsed.date,
text: parsed.text,
html: parsed.html,
snippet: parsed.text
? parsed.text.slice(0, 200)
: (parsed.html ? parsed.html.slice(0, 200).replace(/<[^>]*>/g, '') : ''),
attachments: parsed.attachments?.map((a) => ({
filename: a.filename,
contentType: a.contentType,
size: a.size,
content: includeAttachments ? a.content : undefined,
cid: a.cid,
})),
};
}
/**
* 检查新邮件/未读邮件
* @param {string} mailbox - 邮箱文件夹名称
* @param {number} limit - 最大结果数
* @param {string} recentTime - 最近时间范围
* @param {boolean} unreadOnly - 是否只显示未读邮件
* @returns {array} - 邮件结果数组
*/
async function checkEmails(mailbox = DEFAULT_MAILBOX, limit = 10, recentTime = null, unreadOnly = false) {
const imap = await connect();
try {
await openBox(imap, mailbox);
// 构建搜索条件
const searchCriteria = unreadOnly ? ['UNSEEN'] : ['ALL'];
if (recentTime) {
const sinceDate = parseRelativeTime(recentTime);
searchCriteria.push(['SINCE', sinceDate]);
}
// 获取按日期排序的邮件(最新的在前)
const fetchOptions = {
bodies: [''],
markSeen: false,
};
const messages = await searchMessages(imap, searchCriteria, fetchOptions);
// 按日期排序(最新的在前)
const sortedMessages = messages.sort((a, b) => {
const dateA = a.attributes.date ? new Date(a.attributes.date) : new Date(0);
const dateB = b.attributes.date ? new Date(b.attributes.date) : new Date(0);
return dateB - dateA;
}).slice(0, limit);
const results = [];
for (const item of sortedMessages) {
const bodyStr = item.body;
const parsed = await parseEmail(bodyStr);
results.push({
uid: item.attributes.uid,
...parsed,
flags: item.attributes.flags,
});
}
return results;
} finally {
imap.end();
}
}
/**
* 根据UID获取完整邮件
* @param {string} uid - 邮件UID
* @param {string} mailbox - 邮箱文件夹名称
* @returns {object} - 邮件对象
* @throws {Error} - 如果邮件不存在则抛出错误
*/
async function fetchEmail(uid, mailbox = DEFAULT_MAILBOX) {
const imap = await connect();
try {
await openBox(imap, mailbox);
const searchCriteria = [['UID', uid]];
const fetchOptions = {
bodies: [''],
markSeen: false,
};
const messages = await searchMessages(imap, searchCriteria, fetchOptions);
if (messages.length === 0) {
throw new Error(`Message UID ${uid} not found`);
}
const item = messages[0];
const parsed = await parseEmail(item.body);
return {
uid: item.attributes.uid,
...parsed,
flags: item.attributes.flags,
};
} finally {
imap.end();
}
}
/**
* 下载邮件附件
* @param {string} uid - 邮件UID
* @param {string} mailbox - 邮箱文件夹名称
* @param {string} outputDir - 输出目录
* @param {string} specificFilename - 要下载的特定附件名称
* @returns {object} - 下载结果对象
* @throws {Error} - 如果邮件不存在则抛出错误
*/
async function downloadAttachments(uid, mailbox = DEFAULT_MAILBOX, outputDir = '.', specificFilename = null) {
const imap = await connect();
try {
await openBox(imap, mailbox);
const searchCriteria = [['UID', uid]];
const fetchOptions = {
bodies: [''],
markSeen: false,
};
const messages = await searchMessages(imap, searchCriteria, fetchOptions);
if (messages.length === 0) {
throw new Error(`Message UID ${uid} not found`);
}
const item = messages[0];
const parsed = await parseEmail(item.body, true);
if (!parsed.attachments || parsed.attachments.length === 0) {
return {
uid,
downloaded: [],
message: 'No attachments found',
};
}
// 创建输出目录(如果不存在)
const resolvedDir = validateWritePath(outputDir);
if (!fs.existsSync(resolvedDir)) {
fs.mkdirSync(resolvedDir, { recursive: true });
}
const downloaded = [];
for (const attachment of parsed.attachments) {
// 如果指定了特定文件名,只下载匹配的附件
if (specificFilename && attachment.filename !== specificFilename) {
continue;
}
if (attachment.content) {
const filePath = path.join(resolvedDir, sanitizeFilename(attachment.filename));
fs.writeFileSync(filePath, attachment.content);
downloaded.push({
filename: attachment.filename,
path: filePath,
size: attachment.size,
});
}
}
// 如果请求了特定文件但未找到
if (specificFilename && downloaded.length === 0) {
const availableFiles = parsed.attachments.map(a => a.filename).join(', ');
return {
uid,
downloaded: [],
message: `File "${specificFilename}" not found. Available attachments: ${availableFiles}`,
};
}
return {
uid,
downloaded,
message: `Downloaded ${downloaded.length} attachment(s)`,
};
} finally {
imap.end();
}
}
/**
* 解析相对时间(例如:"2h"、"30m"、"7d")为Date对象
* @param {string} timeStr - 相对时间字符串
* @returns {Date} - 解析后的Date对象
* @throws {Error} - 如果时间格式无效则抛出错误
*/
function parseRelativeTime(timeStr) {
const match = timeStr.match(/^(\d+)(m|h|d)$/);
if (!match) {
throw new Error('Invalid time format. Use: 30m, 2h, 7d');
}
const value = parseInt(match[1]);
const unit = match[2];
const now = new Date();
switch (unit) {
case 'm': // 分钟
return new Date(now.getTime() - value * 60 * 1000);
case 'h': // 小时
return new Date(now.getTime() - value * 60 * 60 * 1000);
case 'd': // 天
return new Date(now.getTime() - value * 24 * 60 * 60 * 1000);
default:
throw new Error('Unknown time unit');
}
}
/**
* 搜索邮件
* @param {object} options - 搜索选项
* @returns {array} - 邮件结果数组
*/
async function searchEmails(options) {
const imap = await connect();
try {
const mailbox = options.mailbox || DEFAULT_MAILBOX;
await openBox(imap, mailbox);
const criteria = [];
if (options.unseen) criteria.push('UNSEEN');
if (options.seen) criteria.push('SEEN');
if (options.from) criteria.push(['FROM', options.from]);
if (options.subject) criteria.push(['SUBJECT', options.subject]);
// 处理相对时间(--recent 2h)
if (options.recent) {
const sinceDate = parseRelativeTime(options.recent);
criteria.push(['SINCE', sinceDate]);
} else {
// 处理绝对日期
if (options.since) criteria.push(['SINCE', options.since]);
if (options.before) criteria.push(['BEFORE', options.before]);
}
// 如果没有条件,默认搜索所有邮件
if (criteria.length === 0) criteria.push('ALL');
const fetchOptions = {
bodies: [''],
markSeen: false,
};
const messages = await searchMessages(imap, criteria, fetchOptions);
const limit = parseInt(options.limit) || 20;
const results = [];
// 按日期排序(最新的在前)
const sortedMessages = messages.sort((a, b) => {
const dateA = a.attributes.date ? new Date(a.attributes.date) : new Date(0);
const dateB = b.attributes.date ? new Date(b.attributes.date) : new Date(0);
return dateB - dateA;
}).slice(0, limit);
for (const item of sortedMessages) {
const parsed = await parseEmail(item.body);
results.push({
uid: item.attributes.uid,
...parsed,
flags: item.attributes.flags,
});
}
return results;
} finally {
imap.end();
}
}
/**
* 标记邮件为已读
* @param {array} uids - 邮件UID数组
* @param {string} mailbox - 邮箱文件夹名称
* @returns {object} - 操作结果对象
*/
async function markAsRead(uids, mailbox = DEFAULT_MAILBOX) {
const imap = await connect();
try {
await openBox(imap, mailbox);
return new Promise((resolve, reject) => {
imap.addFlags(uids, '\\Seen', (err) => {
if (err) reject(err);
else resolve({ success: true, uids, action: 'marked as read' });
});
});
} finally {
imap.end();
}
}
/**
* 标记邮件为未读
* @param {array} uids - 邮件UID数组
* @param {string} mailbox - 邮箱文件夹名称
* @returns {object} - 操作结果对象
*/
async function markAsUnread(uids, mailbox = DEFAULT_MAILBOX) {
const imap = await connect();
try {
await openBox(imap, mailbox);
return new Promise((resolve, reject) => {
imap.delFlags(uids, '\\Seen', (err) => {
if (err) reject(err);
else resolve({ success: true, uids, action: 'marked as unread' });
});
});
} finally {
imap.end();
}
}
/**
* 列出所有邮箱文件夹
* @returns {array} - 邮箱文件夹数组
*/
async function listMailboxes() {
const imap = await connect();
try {
return new Promise((resolve, reject) => {
imap.getBoxes((err, boxes) => {
if (err) reject(err);
else resolve(formatMailboxTree(boxes));
});
});
} finally {
imap.end();
}
}
/**
* 格式化邮箱文件夹树
* @param {object} boxes - 邮箱文件夹对象
* @param {string} prefix - 文件夹前缀
* @returns {array} - 格式化后的邮箱文件夹数组
*/
function formatMailboxTree(boxes, prefix = '') {
const result = [];
for (const [name, info] of Object.entries(boxes)) {
const fullName = prefix ? `${prefix}${info.delimiter}${name}` : name;
result.push({
name: fullName,
delimiter: info.delimiter,
attributes: info.attribs,
});
if (info.children) {
result.push(...formatMailboxTree(info.children, fullName));
}
}
return result;
}
/**
* 主CLI处理函数
*/
async function main() {
const { command, options, positional } = parseArgs();
try {
let result;
switch (command) {
case 'check':
result = await checkEmails(
options.mailbox || DEFAULT_MAILBOX,
parseInt(options.limit) || 10,
options.recent || null,
options.unseen === 'true' // 如果设置了--unseen,只获取未读邮件
);
break;
case 'fetch':
if (!positional[0]) {
throw new Error('UID required: node imap.js fetch <uid>');
}
result = await fetchEmail(positional[0], options.mailbox);
break;
case 'download':
if (!positional[0]) {
throw new Error('UID required: node imap.js download <uid>');
}
result = await downloadAttachments(positional[0], options.mailbox, options.dir || '.', options.file || null);
break;
case 'search':
result = await searchEmails(options);
break;
case 'mark-read':
if (positional.length === 0) {
throw new Error('UID(s) required: node imap.js mark-read <uid> [uid2...]');
}
result = await markAsRead(positional, options.mailbox);
break;
case 'mark-unread':
if (positional.length === 0) {
throw new Error('UID(s) required: node imap.js mark-unread <uid> [uid2...]');
}
result = await markAsUnread(positional, options.mailbox);
break;
case 'list-mailboxes':
result = await listMailboxes();
break;
default:
console.error('Unknown command:', command);
console.error('Available commands: check, fetch, download, search, mark-read, mark-unread, list-mailboxes');
process.exit(1);
}
console.log(JSON.stringify(result, null, 2));
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}
}
main();
元数据信息
该技能的元数据信息如下:
{
"ownerId": "kn70j4ejnwqjpykvwwvgymmdcd8055qp",
"slug": "imap-smtp-email",
"version": "0.0.9",
"publishedAt": 1772525235079
}
免费 AI IDE


更多建议: