dsh-email 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/lib/config.d.ts +28 -8
- package/lib/config.js +80 -22
- package/lib/index.d.ts +3 -3
- package/lib/index.js +157 -68
- package/lib/mail-client.d.ts +36 -13
- package/lib/mail-client.js +286 -72
- package/lib/parse.d.ts +5 -0
- package/lib/parse.js +25 -6
- package/lib/types.d.ts +51 -9
- package/package.json +62 -53
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# dsh-email
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness 邮件工具插件:让 agent
|
|
3
|
+
DeepSeek Harness 邮件工具插件:让 agent 能**查收件箱、读邮件、搜邮件、代发邮件、收发附件**。纯插件实现,零核心改动,安装即可用。
|
|
4
4
|
|
|
5
|
-
Email tools for DeepSeek Harness: list, read, search and send mail through standard IMAP/SMTP — with one-line presets for QQ / 163 / 126 / Sina / Aliyun / Gmail / Outlook / iCloud.
|
|
5
|
+
Email tools for DeepSeek Harness: list, read, search and send mail through standard IMAP/SMTP — with one-line presets for QQ / 163 / 126 / Sina / Aliyun / Gmail / Outlook / iCloud.
|
|
6
|
+
|
|
7
|
+
纯 Node 实现,**全平台通用**(Windows / macOS / Linux 同一份代码),不依赖 shell、无原生二进制。
|
|
6
8
|
|
|
7
9
|
## 工具一览
|
|
8
10
|
|
|
@@ -11,7 +13,9 @@ Email tools for DeepSeek Harness: list, read, search and send mail through stand
|
|
|
11
13
|
| `email_list` | 列出文件夹里最新的邮件(未读过滤、分页、只看摘要不带正文) |
|
|
12
14
|
| `email_read` | 按 uid 读取一封邮件的全文(HTML 邮件自动转纯文本,超长截断) |
|
|
13
15
|
| `email_search` | 按关键词搜索发件人/收件人/主题(服务器端 IMAP SEARCH,不搜正文) |
|
|
14
|
-
| `email_send` |
|
|
16
|
+
| `email_send` | 代发邮件(支持带附件)。**默认发信前会弹确认**,显示收件人、主题和附件数,由你批准后才发出 |
|
|
17
|
+
| `email_folders` | 列出邮箱的文件夹(INBOX/已发送/垃圾邮件/自定义…),拿 path 喂给其他工具 |
|
|
18
|
+
| `email_attachment` | 按序号下载邮件附件到本地文件(大小受 maxAttachmentBytes 限制) |
|
|
15
19
|
|
|
16
20
|
示例对话:
|
|
17
21
|
|
|
@@ -51,6 +55,20 @@ dsh plugin --profile web add dsh-email
|
|
|
51
55
|
inboxFolder: INBOX
|
|
52
56
|
```
|
|
53
57
|
|
|
58
|
+
多账号:一个 `tool-email` 行可以配多个邮箱,工具调用时用 `account` 参数选择:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
- id: tool-email
|
|
62
|
+
config:
|
|
63
|
+
accounts:
|
|
64
|
+
work: { provider: qq, user: work@qq.com, password: 授权码1 }
|
|
65
|
+
home: { provider: '163', user: home@163.com, password: 授权码2 }
|
|
66
|
+
defaultAccount: work # 省略 account 参数时用这个
|
|
67
|
+
downloadDir: E:/attachments # 可选,默认 $DSH_HOME/email-downloads
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
顶层的 `provider`/`user`/`password`/`imap`/`smtp`/`inboxFolder` 仍然可用,作为各账号的共享默认值(v0.1 单账号写法完全兼容)。
|
|
71
|
+
|
|
54
72
|
### 常用邮箱预设
|
|
55
73
|
|
|
56
74
|
| provider | IMAP | SMTP |
|
|
@@ -75,7 +93,12 @@ dsh plugin --profile web add dsh-email
|
|
|
75
93
|
| `smtp.host/port/secure` | 按预设 | 发信服务器 |
|
|
76
94
|
| `inboxFolder` | `INBOX` | 收发工具默认使用的文件夹 |
|
|
77
95
|
| `sendApproval` | `true` | 发信前弹确认(强烈建议保留) |
|
|
78
|
-
| `maxBodyChars` | `20000` | email_read 正文截断上限(1000–200000) |
|
|
96
|
+
| `maxBodyChars` | `20000` | email_read 正文截断上限(1000–200000) |
|
|
97
|
+
| `accounts` | 无 | 具名账号表;账号级字段覆盖顶层简写 |
|
|
98
|
+
| `defaultAccount` | 单账号时自动 | 工具省略 account 参数时使用的账号(多账号必填) |
|
|
99
|
+
| `downloadDir` | `$DSH_HOME/email-downloads` | email_attachment 的落盘目录 |
|
|
100
|
+
| `maxAttachmentBytes` | 20 MiB | 单个附件与附件总大小上限(1024–512 MiB) |
|
|
101
|
+
| `idleTimeoutMs` | `60000` | IMAP 空闲连接回收时间(连接复用,连续操作更快) |
|
|
79
102
|
|
|
80
103
|
## 第一步:拿到授权码
|
|
81
104
|
|
|
@@ -93,20 +116,20 @@ dsh plugin --profile web add dsh-email
|
|
|
93
116
|
- 注意:会话处于 **Full Access(完全访问)** 模式时,harness 会把审批策略置为 never,`email_send` 会被**静默拒绝**(不弹框)。想发信请把访问模式切回 Read Only 或 Write。
|
|
94
117
|
- 本插件不做任何联网上报,凭证只在内存中用于连接你的邮箱服务器。
|
|
95
118
|
|
|
96
|
-
## 已知限制(v0.
|
|
119
|
+
## 已知限制(v0.2)
|
|
97
120
|
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
121
|
+
- **连接复用**:IMAP 按账号池化(空闲自动回收),SMTP 用 nodemailer 连接池;同一账号的并发调用会排队串行(一个连接一次只服务一个操作,这是有意的)。
|
|
122
|
+
- **多账号**:每个账号独立连接池;一个 `tool-email` 行可以配任意多个账号。
|
|
123
|
+
- **附件下载**:email_attachment 按序号下载(与 email_read 的 attachments 顺序一致);文件名会被清洗防路径穿越,已有同名文件自动加后缀,大小受 maxAttachmentBytes 限制。
|
|
101
124
|
- **不支持 OAuth2**:强制 OAuth 的企业环境(部分 M365/Google Workspace)暂不可用。
|
|
102
|
-
- 正文搜索不提供:多数服务器(如 QQ)的 IMAP `TEXT`/`HEADER`
|
|
125
|
+
- 正文搜索不提供:多数服务器(如 QQ)的 IMAP `TEXT`/`HEADER` 搜索要么全量匹配要么不支持,所以只搜主题/发件人/收件人;正文搜索列入后续版本(需客户端下载解析,较慢)。
|
|
103
126
|
|
|
104
127
|
## 开发
|
|
105
128
|
|
|
106
129
|
```sh
|
|
107
130
|
pnpm install
|
|
108
131
|
pnpm run build # tsc → lib/
|
|
109
|
-
pnpm test # 构建 + node --test(配置/解析/注册与审批门,
|
|
132
|
+
pnpm test # 构建 + node --test(配置/解析/注册与审批门,27 个用例,无需真实邮箱)
|
|
110
133
|
```
|
|
111
134
|
|
|
112
135
|
## 协议
|
package/lib/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type ProviderName = 'qq' | '163' | '126' | 'sina' | 'aliyun' | 'gmail' | 'outlook' | 'icloud';
|
|
1
2
|
export interface ImapConfig {
|
|
2
3
|
host?: string;
|
|
3
4
|
port?: number;
|
|
@@ -10,21 +11,30 @@ export interface SmtpConfig {
|
|
|
10
11
|
port?: number;
|
|
11
12
|
secure?: boolean;
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
provider?:
|
|
16
|
-
/** Login address, e.g. you@qq.com. */
|
|
14
|
+
/** One mailbox account. Top-level shorthand fields act as shared defaults. */
|
|
15
|
+
export interface AccountConfig {
|
|
16
|
+
provider?: ProviderName;
|
|
17
17
|
user?: string;
|
|
18
|
-
/** App password / authorization code. Falls back to $DSH_EMAIL_PASSWORD. */
|
|
19
18
|
password?: string;
|
|
20
19
|
imap?: ImapConfig;
|
|
21
20
|
smtp?: SmtpConfig;
|
|
22
|
-
/** Mailbox used by the read/search/list tools. Default 'INBOX'. */
|
|
23
21
|
inboxFolder?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface EmailConfig extends AccountConfig {
|
|
24
24
|
/** Ask the user for approval before email_send. Default true. */
|
|
25
25
|
sendApproval?: boolean;
|
|
26
26
|
/** Plain-text body cap for email_read. Default 20000. */
|
|
27
27
|
maxBodyChars?: number;
|
|
28
|
+
/** Named accounts. Account-level fields override the top-level shorthand. */
|
|
29
|
+
accounts?: Record<string, AccountConfig>;
|
|
30
|
+
/** Which account tools use when the call omits account. Required with 2+ accounts. */
|
|
31
|
+
defaultAccount?: string;
|
|
32
|
+
/** Directory email_attachment writes into. Default $DSH_HOME/email-downloads. */
|
|
33
|
+
downloadDir?: string;
|
|
34
|
+
/** Per-attachment and total-attachment byte cap. Default 20 MiB. */
|
|
35
|
+
maxAttachmentBytes?: number;
|
|
36
|
+
/** Unused IMAP connections close after this many ms. Default 60000. */
|
|
37
|
+
idleTimeoutMs?: number;
|
|
28
38
|
}
|
|
29
39
|
export interface ProviderPreset {
|
|
30
40
|
imap: {
|
|
@@ -41,7 +51,7 @@ export interface ProviderPreset {
|
|
|
41
51
|
export declare const PROVIDER_PRESETS: Record<string, ProviderPreset>;
|
|
42
52
|
export declare const PROVIDER_NAMES: string[];
|
|
43
53
|
export declare const EMAIL_PASSWORD_ENV = "DSH_EMAIL_PASSWORD";
|
|
44
|
-
/** Fully resolved, validated configuration. */
|
|
54
|
+
/** Fully resolved, validated configuration for one account. */
|
|
45
55
|
export interface ResolvedEmailConfig {
|
|
46
56
|
user: string;
|
|
47
57
|
password: string;
|
|
@@ -56,14 +66,24 @@ export interface ResolvedEmailConfig {
|
|
|
56
66
|
secure: boolean;
|
|
57
67
|
};
|
|
58
68
|
inboxFolder: string;
|
|
69
|
+
}
|
|
70
|
+
/** Fully resolved plugin settings: the account map plus shared policy. */
|
|
71
|
+
export interface ResolvedEmailSettings {
|
|
72
|
+
accounts: Map<string, ResolvedEmailConfig>;
|
|
73
|
+
defaultAccount: string;
|
|
59
74
|
sendApproval: boolean;
|
|
60
75
|
maxBodyChars: number;
|
|
61
|
-
|
|
76
|
+
downloadDir: string;
|
|
77
|
+
maxAttachmentBytes: number;
|
|
78
|
+
idleTimeoutMs: number;
|
|
62
79
|
}
|
|
80
|
+
export declare function defaultDownloadDir(): string;
|
|
63
81
|
/**
|
|
64
82
|
* Resolve and validate the raw row config. Throws with an actionable message
|
|
65
83
|
* (in Chinese, since it is what the user and the model both read) when the
|
|
66
84
|
* account is not fully specified.
|
|
67
85
|
*/
|
|
86
|
+
export declare function resolveEmailSettings(config: EmailConfig | undefined): ResolvedEmailSettings;
|
|
87
|
+
/** v0.1-compatible wrapper: resolve the single (or default) account. */
|
|
68
88
|
export declare function resolveEmailConfig(config: EmailConfig | undefined): ResolvedEmailConfig;
|
|
69
89
|
export declare function clampInt(value: unknown, fallback: number, min: number, max: number): number;
|
package/lib/config.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
export const PROVIDER_PRESETS = {
|
|
2
4
|
qq: { imap: { host: 'imap.qq.com', port: 993, secure: true }, smtp: { host: 'smtp.qq.com', port: 465, secure: true } },
|
|
3
5
|
'163': { imap: { host: 'imap.163.com', port: 993, secure: true }, smtp: { host: 'smtp.163.com', port: 465, secure: true } },
|
|
@@ -10,40 +12,94 @@ export const PROVIDER_PRESETS = {
|
|
|
10
12
|
};
|
|
11
13
|
export const PROVIDER_NAMES = Object.keys(PROVIDER_PRESETS);
|
|
12
14
|
export const EMAIL_PASSWORD_ENV = 'DSH_EMAIL_PASSWORD';
|
|
15
|
+
const DEFAULT_MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
16
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 60000;
|
|
17
|
+
export function defaultDownloadDir() {
|
|
18
|
+
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
19
|
+
return join(home, 'email-downloads');
|
|
20
|
+
}
|
|
13
21
|
/**
|
|
14
22
|
* Resolve and validate the raw row config. Throws with an actionable message
|
|
15
23
|
* (in Chinese, since it is what the user and the model both read) when the
|
|
16
24
|
* account is not fully specified.
|
|
17
25
|
*/
|
|
18
|
-
export function
|
|
26
|
+
export function resolveEmailSettings(config) {
|
|
19
27
|
const raw = config ?? {};
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
const common = {
|
|
29
|
+
provider: raw.provider,
|
|
30
|
+
user: raw.user,
|
|
31
|
+
password: raw.password,
|
|
32
|
+
imap: raw.imap,
|
|
33
|
+
smtp: raw.smtp,
|
|
34
|
+
inboxFolder: raw.inboxFolder,
|
|
35
|
+
};
|
|
36
|
+
const entries = raw.accounts === undefined || Object.keys(raw.accounts).length === 0
|
|
37
|
+
? undefined
|
|
38
|
+
: raw.accounts;
|
|
39
|
+
const accounts = new Map();
|
|
40
|
+
if (entries === undefined) {
|
|
41
|
+
accounts.set('default', resolveAccount('default', common, {}, true));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
for (const [name, acc] of Object.entries(entries)) {
|
|
45
|
+
accounts.set(name, resolveAccount(name, common, acc ?? {}, false));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let defaultName;
|
|
49
|
+
if (raw.defaultAccount !== undefined && raw.defaultAccount !== '') {
|
|
50
|
+
if (!accounts.has(raw.defaultAccount)) {
|
|
51
|
+
throw new Error(`dsh-email:defaultAccount "${raw.defaultAccount}" 不存在,可用账号:${[...accounts.keys()].join('、')}`);
|
|
52
|
+
}
|
|
53
|
+
defaultName = raw.defaultAccount;
|
|
54
|
+
}
|
|
55
|
+
else if (accounts.size === 1) {
|
|
56
|
+
defaultName = [...accounts.keys()][0];
|
|
57
|
+
}
|
|
58
|
+
else if (accounts.has('default')) {
|
|
59
|
+
defaultName = 'default';
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
throw new Error(`dsh-email:配置了多个账号(${[...accounts.keys()].join('、')}),请设置 defaultAccount 指定默认账号`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
accounts,
|
|
66
|
+
defaultAccount: defaultName,
|
|
67
|
+
sendApproval: raw.sendApproval !== false,
|
|
68
|
+
maxBodyChars: clampInt(raw.maxBodyChars, 20000, 1000, 200000),
|
|
69
|
+
downloadDir: raw.downloadDir?.trim() || defaultDownloadDir(),
|
|
70
|
+
maxAttachmentBytes: clampInt(raw.maxAttachmentBytes, DEFAULT_MAX_ATTACHMENT_BYTES, 1024, 512 * 1024 * 1024),
|
|
71
|
+
idleTimeoutMs: clampInt(raw.idleTimeoutMs, DEFAULT_IDLE_TIMEOUT_MS, 5000, 600000),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Merge one account over the shared shorthand and validate it. */
|
|
75
|
+
function resolveAccount(name, common, acc, allowEnvPassword) {
|
|
76
|
+
const preset = acc.provider === undefined ? PROVIDER_PRESETS[common.provider ?? ''] : PROVIDER_PRESETS[acc.provider];
|
|
77
|
+
if ((acc.provider ?? common.provider) !== undefined && preset === undefined) {
|
|
78
|
+
throw new Error(`dsh-email:账号 "${name}" 的 provider "${acc.provider ?? common.provider}" 未知,可选:${PROVIDER_NAMES.join('/')};或省略 provider 直接填 imap.host 与 smtp.host`);
|
|
23
79
|
}
|
|
24
|
-
const user =
|
|
25
|
-
const password =
|
|
80
|
+
const user = (acc.user ?? common.user ?? '').trim();
|
|
81
|
+
const password = acc.password ?? common.password ?? (allowEnvPassword ? process.env[EMAIL_PASSWORD_ENV] ?? '' : '');
|
|
26
82
|
const imap = {
|
|
27
|
-
host:
|
|
28
|
-
port:
|
|
29
|
-
secure:
|
|
30
|
-
connectionTimeoutMs:
|
|
31
|
-
socketTimeoutMs:
|
|
83
|
+
host: acc.imap?.host ?? common.imap?.host ?? preset?.imap.host,
|
|
84
|
+
port: acc.imap?.port ?? common.imap?.port ?? preset?.imap.port,
|
|
85
|
+
secure: acc.imap?.secure ?? common.imap?.secure ?? preset?.imap.secure,
|
|
86
|
+
connectionTimeoutMs: acc.imap?.connectionTimeoutMs ?? common.imap?.connectionTimeoutMs,
|
|
87
|
+
socketTimeoutMs: acc.imap?.socketTimeoutMs ?? common.imap?.socketTimeoutMs,
|
|
32
88
|
};
|
|
33
89
|
const smtp = {
|
|
34
|
-
host:
|
|
35
|
-
port:
|
|
36
|
-
secure:
|
|
90
|
+
host: acc.smtp?.host ?? common.smtp?.host ?? preset?.smtp.host,
|
|
91
|
+
port: acc.smtp?.port ?? common.smtp?.port ?? preset?.smtp.port,
|
|
92
|
+
secure: acc.smtp?.secure ?? common.smtp?.secure ?? preset?.smtp.secure,
|
|
37
93
|
};
|
|
38
94
|
const problems = [];
|
|
39
95
|
if (user === '')
|
|
40
|
-
problems.push(
|
|
96
|
+
problems.push(`账号 "${name}" 的 user(邮箱地址)未填写`);
|
|
41
97
|
if (password === '')
|
|
42
|
-
problems.push(
|
|
98
|
+
problems.push(`账号 "${name}" 的 password 未填写(单账号可用环境变量 ${EMAIL_PASSWORD_ENV})`);
|
|
43
99
|
if (imap.host === undefined || imap.host === '')
|
|
44
|
-
problems.push(
|
|
100
|
+
problems.push(`账号 "${name}" 的 imap.host 未填写(可填 provider 预设:${PROVIDER_NAMES.join('/')})`);
|
|
45
101
|
if (smtp.host === undefined || smtp.host === '')
|
|
46
|
-
problems.push(
|
|
102
|
+
problems.push(`账号 "${name}" 的 smtp.host 未填写(同上)`);
|
|
47
103
|
if (problems.length > 0) {
|
|
48
104
|
throw new Error(`dsh-email 未配置:${problems.join(';')}。请在 profile 的 cordis.patch.yml 中覆盖 tool-email 行并重启(见插件 README)`);
|
|
49
105
|
}
|
|
@@ -52,12 +108,14 @@ export function resolveEmailConfig(config) {
|
|
|
52
108
|
password,
|
|
53
109
|
imap: { ...imap, host: imap.host, port: imap.port, secure: imap.secure },
|
|
54
110
|
smtp: { ...smtp, host: smtp.host, port: smtp.port, secure: smtp.secure },
|
|
55
|
-
inboxFolder:
|
|
56
|
-
sendApproval: raw.sendApproval !== false,
|
|
57
|
-
maxBodyChars: clampInt(raw.maxBodyChars, 20000, 1000, 200000),
|
|
58
|
-
providerName: raw.provider ?? null,
|
|
111
|
+
inboxFolder: (acc.inboxFolder ?? common.inboxFolder ?? '').trim() || 'INBOX',
|
|
59
112
|
};
|
|
60
113
|
}
|
|
114
|
+
/** v0.1-compatible wrapper: resolve the single (or default) account. */
|
|
115
|
+
export function resolveEmailConfig(config) {
|
|
116
|
+
const settings = resolveEmailSettings(config);
|
|
117
|
+
return settings.accounts.get(settings.defaultAccount);
|
|
118
|
+
}
|
|
61
119
|
export function clampInt(value, fallback, min, max) {
|
|
62
120
|
const n = typeof value === 'number' ? Math.trunc(value) : fallback;
|
|
63
121
|
if (!Number.isFinite(n))
|
package/lib/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export declare const inject: string[];
|
|
|
4
4
|
export type Config = EmailConfig;
|
|
5
5
|
export declare function apply(ctx: any, config?: Config): void;
|
|
6
6
|
export { PROVIDER_NAMES, EMAIL_PASSWORD_ENV } from './config.js';
|
|
7
|
-
export { resolveEmailConfig, clampInt } from './config.js';
|
|
8
|
-
export { stripHtml, truncateText, flattenAddresses, parseRawMessage } from './parse.js';
|
|
9
|
-
export {
|
|
7
|
+
export { resolveEmailConfig, resolveEmailSettings, clampInt, defaultDownloadDir } from './config.js';
|
|
8
|
+
export { stripHtml, truncateText, flattenAddresses, sanitizeFilename, parseRawMessage } from './parse.js';
|
|
9
|
+
export { EmailPool, MailError, messageOf, validateAttachmentPaths } from './mail-client.js';
|