OpenClaw Skills 邮件管理(imap-smtp-email)技能使用参考手册

2026-03-06 15:13 更新

概述

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_PASSSMTP_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
}
以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号