dsh-email 0.6.2 → 0.8.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.en.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # dsh-email
2
2
 
3
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
4
+
3
5
  ![dsh-email banner](https://raw.githubusercontent.com/STARDUSTLC666/dsh-email/main/assets/banner.png)
4
6
 
5
7
  Email tools for DeepSeek Harness: let the agent **check the inbox, read mail, search mail, send mail on your behalf, and send/receive attachments** through standard IMAP/SMTP — with one-line presets for QQ / 163 / 126 / Sina / Aliyun / Gmail / Outlook / iCloud. Pure plugin implementation, zero core changes, works out of the box.
@@ -12,7 +14,7 @@ Pure Node, **cross-platform** (one codebase for Windows / macOS / Linux), no she
12
14
  |---|---|
13
15
  | `email_list` | List the newest mail in a folder (unread filter, pagination, summaries only, no body) |
14
16
  | `email_read` | Read one message's full text by uid (HTML auto-converted to plain text, oversized bodies truncated) |
15
- | `email_search` | Search sender/recipient/subject by keyword (server-side); with no results, falls back to a body scan of the most recent 30 messages by default |
17
+ | `email_search` | Search subject/sender/recipient/CC by keyword (server-side subject/from/to/cc); with no results, falls back to a body scan of the most recent 30 messages by default (including to/cc) |
16
18
  | `email_send` | Send mail on your behalf (attachments supported). **Prompts for confirmation before sending by default**, showing recipients, subject and attachment count; only sends after you approve |
17
19
  | `email_folders` | List the mailbox folders (INBOX/Sent/Junk/custom…); feed the `path` to other tools |
18
20
  | `email_attachment` | Download an attachment by index (saved to the session workspace by default so the model can read it directly; size capped by `maxAttachmentBytes`) |
@@ -21,6 +23,18 @@ Example:
21
23
 
22
24
  > Check the 10 newest unread messages in my QQ mailbox and list the ones that need a reply.
23
25
 
26
+ ### v0.6.2 improvements
27
+
28
+ - Server-side search now covers `cc`, so the search truly spans subject / sender / recipients / CC.
29
+ - The client-side body fallback also matches `to` and `cc`; one malformed message no longer aborts the whole batch scan.
30
+ - Mail lists are sorted by UID descending to guarantee newest-first results.
31
+ - `email_send` validates `attachments` as a string array of file paths and trims each path.
32
+
33
+
34
+ ## Compatibility
35
+
36
+ Verified against `@deepseek-ai/dsh@0.1.1-rc.2` on 2026-08-26. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
37
+
24
38
  ## Installation
25
39
 
26
40
  ```sh
@@ -38,6 +52,15 @@ After installing, restart `dsh web`. The plugin ships with an empty config and *
38
52
 
39
53
  Values saved in the settings page live in the `dsh-email` namespace of `settings.yaml` and override the YAML default-account config; password fields are marked secret (they never appear in exports or diagnostics).
40
54
 
55
+ ## Uninstall
56
+
57
+ ```bash
58
+ dsh plugin --profile web remove dsh-email
59
+ ```
60
+
61
+ Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
62
+
63
+
41
64
  ## Configuration
42
65
 
43
66
  In your profile's `cordis.patch.yml` (under `$DSH_HOME/profiles/<name>/`), override the `tool-email` line, then restart:
@@ -105,24 +128,21 @@ Every provider requires an authorization code / app-specific password instead of
105
128
  - When the session is in **Full Access** mode, the harness approval policy is never (no confirmation dialogs) — `email_send` is **blocked with a clear hint**. Two ways out: ① switch the access mode back to Read Only / Write; ② turn off `sendApproval` (uncheck "Confirm before sending" in the settings page), explicitly declaring you accept the risk.
106
129
  - This plugin performs no outbound telemetry; credentials are used in memory only to connect to your mail servers.
107
130
 
108
- ## Known limitations (v0.6)
131
+ ## Known limitations
109
132
 
110
- - **Connection reuse**: IMAP is pooled per account (idle connections auto-recycled); SMTP uses nodemailer's connection pool. Concurrent calls for the same account queue serially (one connection serves one operation at a time — intentional).
111
- - **Multiple accounts**: each account has its own connection pool; one `tool-email` line can hold any number of accounts. The settings page's "Multiple accounts (advanced, YAML)" textbox can directly edit the map (including a `defaultAccount` key), overriding `accounts` in cordis.patch.yml.
112
- - **Attachment download**: `email_attachment` locates attachments from `email_read`'s attachment list (matching by filename first, then type+size to the IMAP part; a failed match errors instead of downloading the wrong file); inline images aren't downloadable yet; filenames are sanitized against path traversal, existing names get a suffix, and size is capped by `maxAttachmentBytes`.
113
- - **No OAuth2**: enterprise environments that force OAuth (some M365/Google Workspace) aren't usable yet.
114
- - **Body search uses client-side fallback**: most servers (e.g. QQ) have unreliable IMAP `TEXT`/`HEADER` search, so the server side only searches subject/sender/recipient; with no results it falls back to a body scan of the most recent `bodySearchLimit` messages (slower; disable with `bodySearchFallback`).
115
- - **Password storage form**: the authorization code saved in the settings page is written in plaintext to the local `settings.yaml` (the schema's secret mark only keeps it out of logs/exports/diagnostics; no disk encryption). Don't hand settings.yaml to untrusted people.
116
- - **Settings page vs. plugin-set changes**: saving in the settings page takes effect **immediately** (live), no restart needed; but upgrading or adding/removing plugins (composition-tree changes) still requires restarting `dsh web`.
133
+ - **No OAuth2**: enterprise environments that force OAuth (some M365 / Google Workspace) aren't usable yet; use an app-specific password / authorization code instead.
134
+ - **Body search**: the server side only searches subject / from / to / cc. Most servers (e.g. QQ) have unreliable IMAP `TEXT` / `HEADER` search, so with no results it falls back to a body scan of the most recent `bodySearchLimit` messages (slower; disable with `bodySearchFallback`).
135
+ - **Attachments**: inline images aren't downloadable separately yet; a failed attachment match errors instead of downloading the wrong file (safe default).
136
+ - **Password storage**: the authorization code saved in the settings page is written in plaintext to the local `settings.yaml` (the secret mark only keeps it out of logs / exports / diagnostics; no disk encryption). Don't hand `settings.yaml` to untrusted people.
117
137
 
118
138
  ## Development
119
139
 
120
140
  ```sh
121
141
  pnpm install
122
142
  pnpm run build # tsc → lib/
123
- pnpm test # 构建 + node --test(配置/解析/注册与审批门,43 个用例,无需真实邮箱)
143
+ pnpm test # 构建 + node --test(配置/解析/注册与审批门,44 个用例,无需真实邮箱)
124
144
  ```
125
145
 
126
146
  ## License
127
147
 
128
- MIT. This is a community plugin, not affiliated with DeepSeek; `@deepseek-ai/*` is an officially reserved namespace.
148
+ MIT. This is a community plugin, not affiliated with DeepSeek; `@deepseek-ai/*` is an officially reserved namespace.
package/README.md CHANGED
@@ -1,166 +1,185 @@
1
- [English](README.en.md)
2
-
3
- # dsh-email
4
-
5
- > **给 agent 一个邮箱**:八个国内邮箱预设开箱即用,收发/搜索/附件下载全搞定。
6
-
7
- ![npm version](https://img.shields.io/npm/v/dsh-email?label=npm&color=blue) ![npm downloads](https://img.shields.io/npm/dm/dsh-email) ![license](https://img.shields.io/npm/l/dsh-email) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-email?style=social)
8
-
9
-
10
- ![dsh-email banner](https://raw.githubusercontent.com/STARDUSTLC666/dsh-email/main/assets/banner.png)
11
-
12
-
13
- DeepSeek Harness 邮件工具插件:让 agent 能**查收件箱、读邮件、搜邮件、代发邮件、收发附件**。纯插件实现,零核心改动,安装即可用。
14
-
15
- 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.
16
-
17
- Node 实现,**全平台通用**(Windows / macOS / Linux 同一份代码),不依赖 shell、无原生二进制。
18
-
19
- ## 工具一览
20
-
21
- | 工具 | 作用 |
22
- |---|---|
23
- | `email_list` | 列出文件夹里最新的邮件(未读过滤、分页、只看摘要不带正文) |
24
- | `email_read` | 按 uid 读取一封邮件的全文(HTML 邮件自动转纯文本,超长截断) |
25
- | `email_search` | 按关键词搜索发件人/收件人/主题(服务器端);无结果时默认回退到最近 30 封的正文扫描 |
26
- | `email_send` | 代发邮件(支持带附件)。**默认发信前会弹确认**,显示收件人、主题和附件数,由你批准后才发出 |
27
- | `email_folders` | 列出邮箱的文件夹(INBOX/已发送/垃圾邮件/自定义…),拿 path 喂给其他工具 |
28
- | `email_attachment` | 按序号下载邮件附件(默认存到会话工作区,模型可直接读取;大小受 maxAttachmentBytes 限制) |
29
-
30
- 示例对话:
31
-
32
- > 帮我看下 QQ 邮箱最新的 10 封未读,把要回复的列出来。
33
-
34
- ## 安装
35
-
36
- ```sh
37
- dsh plugin --profile web add dsh-email
38
- ```
39
-
40
- (或从 GitHub 安装:`dsh plugin --profile web add github:你的账号/dsh-email#<commit>`,随后按提示在 profile `pnpm-workspace.yaml` 里授权 `prepare` 构建。)
41
-
42
- 装好后重启 `dsh web`。插件自带空配置,**不会弄崩启动**;配置前调用任何 email 工具都会返回明确的配置提示。
43
-
44
- **配置方式有两种(任选其一):**
45
-
46
- 1. **网页设置(推荐)**:重启后打开 **设置 邮件 (dsh-email)**,表单里填邮箱地址和授权码,点「保存并应用」,还带「测试连接」按钮。零 YAML、零重启。
47
- 2. **YAML**:按下面的 cordis.patch.yml 模板手写;设置页的「多账号(高级,YAML)」文本框也能填账号映射(覆盖 YAML 里的 accounts)。
48
-
49
- 设置页保存的值存在 `settings.yaml` 的 `dsh-email` 命名空间里,覆盖 YAML 的默认账号配置;密码字段标记为 secret(不会出现在任何导出/诊断里)。
50
-
51
- ## 配置
52
-
53
- 在你 profile 的 `cordis.patch.yml` 里覆盖 `tool-email` 行(在 `$DSH_HOME/profiles/<name>/` 下),然后重启:
54
-
55
- ```yaml
56
- - id: tool-email
57
- config:
58
- provider: qq # qq | 163 | 126 | sina | aliyun | gmail | outlook | icloud
59
- user: you@qq.com
60
- password: 你的授权码 # 强烈建议改用环境变量 DSH_EMAIL_PASSWORD,见下
61
- ```
62
-
63
- 不需要预设?手填任意 IMAP/SMTP 服务器即可:
64
-
65
- ```yaml
66
- - id: tool-email
67
- config:
68
- user: you@corp.example
69
- password: 你的授权码
70
- imap: { host: imap.corp.example, port: 993, secure: true }
71
- smtp: { host: smtp.corp.example, port: 465, secure: true }
72
- inboxFolder: INBOX
73
- ```
74
-
75
- 多账号:一个 `tool-email` 行可以配多个邮箱,工具调用时用 `account` 参数选择:
76
-
77
- ```yaml
78
- - id: tool-email
79
- config:
80
- accounts:
81
- work: { provider: qq, user: work@qq.com, password: 授权码1 }
82
- home: { provider: '163', user: home@163.com, password: 授权码2 }
83
- defaultAccount: work # 省略 account 参数时用这个
84
- downloadDir: E:/attachments # 可选,默认 $DSH_HOME/email-downloads
85
- ```
86
-
87
- 顶层的 `provider`/`user`/`password`/`imap`/`smtp`/`inboxFolder` 仍然可用,作为各账号的共享默认值(v0.1 单账号写法完全兼容)。
88
-
89
- ### 常用邮箱预设
90
-
91
- | provider | IMAP | SMTP |
92
- |---|---|---|
93
- | `qq` | imap.qq.com:993 (SSL) | smtp.qq.com:465 (SSL) |
94
- | `163` | imap.163.com:993 | smtp.163.com:465 |
95
- | `126` | imap.126.com:993 | smtp.126.com:465 |
96
- | `sina` | imap.sina.com:993 | smtp.sina.com:465 |
97
- | `aliyun` | imap.aliyun.com:993 | smtp.aliyun.com:465 |
98
- | `gmail` | imap.gmail.com:993 | smtp.gmail.com:465 |
99
- | `outlook` | outlook.office365.com:993 | smtp.office365.com:587 (STARTTLS) |
100
- | `icloud` | imap.mail.me.com:993 | smtp.mail.me.com:587 (STARTTLS) |
101
-
102
- ### 完整配置项
103
-
104
- | 字段 | 默认 | 说明 |
105
- |---|---|---|
106
- | `provider` | | 预设名,自动填 imap/smtp 地址;显式写的 host/port/secure 优先 |
107
- | `user` | 必填 | 登录邮箱地址 |
108
- | `password` | 必填* | 授权码/应用专用密码;*也可用环境变量 `DSH_EMAIL_PASSWORD` |
109
- | `imap.host/port/secure` | 按预设 | 收信服务器(另有 connectionTimeoutMs/socketTimeoutMs 可调超时) |
110
- | `smtp.host/port/secure` | 按预设 | 发信服务器 |
111
- | `inboxFolder` | `INBOX` | 收发工具默认使用的文件夹 |
112
- | `sendApproval` | `true` | 发信前弹确认(强烈建议保留) |
113
- | `maxBodyChars` | `20000` | email_read 正文截断上限(1000–200000) |
114
- | `accounts` | | 具名账号表;账号级字段覆盖顶层简写 |
115
- | `accountsYaml` | 无 | 设置页「多账号(高级)」文本框的 YAML 文本;非空时覆盖 accounts |
116
- | `defaultAccount` | 单账号时自动 | 工具省略 account 参数时使用的账号(多账号必填) |
117
- | `downloadDir` | 会话工作区下 .dsh-email-downloads(回退 $DSH_HOME/email-downloads) | email_attachment 的落盘目录;显式设置后固定 |
118
- | `maxAttachmentBytes` | 20 MiB | 单个附件与附件总大小上限(1024–512 MiB) |
119
- | `idleTimeoutMs` | `60000` | IMAP 空闲连接回收时间(连接复用,连续操作更快) |
120
- | `bodySearchFallback` | `true` | 服务器搜索无结果时,回退到客户端扫描最近邮件的正文 |
121
- | `bodySearchLimit` | `30` | 正文回退扫描的邮件数量(5-200) |
122
-
123
- ## 第一步:拿到授权码
124
-
125
- 各邮箱都要求用「授权码/应用专用密码」而不是登录密码:
126
-
127
- - **QQ 邮箱**:设置 账户 开启 IMAP/SMTP 服务 → 生成授权码
128
- - **163/126**:设置 → POP3/SMTP/IMAP → 开启 → 新增授权码
129
- - **Gmail**:开启两步验证 安全 应用专用密码
130
- - **Outlook**:Microsoft 账户安全 应用密码(部分账号需先开两步验证)
131
-
132
- ## 安全须知
133
-
134
- - **授权码就是你的邮箱钥匙**。它写在本机(profile 的 `cordis.patch.yml` `settings.yaml`),请勿提交到任何 Git 仓库;更推荐用环境变量 `DSH_EMAIL_PASSWORD`。
135
- - `email_send` 默认走 DSH 审批通道:每次发信都显示「发送邮件给 xx,主题「xx」」,你批准才发出。没有审批通道的环境(如无 UI 的 headless)会**直接拒绝发信**,这是安全默认。
136
- - 会话处于 **Full Access(完全访问)** 模式时,harness 的审批策略是 never(不弹任何确认框)——`email_send` 会**被拦截并给出明确提示**。两条出路:① 把访问模式切回 Read Only / Write;② 关闭 `sendApproval`(设置页勾掉「发信前确认」),即显式声明自行承担风险。
137
- - 本插件不做任何联网上报,凭证只在内存中用于连接你的邮箱服务器。
138
-
139
- ## 已知限制(v0.3)
140
-
141
- - **连接复用**:IMAP 按账号池化(空闲自动回收),SMTP nodemailer 连接池;同一账号的并发调用会排队串行(一个连接一次只服务一个操作,这是有意的)。
142
- - **多账号**:每个账号独立连接池;一个 `tool-email` 行可以配任意多个账号。设置页的「多账号(高级,YAML)」文本框可直接编辑映射(可含 defaultAccount 键),覆盖 cordis.patch.yml accounts。
143
- - **附件下载**:email_attachment email_read 的附件列表定位(先按文件名、再按类型+大小匹配到 IMAP 部件,定位失败会报错而不是下载错文件);内嵌图片暂不支持下载;文件名会被清洗防路径穿越,已有同名文件自动加后缀,大小受 maxAttachmentBytes 限制。
144
- - **不支持 OAuth2**:强制 OAuth 的企业环境(部分 M365/Google Workspace)暂不可用。
145
- - 正文搜索走客户端回退:多数服务器(如 QQ)的 IMAP `TEXT`/`HEADER` 搜索不可靠,所以服务器端只搜主题/发件人/收件人;无结果时回退到最近 `bodySearchLimit` 封的正文扫描(较慢,可关 `bodySearchFallback`)。
146
- - **密码落盘形式**:设置页保存的授权码以明文写在本机 `settings.yaml`(schema 标记 secret 只是保证它不进日志/导出/诊断,不做磁盘加密)。请勿把 settings.yaml 交给不信任的人。
147
- - **设置页与插件集变更**:设置页保存后工具**立即生效**(live),无需重启;但升级/增删插件(组合树变化)仍需重启 `dsh web`。
148
-
149
- ## 开发
150
-
151
- ```sh
152
- pnpm install
153
- pnpm run build # tsc lib/
154
- pnpm test # 构建 + node --test(配置/解析/注册与审批门,43 个用例,无需真实邮箱)
155
- ```
156
-
157
- ## 协议
158
-
159
- MIT。这是一个社区插件,与 DeepSeek 官方无关;`@deepseek-ai/*` 为官方保留命名空间。
160
-
161
- ## 相关插件
162
-
163
- - [dsh-slack](https://github.com/STARDUSTLC666/dsh-slack) — Slack 通知/收件箱
164
- - [dsh-dingtalk](https://github.com/STARDUSTLC666/dsh-dingtalk) — 钉钉群通知(零依赖)
165
- - [dsh-email](https://github.com/STARDUSTLC666/dsh-email) 邮件六件套 + Web 设置页
166
-
1
+ [English](README.en.md)
2
+
3
+ # dsh-email
4
+
5
+ > **给 agent 一个邮箱**:八个国内邮箱预设开箱即用,收发/搜索/附件下载全搞定。
6
+
7
+ ![npm version](https://img.shields.io/npm/v/dsh-email?label=npm&color=blue) ![npm downloads](https://img.shields.io/npm/dm/dsh-email) ![license](https://img.shields.io/npm/l/dsh-email) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-email?style=social)
8
+
9
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
10
+
11
+
12
+ ![dsh-email banner](https://raw.githubusercontent.com/STARDUSTLC666/dsh-email/main/assets/banner.png)
13
+
14
+
15
+ DeepSeek Harness 邮件工具插件:让 agent 能**查收件箱、读邮件、搜邮件、代发邮件、收发附件**。纯插件实现,零核心改动,安装即可用。
16
+
17
+ 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.
18
+
19
+ Node 实现,**全平台通用**(Windows / macOS / Linux 同一份代码),不依赖 shell、无原生二进制。
20
+
21
+ ## 工具一览
22
+
23
+ | 工具 | 作用 |
24
+ |---|---|
25
+ | `email_list` | 列出文件夹里最新的邮件(未读过滤、分页、只看摘要不带正文) |
26
+ | `email_read` | uid 读取一封邮件的全文(HTML 邮件自动转纯文本,超长截断) |
27
+ | `email_search` | 按关键词搜索主题/发件人/收件人/抄送(服务器端 subject/from/to/cc);无结果时默认回退到最近 30 封的正文扫描(含 to/cc) |
28
+ | `email_send` | 代发邮件(支持带附件)。**默认发信前会弹确认**,显示收件人、主题和附件数,由你批准后才发出 |
29
+ | `email_folders` | 列出邮箱的文件夹(INBOX/已发送/垃圾邮件/自定义…),拿 path 喂给其他工具 |
30
+ | `email_attachment` | 按序号下载邮件附件(默认存到会话工作区,模型可直接读取;大小受 maxAttachmentBytes 限制) |
31
+
32
+ 示例对话:
33
+
34
+ > 帮我看下 QQ 邮箱最新的 10 封未读,把要回复的列出来。
35
+
36
+ ### v0.6.2 优化
37
+
38
+ - 服务器端搜索补齐 `cc`,搜索范围真正覆盖主题 / 发件人 / 收件人 / 抄送。
39
+ - 正文回退扫描的匹配范围也加入 `to` / `cc`;单封邮件解析失败不会中断整批扫描。
40
+ - 邮件列表结果强制按 UID 降序,保证「最新在前」。
41
+ - `email_send` 的附件参数严格校验为路径字符串数组,并做 trim。
42
+
43
+
44
+ ## 兼容性
45
+
46
+ 在 `@deepseek-ai/dsh@0.1.1-rc.2` 上验证(2026-08-26)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
47
+
48
+ ## 安装
49
+
50
+ ```sh
51
+ dsh plugin --profile web add dsh-email
52
+ ```
53
+
54
+ (或从 GitHub 安装:`dsh plugin --profile web add github:你的账号/dsh-email#<commit>`,随后按提示在 profile 的 `pnpm-workspace.yaml` 里授权 `prepare` 构建。)
55
+
56
+ 装好后重启 `dsh web`。插件自带空配置,**不会弄崩启动**;配置前调用任何 email 工具都会返回明确的配置提示。
57
+
58
+ **配置方式有两种(任选其一):**
59
+
60
+ 1. **网页设置(推荐)**:重启后打开 **设置 → 邮件 (dsh-email)**,表单里填邮箱地址和授权码,点「保存并应用」,还带「测试连接」按钮。零 YAML、零重启。
61
+ 2. **YAML**:按下面的 cordis.patch.yml 模板手写;设置页的「多账号(高级,YAML)」文本框也能填账号映射(覆盖 YAML 里的 accounts)。
62
+
63
+ 设置页保存的值存在 `settings.yaml` 的 `dsh-email` 命名空间里,覆盖 YAML 的默认账号配置;密码字段标记为 secret(不会出现在任何导出/诊断里)。
64
+
65
+ ## 卸载
66
+
67
+ ```bash
68
+ dsh plugin --profile web remove dsh-email
69
+ ```
70
+
71
+ 卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
72
+
73
+
74
+ ## 配置
75
+
76
+ 在你 profile 的 `cordis.patch.yml` 里覆盖 `tool-email` 行(在 `$DSH_HOME/profiles/<name>/` 下),然后重启:
77
+
78
+ ```yaml
79
+ - id: tool-email
80
+ config:
81
+ provider: qq # qq | 163 | 126 | sina | aliyun | gmail | outlook | icloud
82
+ user: you@qq.com
83
+ password: 你的授权码 # 强烈建议改用环境变量 DSH_EMAIL_PASSWORD,见下
84
+ ```
85
+
86
+ 不需要预设?手填任意 IMAP/SMTP 服务器即可:
87
+
88
+ ```yaml
89
+ - id: tool-email
90
+ config:
91
+ user: you@corp.example
92
+ password: 你的授权码
93
+ imap: { host: imap.corp.example, port: 993, secure: true }
94
+ smtp: { host: smtp.corp.example, port: 465, secure: true }
95
+ inboxFolder: INBOX
96
+ ```
97
+
98
+ 多账号:一个 `tool-email` 行可以配多个邮箱,工具调用时用 `account` 参数选择:
99
+
100
+ ```yaml
101
+ - id: tool-email
102
+ config:
103
+ accounts:
104
+ work: { provider: qq, user: work@qq.com, password: 授权码1 }
105
+ home: { provider: '163', user: home@163.com, password: 授权码2 }
106
+ defaultAccount: work # 省略 account 参数时用这个
107
+ downloadDir: E:/attachments # 可选,默认 $DSH_HOME/email-downloads
108
+ ```
109
+
110
+ 顶层的 `provider`/`user`/`password`/`imap`/`smtp`/`inboxFolder` 仍然可用,作为各账号的共享默认值(v0.1 单账号写法完全兼容)。
111
+
112
+ ### 常用邮箱预设
113
+
114
+ | provider | IMAP | SMTP |
115
+ |---|---|---|
116
+ | `qq` | imap.qq.com:993 (SSL) | smtp.qq.com:465 (SSL) |
117
+ | `163` | imap.163.com:993 | smtp.163.com:465 |
118
+ | `126` | imap.126.com:993 | smtp.126.com:465 |
119
+ | `sina` | imap.sina.com:993 | smtp.sina.com:465 |
120
+ | `aliyun` | imap.aliyun.com:993 | smtp.aliyun.com:465 |
121
+ | `gmail` | imap.gmail.com:993 | smtp.gmail.com:465 |
122
+ | `outlook` | outlook.office365.com:993 | smtp.office365.com:587 (STARTTLS) |
123
+ | `icloud` | imap.mail.me.com:993 | smtp.mail.me.com:587 (STARTTLS) |
124
+
125
+ ### 完整配置项
126
+
127
+ | 字段 | 默认 | 说明 |
128
+ |---|---|---|
129
+ | `provider` | | 预设名,自动填 imap/smtp 地址;显式写的 host/port/secure 优先 |
130
+ | `user` | 必填 | 登录邮箱地址 |
131
+ | `password` | 必填* | 授权码/应用专用密码;*也可用环境变量 `DSH_EMAIL_PASSWORD` |
132
+ | `imap.host/port/secure` | 按预设 | 收信服务器(另有 connectionTimeoutMs/socketTimeoutMs 可调超时) |
133
+ | `smtp.host/port/secure` | 按预设 | 发信服务器 |
134
+ | `inboxFolder` | `INBOX` | 收发工具默认使用的文件夹 |
135
+ | `sendApproval` | `true` | 发信前弹确认(强烈建议保留) |
136
+ | `maxBodyChars` | `20000` | email_read 正文截断上限(1000–200000) |
137
+ | `accounts` | 无 | 具名账号表;账号级字段覆盖顶层简写 |
138
+ | `accountsYaml` | 无 | 设置页「多账号(高级)」文本框的 YAML 文本;非空时覆盖 accounts |
139
+ | `defaultAccount` | 单账号时自动 | 工具省略 account 参数时使用的账号(多账号必填) |
140
+ | `downloadDir` | 会话工作区下 .dsh-email-downloads(回退 $DSH_HOME/email-downloads) | email_attachment 的落盘目录;显式设置后固定 |
141
+ | `maxAttachmentBytes` | 20 MiB | 单个附件与附件总大小上限(1024–512 MiB) |
142
+ | `idleTimeoutMs` | `60000` | IMAP 空闲连接回收时间(连接复用,连续操作更快) |
143
+ | `bodySearchFallback` | `true` | 服务器搜索无结果时,回退到客户端扫描最近邮件的正文 |
144
+ | `bodySearchLimit` | `30` | 正文回退扫描的邮件数量(5-200) |
145
+
146
+ ## 第一步:拿到授权码
147
+
148
+ 各邮箱都要求用「授权码/应用专用密码」而不是登录密码:
149
+
150
+ - **QQ 邮箱**:设置 → 账户 → 开启 IMAP/SMTP 服务 → 生成授权码
151
+ - **163/126**:设置 → POP3/SMTP/IMAP → 开启 → 新增授权码
152
+ - **Gmail**:开启两步验证 → 安全 → 应用专用密码
153
+ - **Outlook**:Microsoft 账户安全应用密码(部分账号需先开两步验证)
154
+
155
+ ## 安全须知
156
+
157
+ - **授权码就是你的邮箱钥匙**。它写在本机(profile 的 `cordis.patch.yml` 或 `settings.yaml`),请勿提交到任何 Git 仓库;更推荐用环境变量 `DSH_EMAIL_PASSWORD`。
158
+ - `email_send` 默认走 DSH 审批通道:每次发信都显示「发送邮件给 xx,主题「xx」」,你批准才发出。没有审批通道的环境(如无 UI 的 headless)会**直接拒绝发信**,这是安全默认。
159
+ - 会话处于 **Full Access(完全访问)** 模式时,harness 的审批策略是 never(不弹任何确认框)——`email_send` 会**被拦截并给出明确提示**。两条出路:① 把访问模式切回 Read Only / Write;② 关闭 `sendApproval`(设置页勾掉「发信前确认」),即显式声明自行承担风险。
160
+ - 本插件不做任何联网上报,凭证只在内存中用于连接你的邮箱服务器。
161
+
162
+ ## 已知限制
163
+
164
+ - **不支持 OAuth2**:强制 OAuth 的企业环境(部分 M365 / Google Workspace)暂不可用,只能使用邮箱服务商的应用专用密码 / 授权码。
165
+ - **正文搜索**:服务器端只搜 subject / from / to / cc;多数服务器(如 QQ)的 IMAP `TEXT` / `HEADER` 搜索不可靠,无结果时回退到最近 `bodySearchLimit` 封的正文扫描(较慢,可用 `bodySearchFallback` 关闭)。
166
+ - **附件**:内嵌图片暂不支持单独下载;附件定位失败会直接报错而不是下载错误文件(安全默认)。
167
+ - **密码落盘**:设置页保存的授权码以明文写在本机 `settings.yaml`(secret 标记只保证它不进日志 / 导出 / 诊断,不做磁盘加密)。请勿把 `settings.yaml` 交给不信任的人。
168
+
169
+ ## 开发
170
+
171
+ ```sh
172
+ pnpm install
173
+ pnpm run build # tsc → lib/
174
+ pnpm test # 构建 + node --test(配置/解析/注册与审批门,44 个用例,无需真实邮箱)
175
+ ```
176
+
177
+ ## 协议
178
+
179
+ MIT。这是一个社区插件,与 DeepSeek 官方无关;`@deepseek-ai/*` 为官方保留命名空间。
180
+
181
+ ## 相关插件
182
+
183
+ - [dsh-slack](https://github.com/STARDUSTLC666/dsh-slack) — Slack 通知/收件箱
184
+ - [dsh-dingtalk](https://github.com/STARDUSTLC666/dsh-dingtalk) — 钉钉群通知(零依赖)
185
+ - [dsh-email](https://github.com/STARDUSTLC666/dsh-email) — 邮件六件套 + Web 设置页
package/lib/index.d.ts CHANGED
@@ -2,6 +2,11 @@ import { type EmailConfig } from './config.js';
2
2
  export declare const name = "tool-email";
3
3
  export declare const inject: string[];
4
4
  export type Config = EmailConfig;
5
+ /**
6
+ * 解析天级日期参数为 Date。接受 YYYY-MM-DD 或完整 ISO 时间。
7
+ * endInclusive=true 时返回“该日结束”(次日零点),用于 until 语义(IMAP BEFORE 是不含当天的)。
8
+ */
9
+ export declare function parseEmailDay(input: string, label: string, endInclusive?: boolean): Date;
5
10
  export declare function apply(ctx: any, config?: Config): void;
6
11
  export { PROVIDER_NAMES, EMAIL_PASSWORD_ENV } from './config.js';
7
12
  export { resolveEmailConfig, resolveEmailSettings, parseAccountsYaml, clampInt, defaultDownloadDir } from './config.js';
package/lib/index.js CHANGED
@@ -5,6 +5,28 @@ import { EmailSettingsBackend, installEmailSettingsWeb } from './web.js';
5
5
  export const name = 'tool-email';
6
6
  export const inject = ['settings', 'tools'];
7
7
  const MAX_LIMIT = 100;
8
+ /**
9
+ * 解析天级日期参数为 Date。接受 YYYY-MM-DD 或完整 ISO 时间。
10
+ * endInclusive=true 时返回“该日结束”(次日零点),用于 until 语义(IMAP BEFORE 是不含当天的)。
11
+ */
12
+ export function parseEmailDay(input, label, endInclusive = false) {
13
+ const text = input.trim();
14
+ let date = null;
15
+ if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
16
+ date = new Date(text + 'T00:00:00Z');
17
+ }
18
+ else {
19
+ const parsed = new Date(text);
20
+ if (!Number.isNaN(parsed.getTime()))
21
+ date = parsed;
22
+ }
23
+ if (date === null || Number.isNaN(date.getTime())) {
24
+ throw new Error(label + ' 不是有效日期,请给形如 2026-08-01 或 2026-08-01T00:00:00Z 的值');
25
+ }
26
+ if (endInclusive)
27
+ return new Date(date.getTime() + 24 * 3600 * 1000);
28
+ return date;
29
+ }
8
30
  const ACCOUNT_HINT = '账号名(配置了 accounts 多个账号时选择),省略时用 defaultAccount。可用账号见 email_folders 的报错或插件 README';
