@ dianet/ 地图
一个现代的零依赖性 TypeScript IMAP 客户端库,可以恢复已中止的功能 简单 软件包.
背景情况
这个图书馆是作为两个流行但现在已经过时的IMAP图书馆的现代取而代之:
- 简单 - 一个流行的包装
imap用于简化 IMAP 操作的软件包。 最近一次更新于2020年,现已贬值. - 图像 - IMAP客户端
imap-simple取决于。 2019年最后一次更新,不再维持.
这两个图书馆都了解安全漏洞,与现代Node.js版本不相容. @dyanet/imap 以兼容的API提供新的零依赖性执行,使移徙变得直截了当.
特征
- 无依赖: 只使用已内置模块(
net, (中文(简体) ),tls, (中文(简体) ),crypto, (中文(简体) ),events(中文(简体) ) - 类型脚本第一: 在 TypeScript 中以严格模式写入, 包含完整类型定义
- 简单兼容: 方便迁移的倒置替换 API
- 承诺: 用于所有操作的现代合成/等待 API
- OAuth2 辅助: Gmail 和 Microsoft 365 的 XOOITH2 认证
- IMAP 扩展:IDLE,CONDSTORE,QRESYNC支持实时更新并高效同步
- 小脚印: 50KB 下的目标捆绑大小
安装
npm install @dyanet/imap
快速启动
import { ImapClient } from '@dyanet/imap';
const client = await ImapClient.connect({
imap: {
host: 'imap.example.com',
port: 993,
user: '[email protected]',
password: 'password',
tls: true
}
});
// Open INBOX
const mailbox = await client.openBox('INBOX');
console.log(`Total messages: ${mailbox.messages.total}`);
// Search for unread messages
const messages = await client.search(['UNSEEN'], { bodies: ['HEADER'] });
// Process messages
for (const msg of messages) {
console.log(`UID: ${msg.uid}, Flags: ${msg.attributes.flags}`);
}
// Close connection
await client.end();
建筑
图书馆采用分层结构,将关注事项分开,并实现可测试性:
┌─────────────────────────────────────────────────────────────┐
│ ImapClient (Public API) │
│ connect() | openBox() | search() | fetch() | move() | end()│
├─────────────────────────────────────────────────────────────┤
│ Command Layer │
│ CommandBuilder | ResponseParser | MimeParser │
├─────────────────────────────────────────────────────────────┤
│ Protocol Layer │
│ ImapProtocol | TagGenerator | LiteralHandler │
├─────────────────────────────────────────────────────────────┤
│ Transport Layer │
│ ImapConnection | TLS/TCP Socket Management │
└─────────────────────────────────────────────────────────────┘
图层责任
- 运输层: 使用节点管理原始 TCP/ TLS 套接字连接
net财务报告和审定财务报表tls模块 - 协议层: 执行IMAP协议解析,包括有标记/未标记的回复和文字
- 命令层: 构建 IMAP 命令并解析命令特定响应
- 公共 API: 显示用户界面
ImapClient基于承诺方法的类
API 参考
图像快捷
IMAP业务的主要客户类.
静态方法
ImapClient.connect(config: ImapConfig): Promise<ImapClient>
创建新客户端并连接到服务器.
const client = await ImapClient.connect({
imap: {
host: 'imap.example.com',
port: 993,
user: '[email protected]',
password: 'password',
tls: true,
tlsOptions: { rejectUnauthorized: true }
}
});
实例方法
end(): Promise<void>
通过发送LOGOUT命令来优雅地关闭连接 .
await client.end();
getBoxes(): Promise<MailboxTree>
列出服务器上的所有邮箱.
const boxes = await client.getBoxes();
// { INBOX: { attribs: [], delimiter: '/', children: {...} } }
openBox(mailboxName: string, readOnly?: boolean): Promise<Mailbox>
打开用于操作的邮箱.
const mailbox = await client.openBox('INBOX');
console.log(mailbox.messages.total); // Total message count
console.log(mailbox.messages.unseen); // Unseen message count
addBox(mailboxName: string): Promise<void>
创建新邮箱.
await client.addBox('Archive');
delBox(mailboxName: string): Promise<void>
删除邮箱.
await client.delBox('OldFolder');
renameBox(oldName: string, newName: string): Promise<void>
重命名邮箱.
await client.renameBox('OldName', 'NewName');
search(criteria: SearchCriteria[]): Promise<number[]>
search(criteria: SearchCriteria[], fetchOptions: FetchOptions): Promise<Message[]>
搜索符合给定标准的信息.
当呼叫没有 fetchOptions,返回一个 UID 数组(number[]) (中文(简体) ).
当呼叫时 fetchOptions,返回数组 Message 已获取数据的对象.
// Search for UIDs only
const uids = await client.search(['UNSEEN']);
console.log(uids); // [1, 2, 3]
// Search and fetch message data
const messages = await client.search(['UNSEEN'], { bodies: ['HEADER'] });
// Multiple criteria (AND logic)
const messages = await client.search([
'UNSEEN',
['FROM', '[email protected]'],
['SINCE', new Date('2024-01-01')]
], { bodies: ['HEADER'] });
fetch(source: string | number[], fetchOptions: FetchOptions): Promise<Message[]>
通过 UID 获取信件 .
const messages = await client.fetch([1, 2, 3], {
bodies: ['HEADER', 'TEXT'],
struct: true,
envelope: true
});
addFlags(uids: number[], flags: string[]): Promise<void>
在信件中添加旗帜 .
await client.addFlags([1, 2, 3], ['\\Seen', '\\Flagged']);
delFlags(uids: number[], flags: string[]): Promise<void>
从信件中删除旗帜 .
await client.delFlags([1, 2, 3], ['\\Seen']);
move(uids: number[], destBox: string): Promise<void>
将信件移动到另一个邮箱(复制后标记为已删除).
await client.move([1, 2, 3], 'Archive');
await client.expunge(); // Remove originals
copy(uids: number[], destBox: string): Promise<void>
复制信件到另一个邮箱 .
await client.copy([1, 2, 3], 'Backup');
expunge(): Promise<void>
永久删除标有 \Deleted 旗帜.
await client.expunge();
idle(): Promise<IdleController>
输入IDLE模式用于实时邮箱通知(RFC 217).
const idle = await client.idle();
idle.on('exists', (count) => {
console.log(`New message count: ${count}`);
});
idle.on('expunge', (seqno) => {
console.log(`Message ${seqno} was expunged`);
});
// Stop IDLE when done
await idle.stop();
watch(options?: { pollInterval?: number }): Promise<IdleController>
开始观看邮箱更改 。 如果支持使用IDLE,否则会回到投票.
const watcher = await client.watch({ pollInterval: 30000 });
watcher.on('exists', (count) => {
console.log(`New message count: ${count}`);
});
await watcher.stop();
poll(): Promise<IdleNotification[]>
使用 NOOP 命令对邮箱进行修改。 没有 IDLE 支持的服务器倒置 .
const notifications = await client.poll();
for (const n of notifications) {
if (n.type === 'exists') {
console.log(`New message count: ${n.count}`);
}
}
openBoxWithQresync(mailboxName, qresync, readOnly?): Promise<QresyncResult>
开通了QRESYNC的邮箱快速再同步(RFC 7162).
// Save state from previous session
const savedState = {
uidValidity: box.uidvalidity,
lastKnownModseq: box.highestModseq
};
// Later session - resync efficiently
const result = await client.openBoxWithQresync('INBOX', savedState);
console.log('Vanished UIDs:', result.vanished);
hasCapability(capability: string): boolean
检查服务器是否支持特定的能力 .
if (client.hasCapability('IDLE')) {
const idle = await client.idle();
}
hasCondstore(): boolean
检查服务器是否支持 CONDSTORE 扩展( RFC 7162) 高效旗下同步.
hasQresync(): boolean
检查服务器是否支持QRESYNC扩展(RFC 7162)进行快信箱再同步.
getCapabilities(): Set<string>
返回所有服务器能力 .
refreshCapabilities(): Promise<Set<string>>
刷新并返回服务器能力 .
属性
isConnected: boolean- 目前客户端是否已连接selectedMailbox: Mailbox | null- 目前选定的邮箱
活动
error- 当连接出错时被解禁close- 连接关闭时被放出
client.on('error', (err) => console.error('Connection error:', err));
client.on('close', () => console.log('Connection closed'));
配置
interface ImapConfig {
imap: {
host: string; // IMAP server hostname
port?: number; // Port (default: 993)
user: string; // Username
password?: string; // Password (for basic auth)
xoauth2?: XOAuth2Options; // OAuth2 auth (for Gmail, Microsoft 365)
tls?: boolean; // Use TLS (default: true)
tlsOptions?: TlsOptions;
authTimeout?: number; // Auth timeout in ms (default: 30000)
connTimeout?: number; // Connection timeout in ms (default: 30000)
};
}
interface XOAuth2Options {
user: string; // User email address
accessToken: string; // OAuth2 access token
}
interface TlsOptions {
rejectUnauthorized?: boolean;
ca?: string | Buffer | string[] | Buffer[];
cert?: string | Buffer;
key?: string | Buffer;
}
OAuth2 认证 (Gmail, Microsoft 365)
对于Gmail和Microsoft 365,基本密码认证被贬值. 替换使用 OAuth2 :
// Gmail with OAuth2
const client = await ImapClient.connect({
imap: {
host: 'imap.gmail.com',
port: 993,
user: '[email protected]',
xoauth2: {
user: '[email protected]',
accessToken: 'ya29.your-oauth2-access-token'
},
tls: true
}
});
// Microsoft 365 with OAuth2
const client = await ImapClient.connect({
imap: {
host: 'outlook.office365.com',
port: 993,
user: '[email protected]',
xoauth2: {
user: '[email protected]',
accessToken: 'eyJ0eXAiOiJKV1Q...'
},
tls: true
}
});
搜索标准
支持的搜索标准 :
| 标准 | 说明 |
|---|---|
'ALL' | 所有信件 |
'UNSEEN' | 未读信件 |
'SEEN' | 读取信件 |
'FLAGGED' | 标记信件 |
'UNFLAGGED' | 未标记的消息 |
'ANSWERED' | 已回复信件 |
'DELETED' | 删除信件 |
'DRAFT' | 电文草稿 |
'NEW' | 新消息 |
'RECENT' | 最近的消息 |
['FROM', string] | 从地址包含 |
['TO', string] | 地址包含 |
['CC', string] | CC 地址包含 |
['SUBJECT', string] | 主题包含 |
['BODY', string] | 包含 |
['SINCE', Date] | 至今 |
['BEFORE', Date] | 日期之前 |
['ON', Date] | 日期 |
['LARGER', number] | 大于字节 |
['SMALLER', number] | 比字节小 |
['UID', string] | UID 序列 |
['HEADER', field, value] | 页眉字段包含 |
获取选项
interface FetchOptions {
bodies?: string | string[]; // Body parts to fetch
struct?: boolean; // Include body structure
envelope?: boolean; // Include envelope
size?: boolean; // Include size
markSeen?: boolean; // Mark as seen when fetching
}
机体部件( E)bodies 选项)
这个 bodies 选项接受各种格式来获取消息的不同部分:
| 数值 | 说明 |
|---|---|
'HEADER' | 所有信件头 |
'HEADER.FIELDS (FROM SUBJECT DATE)' | 仅指定标题 |
'HEADER.FIELDS.NOT (BCC)' | 除指定外,所有信头 |
'TEXT' | 信件正文( 没有信头) |
'' (空字符串) | 全部信息(标题+正文) |
'1' | 第一个MIME部分 |
'1.2' | 嵌入式MIME部分(第1部分第2部分) |
'1.HEADER' | 特定MIME部分的标题 |
'1.TEXT' | 具体多边措施部分的正文 |
示例用法:
// Fetch only specific headers
const messages = await client.fetch(uids, {
bodies: ['HEADER.FIELDS (FROM SUBJECT DATE)']
});
// Fetch headers and body text
const messages = await client.fetch(uids, {
bodies: ['HEADER', 'TEXT']
});
// Fetch entire message
const messages = await client.fetch(uids, {
bodies: ['']
});
页眉解析
图书馆输出 a parseHeaders() 用于解析原始标题文本的工具 :
import { parseHeaders } from '@dyanet/imap';
// Parse headers from a message part
const headerPart = message.parts.find(p => p.which.includes('HEADER'));
const headers = parseHeaders(headerPart.body.toString());
// Access headers (keys are lowercase)
const subject = headers.get('subject'); // string | string[]
const from = headers.get('from');
const date = headers.get('date');
标准旗帜
\Seen- 信件已经读取\Answered- 信件已回复\Flagged- 信息被标记\Deleted- 信件被标记为删除\Draft- 讯息是草稿
扩展能力
库为高级功能支持了多个IMAP扩展. 使用 hasCapability() 在使用这些特性之前检查服务器支持.
IDLE (RFC 2177) (法语)
没有投票的实时邮箱通知.
if (client.hasCapability('IDLE')) {
const idle = await client.idle();
idle.on('exists', (count) => console.log(`Messages: ${count}`));
idle.on('expunge', (seqno) => console.log(`Deleted: ${seqno}`));
idle.on('fetch', ({ seqno, flags }) => console.log(`Flags changed: ${seqno}`));
// Stop when done
await idle.stop();
}
联系(RFC 7162)
使用 MODSEQ 值进行高效旗帜同步的有条件STORE.
if (client.hasCondstore()) {
const mailbox = await client.openBox('INBOX');
// mailbox.highestModseq contains the current MODSEQ value
console.log(`Highest MODSEQ: ${mailbox.highestModseq}`);
}
QRESYNC (RFC 7162) (中文(简体) )
快速邮箱再同步 - 重接后高效同步.
if (client.hasQresync()) {
// Save state when disconnecting
const savedState = {
uidValidity: mailbox.uidvalidity,
lastKnownModseq: mailbox.highestModseq
};
// On reconnect, get only changes
const result = await client.openBoxWithQresync('INBOX', savedState);
console.log('Expunged UIDs:', result.vanished);
console.log('Current mailbox:', result.mailbox);
}
能力检测
// Check specific capability
if (client.hasCapability('IDLE')) { /* ... */ }
// Get all capabilities
const caps = client.getCapabilities();
console.log('Server supports:', [...caps].join(', '));
// Refresh capabilities (after authentication changes)
await client.refreshCapabilities();
处理错误
库对不同的错误类别使用打入错误分类 :
import {
ImapError,
ImapProtocolError,
ImapNetworkError,
ImapParseError,
ImapTimeoutError
} from '@dyanet/imap';
try {
await client.openBox('NonExistent');
} catch (err) {
if (err instanceof ImapProtocolError) {
console.log('Server error:', err.serverResponse);
} else if (err instanceof ImapNetworkError) {
console.log('Network error:', err.host, err.port);
} else if (err instanceof ImapTimeoutError) {
console.log('Timeout:', err.operation, err.timeoutMs);
}
}
| 错误类 | 来源 | 包含 |
|---|---|---|
ImapProtocolError | 服务器 NO/BAD 响应 | 服务器响应文本, 命令 |
ImapNetworkError | 连接失败 | 主机、 端口、 内在错误 |
ImapParseError | 答复不当 | 无法解析的原始数据 |
ImapTimeoutError | 运行超时 | 操作名称、暂停时间 |
A. 从简单移民
这个库是用来代替 Imp-simp 的:
// Before (imap-simple)
import imapSimple from 'imap-simple';
const connection = await imapSimple.connect(config);
// After (@dyanet/imap)
import { ImapClient } from '@dyanet/imap';
const connection = await ImapClient.connect(config);
配置格式和方法签名与imap-simp兼容.
试试看自己的邮箱
进来的两条路,取决于你是否想看图书馆的工作还是看一个 真实的应用程序建在它上.
📹 一个视频的行走即将到来. 在此之前,以下书面步骤 是完整的路径.
1. 实例应用软件——这是我们用它来测试的
examples/gmail-viewer/ 是一个完整的Express 应用程序,将签名输入 Gmail over
OAuth2并浏览您的收件箱. 我们用它来练习
库对一个活服务器,所以这是最快的确认方法
工作结束到结束.
git clone https://github.com/dyanet/imap.git
cd imap/examples/gmail-viewer
npm install
cp .env.example .env # then fill in the two Google values below
npm run dev # builds and starts on http://localhost:3000
| 命令 | 它会做什么 |
|---|---|
npm run dev | 构建然后启动( 先使用) |
npm run build | 编译类型脚本到 dist/ |
npm start | 运行已建的 dist/server.js |
npm run verify | 不排放类型检查 |
获得两个Google值. 在那个
谷歌云控台数字 :
创建( 或选择) 项目 启用 Gmail API 邮件 → 创建
证书 – OAuth 客户端 ID – Web 应用程序 • 添加
http://localhost:3000/callback 作为授权的重定向URI. 复制
客户身份和秘密进入 .env 作为 GOOGLE_CLIENT_ID 财务报告和审定财务报表
GOOGLE_CLIENT_SECRET,然后打开 http://localhost:3000 并签入.
详细情况,包括部署情况
examples/gmail-viewer/README.md。 。 。 .
关于展望: 发送时的示例应用程序是 Gmail- only —— 其 OAuth 流 被硬码到谷歌的终点。 指着Outlook意味着 在 Microsoft Entra ID 中注册一个应用程序并交换授权/托肯 URL和范围. 下面的图书馆是提供者不可知的;见下文.
2. 直接针对Gmail或Outlook的图书馆
ImapConfig 使用任何主机或密码或 XOUSTH2,所以不
需要实例应用程序来尝试邮箱.
邮件 ——最快的路线,因为Google仍然接受应用密码 为IMAP. 开启二步验证,然后在 (原始内容存档于2018-03-27). my account.google.com/appasswords数字 :
import { ImapClient } from '@dyanet/imap';
const client = new ImapClient({
imap: {
host: 'imap.gmail.com',
port: 993,
user: '[email protected]',
password: 'your-16-char-app-password', // not your account password
tls: true,
},
});
await client.connect();
const box = await client.openBox('INBOX');
console.log(`${box.messages.total} messages`);
await client.end();
展望/微软365 —— 说 应用程序密码在这里不再有效 .
微软完成了IMAP、POP和SMTP的基础认证
; 应用程序密码依赖它,停止运行,不能是
重新生成。 OAuth2 是唯一的路径, 因此 Outlook 需要 xoauth2 相当
比起来 password数字 :
const client = new ImapClient({
imap: {
// Microsoft 365 / work accounts. Personal outlook.com and hotmail.com
// mailboxes use imap-mail.outlook.com.
host: 'outlook.office365.com',
port: 993,
user: '[email protected]',
xoauth2: {
user: '[email protected]',
accessToken: entraAccessToken, // from Microsoft Entra ID
},
tls: true,
},
});
获取 entraAccessToken 通过在 微软环境
身份证,给予它 IMAP.AccessAsUser.All 授权,以及
运行标准授权码流。 见
微软IMAP OAuth2 指南。 。 。 .
3. 不需要邮箱
工匠们 运行此库的 MIME 和 RFC 2047 标题在浏览器中解析 – 粘贴 生头并看解码. 没有安装和账户 所需经费.
类型脚本支持
严格模式的完整类型脚本支持. 所有公共类型都导出 :
import type {
ImapConfig,
TlsOptions,
Mailbox,
MailboxTree,
Message,
MessagePart,
SearchCriteria,
FetchOptions,
Envelope,
BodyStructure,
Address
} from '@dyanet/imap';
所需资源
- 节点.js >= 20.0.0
许可证
麻省理工学院