9
31
  /**
10
32
  * Compile the author DSL map into a raw JSON Schema object, exactly what
@@ -227,12 +249,14 @@ export function apply(ctx, config = {}) {
227
249
  installEmailSettingsWeb(ctx, new EmailSettingsBackend(ctx, settingsScope, config));
228
250
  ctx.tools.register({
229
251
  name: 'email_list',
230
- description: 'List recent emails in a mailbox folder (newest first). Returns uid, date, sender, subject and flags without message bodies; use email_read with a uid to fetch the full text.',
252
+ description: 'List recent emails in a mailbox folder (newest first). Returns uid, date, sender, subject and flags without message bodies; use email_read with a uid to fetch the full text. Optional since/until (dates like 2026-08-01) filter by received date.',
231
253
  parameters: compileParameters({
232
254
  folder: { type: 'string', description: 'IMAP folder path (see email_folders); defaults to the account inboxFolder' },
233
255
  limit: { type: 'integer', description: 'How many messages to return, 1-100, default 20' },
234
256
  offset: { type: 'integer', description: 'Skip this many newest messages first, default 0' },
235
257
  unreadOnly: { type: 'boolean', description: 'Only list unread messages, default false' },
258
+ since: { type: 'string', description: '只列出该日期(含)之后收到的邮件,如 2026-08-01(可选)' },
259
+ until: { type: 'string', description: '只列出该日期(含)之前收到的邮件,如 2026-08-26(可选)' },
236
260
  account: { type: 'string', description: ACCOUNT_HINT },
237
261
  }),
238
262
  output: {
@@ -243,7 +267,9 @@ export function apply(ctx, config = {}) {
243
267
  const args = rawArgs;
244
268
  const limit = clampInt(args.limit, 20, 1, MAX_LIMIT);
245
269
  const offset = clampInt(args.offset, 0, 0, 10000);
246
- return await getPool().list(args.account, args.folder?.trim() || '', limit, offset, args.unreadOnly === true);
270
+ const since = args.since?.trim() ? parseEmailDay(args.since, 'since') : undefined;
271
+ const until = args.until?.trim() ? parseEmailDay(args.until, 'until', true) : undefined;
272
+ return await getPool().list(args.account, args.folder?.trim() || '', limit, offset, args.unreadOnly === true, since, until);
247
273
  },
248
274
  });
249
275
  ctx.tools.register({
@@ -273,6 +299,8 @@ export function apply(ctx, config = {}) {
273
299
  query: { type: 'string', required: true, description: 'Keyword to search for' },
274
300
  folder: { type: 'string', description: 'IMAP folder to search in; defaults to the account inboxFolder' },
275
301
  limit: { type: 'integer', description: 'How many matches to return, 1-100, default 10' },
302
+ since: { type: 'string', description: '只搜索该日期(含)之后的邮件,如 2026-08-01(可选)' },
303
+ until: { type: 'string', description: '只搜索该日期(含)之前的邮件,如 2026-08-26(可选)' },
276
304
  account: { type: 'string', description: ACCOUNT_HINT },
277
305
  }),
278
306
  output: {
@@ -284,7 +312,9 @@ export function apply(ctx, config = {}) {
284
312
  if (typeof args.query !== 'string' || args.query.trim() === '')
285
313
  throw new Error('query 不能为空');
286
314
  const limit = clampInt(args.limit, 10, 1, MAX_LIMIT);
287
- return await getPool().search(args.account, args.query.trim(), args.folder?.trim() || '', limit);
315
+ const since = args.since?.trim() ? parseEmailDay(args.since, 'since') : undefined;
316
+ const until = args.until?.trim() ? parseEmailDay(args.until, 'until', true) : undefined;
317
+ return await getPool().search(args.account, args.query.trim(), args.folder?.trim() || '', limit, since, until);
288
318
  },
289
319
  });
290
320
  ctx.tools.register({
@@ -327,6 +357,43 @@ export function apply(ctx, config = {}) {
327
357
  return await getPool().folders(args.account, args.subscribedOnly === true);
328
358
  },
329
359
  });
360
+ ctx.tools.register({
361
+ name: 'email_health',
362
+ description: 'Self-check for dsh-email: summarizes configured accounts (provider / IMAP / SMTP hosts) without any network connection and never shows passwords. Run this first when troubleshooting.',
363
+ parameters: compileParameters({}),
364
+ output: {
365
+ schema: { type: 'object', additionalProperties: true },
366
+ render: (_args, value) => {
367
+ const rec = (value ?? {});
368
+ const rawChecks = Array.isArray(rec.checks) ? rec.checks : [];
369
+ const lines = ['dsh-email 自检' + (rec.ok === true ? ':正常。' : ':发现问题。')];
370
+ for (const item of rawChecks) {
371
+ const c = (item ?? {});
372
+ lines.push('- ' + String(c.name) + ':' + (c.ok === true ? '✅ ' + String(c.detail ?? '') : '❌ ' + String(c.detail ?? '')));
373
+ }
374
+ return [{ type: 'text', text: lines.join('\n') }];
375
+ },
376
+ },
377
+ async execute() {
378
+ const accounts = (config.accounts ?? {});
379
+ const names = Object.keys(accounts);
380
+ const checks = [];
381
+ if (names.length === 0) {
382
+ const topLevelUser = typeof config.user === 'string' && config.user.trim() !== '';
383
+ checks.push({ name: '默认账号', ok: topLevelUser, detail: topLevelUser ? '顶层账号已配置' : '未配置账号:请在 cordis.patch.yml 配置 accounts(或顶层 user/password)' });
384
+ }
385
+ else {
386
+ for (const accountName of names.slice(0, 8)) {
387
+ const account = accounts[accountName] ?? {};
388
+ const hasUser = typeof account.user === 'string' && account.user.trim() !== '';
389
+ const provider = typeof account.provider === 'string' && account.provider !== '' ? account.provider : 'custom';
390
+ checks.push({ name: '账号 ' + accountName, ok: hasUser, detail: hasUser ? provider + ' / ' + account.user : '未配置 user' });
391
+ }
392
+ }
393
+ const ok = checks.every((c) => c.ok === true);
394
+ return { ok, plugin: 'dsh-email', accountCount: names.length, checks };
395
+ },
396
+ });
330
397
  ctx.tools.register({
331
398
  name: 'email_attachment',
332
399
  description: 'Download one attachment of a message to a local file (size capped by maxAttachmentBytes). The index matches the attachments array of email_read. Returns the absolute path of the written file.',
@@ -44,8 +44,8 @@ export declare class EmailPool {
44
44
  startIdleSweep(): void;
45
45
  dispose(): void;
46
46
  private transporter;
47
- list(accountName: string | undefined, folder: string, limit: number, offset: number, unreadOnly: boolean): Promise<EmailListResult>;
48
- search(accountName: string | undefined, query: string, folder: string, limit: number): Promise<EmailSearchResult>;
47
+ list(accountName: string | undefined, folder: string, limit: number, offset: number, unreadOnly: boolean, since?: Date, until?: Date): Promise<EmailListResult>;
48
+ search(accountName: string | undefined, query: string, folder: string, limit: number, since?: Date, until?: Date): Promise<EmailSearchResult>;
49
49
  /** Client-side scan of the tail of the mailbox, newest first. */
50
50
  private searchBodies;
51
51
  private fetchListed;
@@ -226,7 +226,7 @@ export class EmailPool {
226
226
  }
227
227
  return t;
228
228
  }
229
- async list(accountName, folder, limit, offset, unreadOnly) {
229
+ async list(accountName, folder, limit, offset, unreadOnly, since, until) {
230
230
  const name = this.resolveName(accountName);
231
231
  const cfg = this.account(name);
232
232
  const folderName = folder || cfg.inboxFolder;
@@ -235,14 +235,22 @@ export class EmailPool {
235
235
  const total = mailbox === false ? 0 : mailbox.exists;
236
236
  let scopeCount = total;
237
237
  let uids = [];
238
- if (unreadOnly) {
239
- const found = await client.search({ seen: false }, { uid: true });
238
+ const hasDateFilter = since !== undefined || until !== undefined;
239
+ if (unreadOnly || hasDateFilter) {
240
+ const query = {};
241
+ if (unreadOnly)
242
+ query.seen = false;
243
+ if (since !== undefined)
244
+ query.since = since;
245
+ if (until !== undefined)
246
+ query.before = until;
247
+ const found = await client.search(query, { uid: true });
240
248
  uids = found === false ? [] : found;
241
249
  scopeCount = uids.length;
242
250
  }
243
251
  else if (total > 0) {
244
252
  const start = Math.max(1, total - (limit + offset) + 1);
245
- const fetched = await client.fetchAll(start + ':*', { uid: true }, { uid: true });
253
+ const fetched = await client.fetchAll(start + ':*', { uid: true });
246
254
  uids = fetched.map(message => message.uid);
247
255
  }
248
256
  uids.reverse();
@@ -251,7 +259,7 @@ export class EmailPool {
251
259
  return { account: name, count: scopeCount, folder: folderName, messages };
252
260
  });
253
261
  }
254
- async search(accountName, query, folder, limit) {
262
+ async search(accountName, query, folder, limit, since, until) {
255
263
  const name = this.resolveName(accountName);
256
264
  const cfg = this.account(name);
257
265
  const folderName = folder || cfg.inboxFolder;
@@ -259,11 +267,16 @@ export class EmailPool {
259
267
  // No nested OR and no TEXT search: several servers (QQ among them)
260
268
  // silently answer those with empty or match-everything results.
261
269
  // subject/from/to/cc searches unioned client-side behave well everywhere.
270
+ const dateRange = {};
271
+ if (since !== undefined)
272
+ dateRange.since = since;
273
+ if (until !== undefined)
274
+ dateRange.before = until;
262
275
  const found = await Promise.all([
263
- client.search({ subject: query }, { uid: true }),
264
- client.search({ from: query }, { uid: true }),
265
- client.search({ to: query }, { uid: true }),
266
- client.search({ cc: query }, { uid: true }),
276
+ client.search({ subject: query, ...dateRange }, { uid: true }),
277
+ client.search({ from: query, ...dateRange }, { uid: true }),
278
+ client.search({ to: query, ...dateRange }, { uid: true }),
279
+ client.search({ cc: query, ...dateRange }, { uid: true }),
267
280
  ]);
268
281
  const uids = [...new Set(found.flatMap(result => result === false ? [] : result))].sort((a, b) => a - b);
269
282
  uids.reverse();
@@ -284,7 +297,7 @@ export class EmailPool {
284
297
  if (total === 0)
285
298
  return [];
286
299
  const start = Math.max(1, total - this.settings.bodySearchLimit + 1);
287
- const fetched = await client.fetchAll(start + ':*', { uid: true, envelope: true, flags: true, size: true, bodyStructure: true, source: true }, { uid: true });
300
+ const fetched = await client.fetchAll(start + ':*', { uid: true, envelope: true, flags: true, size: true, bodyStructure: true, source: true });
288
301
  const out = [];
289
302
  for (const message of [...fetched].reverse()) {
290
303
  if (out.length >= limit)
package/lib/parse.js CHANGED
@@ -47,8 +47,8 @@ export function truncateText(text, maxChars) {
47
47
  */
48
48
  export function sanitizeFilename(raw, fallback = 'attachment.bin') {
49
49
  let name = String(raw ?? '').replace(/\\/g, '/').split('/').pop() ?? '';
50
- name = name.replace(/[\u0000-\u001f\u007f]/g, '').trim();
51
- if (name === '' || name === '.' || name === '..')
50
+ name = name.replace(/[\u0000-\u001f\u007f]/g, '').replace(/[<>:"|?*]/g, '_').trim().replace(/[. ]+$/g, '');
51
+ if (name === '' || name === '.' || name === '..' || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name))
52
52
  name = fallback;
53
53
  if (name.length > 120) {
54
54
  const dot = name.lastIndexOf('.');
package/lib/types.d.ts CHANGED
@@ -88,6 +88,8 @@ export interface EmailListArgs extends AccountArg {
88
88
  limit?: number;
89
89
  offset?: number;
90
90
  unreadOnly?: boolean;
91
+ since?: string;
92
+ until?: string;
91
93
  }
92
94
  export interface EmailReadArgs extends AccountArg {
93
95
  uid: number;
@@ -97,6 +99,8 @@ export interface EmailSearchArgs extends AccountArg {
97
99
  query: string;
98
100
  folder?: string;
99
101
  limit?: number;
102
+ since?: string;
103
+ until?: string;
100
104
  }
101
105
  export interface EmailSendArgs extends AccountArg {
102
106
  to: string;
package/package.json CHANGED
@@ -1,71 +1,73 @@
1
- {
2
- "name": "dsh-email",
3
- "version": "0.6.2",
4
- "description": "IMAP/SMTP email tools for DeepSeek Harness: list, read, search and send mail, with QQ/163/126/Sina/Aliyun/Gmail/Outlook/iCloud presets.",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/index.d.ts",
11
- "default": "./lib/index.js"
12
- },
13
- "./client": "./lib/client.js",
14
- "./cordis.patch.yml": "./cordis.patch.yml",
15
- "./package.json": "./package.json"
16
- },
17
- "files": [
18
- "lib",
19
- "cordis.patch.yml",
20
- "README.md"
21
- ],
22
- "keywords": [
23
- "dsh-plugin",
24
- "deepseek-harness",
25
- "dsh",
26
- "email",
27
- "imap",
28
- "smtp"
29
- ],
30
- "author": "stardustlc",
31
- "repository": {
32
- "type": "git",
33
- "url": "https://github.com/STARDUSTLC666/dsh-email"
34
- },
35
- "bugs": {
36
- "url": "https://github.com/STARDUSTLC666/dsh-email/issues"
37
- },
38
- "homepage": "https://github.com/STARDUSTLC666/dsh-email#readme",
39
- "license": "MIT",
40
- "engines": {
41
- "node": ">=20"
42
- },
43
- "dsh": {
44
- "bundle": {
45
- "patch": "./cordis.patch.yml"
46
- },
47
- "client": {
48
- "inject": [
49
- "@deepseek-ai/dsh-client-runtime",
50
- "@deepseek-ai/dsh-client-ui-settings"
51
- ],
52
- "platform": "web"
53
- }
54
- },
55
- "dependencies": {
56
- "imapflow": "^1.7.0",
57
- "mailparser": "^3.9.15",
58
- "nodemailer": "^9.0.5",
59
- "schemastery": "^3.18.0",
60
- "yaml": "^2.9.0"
61
- },
62
- "devDependencies": {
63
- "@types/node": "^26.2.0",
64
- "@types/nodemailer": "^8.0.1",
65
- "typescript": "^5.7.3"
66
- },
67
- "scripts": {
68
- "build": "tsc",
69
- "test": "pnpm run build && node --test \"test/*.test.mjs\""
70
- }
71
- }
1
+ {
2
+ "name": "dsh-email",
3
+ "version": "0.8.0",
4
+ "description": "IMAP/SMTP email tools for DeepSeek Harness: list, read, search and send mail, with QQ/163/126/Sina/Aliyun/Gmail/Outlook/iCloud presets.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./client": "./lib/client.js",
14
+ "./cordis.patch.yml": "./cordis.patch.yml",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "lib",
19
+ "cordis.patch.yml",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "prepublishOnly": "pnpm run build",
25
+ "test": "pnpm run build && node --test \"test/*.test.mjs\""
26
+ },
27
+ "keywords": [
28
+ "dsh-plugin",
29
+ "deepseek-harness",
30
+ "dsh",
31
+ "email",
32
+ "imap",
33
+ "smtp"
34
+ ],
35
+ "author": "stardustlc",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/STARDUSTLC666/dsh-email.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/STARDUSTLC666/dsh-email/issues"
42
+ },
43
+ "homepage": "https://github.com/STARDUSTLC666/dsh-email#readme",
44
+ "license": "MIT",
45
+ "packageManager": "pnpm@11.7.0",
46
+ "engines": {
47
+ "node": ">=22"
48
+ },
49
+ "dsh": {
50
+ "bundle": {
51
+ "patch": "./cordis.patch.yml"
52
+ },
53
+ "client": {
54
+ "inject": [
55
+ "@deepseek-ai/dsh-client-runtime",
56
+ "@deepseek-ai/dsh-client-ui-settings"
57
+ ],
58
+ "platform": "web"
59
+ }
60
+ },
61
+ "dependencies": {
62
+ "imapflow": "^1.7.0",
63
+ "mailparser": "^3.9.15",
64
+ "nodemailer": "^9.0.5",
65
+ "schemastery": "^3.18.0",
66
+ "yaml": "^2.9.0"
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "^26.2.0",
70
+ "@types/nodemailer": "^8.0.1",
71
+ "typescript": "^5.7.3"
72
+ }
73
+ }