secondlgn/dsh-mail ↗★ 0

dsh-mail

Mail channel for DSH: one dedicated mailbox per machine, email as session entry, replies continue the session 适合需要通过电子邮件远程驱动和与 DSH 代理交互的用户。

Package
dsh-mail
Compatibility
Unverified
Cordis peer range
~4.0.4
Version
0.1.0
License
MIT
Last updated
Oct 11, 2026

Other repositories with this package name

Install

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:secondlgn/dsh-mail

这里只能按 id 覆盖配置。

  • id: dsh-mail config: enabled: true imap: host: imap.example.com port: 993 user: agent@example.com passwordEnv: DSH_MAIL_PASSWORD smtp: host: smtp.example.com port: 465 user: agent@example.com allowFrom: - you@example.com workspaceDir: C:/work

`bundles` 里必须加上 `dsh-mail`,它自带的 `cordis.patch.yml` 才会被插入;那里面只有中性默认值
(`enabled: false`),真正的账号与白名单写在上面的 profile 补丁里。补丁会**整体替换**该行的
`config`,所以上面这份配置要写全,不能只写增量。

密码**不要写进配置**:用 `passwordEnv`(默认 `DSH_MAIL_PASSWORD`)指向环境变量。

`allowFrom` 为空时**任何人都无法驱动这台机器**——这是安全的默认值,不是 bug。

### 关于 workspaceDir

`workspaceDir` 决定邮箱会话的工作目录,**每台机器不同,不要照抄**。留空则用当前工作目录。
它必须是该机器上真实存在的绝对路径。

### 界面配置:暂时不可用

浏览器半侧(`lib/client.js`)仍在仓库里,它会在侧栏的插件页注册一张「邮件通道」卡片,但**目前不要指望它能用**。

原因是一个被证伪的前提:DSH 的设置服务只能编辑 schema 上用 `.volatile()` 声明的字段,而
**运行时解析到的 schemastery 是 3.18.1,其中 `Schema.prototype.volatile` 根本不存在**:

$ node -e "…require('@deepseek-ai/schemastery')…console.log(typeof z.boolean().volatile)" undefined


(`app.asar` 内附的源码副本是 3.18.4、有 `.volatile()`,这正是我当初误判的来源——我读了随包源码,
却没有验证运行时实际解析到的版本。)

在 schema 上声明 `.volatile()` 会在构造 Config 时抛 `TypeError`,插件行**整个起不来**,表现为
「插件开着但邮件完全不被处理」。因此这些声明已全部回退,配置恢复为普通字段。

没有 volatile,设置服务就不会为该条目投影出任何表单字段,页面拿不到可编辑状态。要让界面配置复活,
需要先解决版本问题(升级运行时的 schemastery,或让 DSH 的设置服务支持非 volatile 字段)。

**在此之前,请直接改 `cordis.patch.yml`**(见上面的配置示例);改完重启 DSH 生效。

`stateDir` 与 `dshHome` 即使将来能编辑也应留在文件里:它们决定状态库的位置,运行时改动会让通道与
自己的历史脱钩。

### 验证安装

重启 DSH 后,插件行应以 `include:dsh-mail` 出现且状态为 `active`。若没出现,按顺序检查:

1. `bundles` 里是否真的加了 `dsh-mail`;
2. profile 补丁里是否误写了 `name:`(见上面的警告);
3. `package.json` 是否为合法 JSON。

### 网易邮箱(163/126/yeah)必读

网易对第三方客户端有额外要求,插件已经内置处理,但值得知道原因:

- **登录后必须先发 `ID` 命令**(RFC 2971)声明客户端身份,否则 `SELECT INBOX` 会直接返回
  `NO [Unsafe Login]`。插件在 `CAPABILITY` 里看到 `ID` 就自动发送。
- **Sent 文件夹名为 modified UTF-7**(163 上是 `&XfJT0ZAB-`,即「已发送」)。插件靠 RFC 6154 的
  `\Sent` 属性识别它,不依赖名字,所以不需要手工配置 `sentMailbox`。
- 首次接入建议先在邮箱设置里开启 **IMAP/SMTP 服务**,并使用**授权码**而不是登录密码。

参考配置(同样是**不带 `name:`** 的 id 覆盖):

```yaml
- id: dsh-mail
  config:
    enabled: true
    imap:
      host: imap.163.com
      port: 993
      user: you@163.com
      passwordEnv: DSH_MAIL_PASSWORD
    smtp:
      host: smtp.163.com
      port: 465
      user: you@163.com
      passwordEnv: DSH_MAIL_PASSWORD
    allowFrom: [you@163.com]

上线前自检

仓库 tools/ 里有两个只读检查脚本,在仓库根目录运行(都需要 DSH_MAIL_PASSWORD 可见):

node tools/mail-probe.mjs     you@163.com   # 登录 + 能力 + Sent 文件夹识别
node tools/mail-rehearsal.mjs you@163.com   # 抓最新几封真实邮件跑一遍判定与解析

两者都不发信、不写状态、不标记已读,可以在启用插件前对着真实邮箱放心跑。

配置项

键默认说明
enabledtrue关掉后插件不建立任何连接
imap.host / imap.user必填IMAP 服务器与账号
imap.port / imap.secure993 / true隐式 TLS
imap.password / imap.passwordEnv'' / DSH_MAIL_PASSWORD密码内联或环境变量名
imap.mailboxINBOX轮询的文件夹
imap.sentMailbox''留空则自动识别 \Sent,再按常见名称兜底
imap.pollIntervalMs20000轮询间隔(有效下限 2000),IDLE 不可用时按它轮询
imap.idletrue首选 IDLE,不可用时自动退回轮询
imap.maxIdleMs240000单次 IDLE 的最长等待上限
imap.rejectUnauthorizedtrue关掉即不校验 IMAP 服务器证书(仅用于自签调试)
smtp.host同 imap.host留空即复用 IMAP 主机
smtp.port / smtp.secure465 / true提交端口
smtp.password / smtp.passwordEnv'' / DSH_MAIL_PASSWORD留空则复用 IMAP 的密码解析结果
smtp.saveToSenttrue发信后回写 Sent 文件夹
smtp.rejectUnauthorizedtrue关掉即不校验 SMTP 服务器证书(仅用于自签调试)
allowFrom[]允许驱动 agent 的发件人地址(精确匹配)
selfAddresses[]额外视为「自己」的地址,用于自环检测
fromNameDSH Agent发信显示名
tagPrefix[dsh-主题标签前缀
workspaceDirprocess.cwd()新建会话的工作目录
agentPreset''邮箱会话使用的 agent 预设,留空用 profile 默认
stateDir/mail插件状态目录
dshHome$DSH_HOME覆盖状态目录推导所用的 DSH 主目录
sendReceipttrue受理后立即回执
sendResulttrue轮次结束后回结果
includeModePreambletrue在提示里注入邮件排版要求
interruptSameThreadtrue同线程回复打断正在跑的轮次
maxMessageBytes32 MiB超过则丢弃
maxResultChars60000回信正文上限,超出截断并标注
attachments.enabledtrue是否落盘附件
attachments.maxBytes / maxCount10 MiB / 10单附件与数量上限
attachments.dir/.dsh-mail/attachments附件根目录
recoveryrerun崩溃恢复:rerun 重跑未完成的信,其他值标记失败

工作方式

会话判定

1. From 在白名单?              否 → 丢弃(不记录、不回信)
2. 自动回复 / 群发 / 邮件列表? 是 → 丢弃
3. Message-ID 已处理完?        是 → 丢弃
4. In-Reply-To/References → 我方 Message-ID → 命中线程标签
   主题里的 [dsh-xxxx] → 命中线程标签
   都没有 → 新建会话(旧线程若断了,就是新会话)

打断语义(关键行为)

只有回复属于「正在运行的那个会话」时才打断。 别的会话的来信只排队,不打扰当前轮次。

打断的实现是「先投递、再取消」:把新内容以 queue 投进会话,然后 cancel 掉当前轮次,让队列里的新提示立刻接管。steer 可用时优先用它。

投递保证

至少一次。进程在「已投递」与「记为完成」之间崩溃时会重放——重复回答比漏信便宜。为控制代价,重放前先读 state.json 里记录的原会话与收件人,不会因为线程映射变化而走错会话;同一线程的同一段文本也不会重复发送。

回执与回信

邮件时机内容
受理回执通过准入后立即已收到(回执 xxxxxx),会话 [dsh-xxxx],开始处理
结果回信轮次结束该轮的助手文本;未完成/被取消的轮次不发

回执的意义是让你在第一封信里就拿到会话号:之后就算客户端丢了 References,只要主题里的 [dsh-xxxx] 还在,就能续接。

turn/end 原因为 interrupted 或 forked 时不发结果,避免把被打断的半成品当成答复。

邮件模式提示

入站内容会包一层元信息送进模型,说明这是邮件而非对话框、只输出纯文本、控制行宽、不要提实现细节、不要复述提示本身,并附上发件人、主题、会话号、回执号。

状态文件

/
  messages.json   Message-ID → { 发件人, 主题, 正文, 会话, 线程标签, 回执号, 阶段, 时间 }
  threads.json    线程标签 ↔ 会话,以及我方发出的 Message-ID
  cursor.json     轮询水位(UID + UIDVALIDITY)

写入是原子的(临时文件 + fsync + rename),并按文件串行化。UIDVALIDITY 变化会重置 UID 水位,但消息身份始终由 Message-ID 决定,所以重置不会重放已完成的信。

开发

npm test                    # node --test,自动发现 test/*.test.mjs
npm run test:single-process # 沙箱下的退路:不用按文件子进程隔离

零依赖,npm install 不是必需的。用 DSH 自带的 Node 跑也一样(自带 TLS 与 node:test):

"C:/Users//.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/node/bin/node.exe" --test

默认的按文件子进程隔离在某些文件沙箱下会以 EPERM 失败,这时改用 npm run test:single-process (把全部用例放进一个进程)。

测试全部离线:真实 TLS 连接(自签证书在进程内生成)对接脚本化的 IMAP/SMTP 服务器,加一个 Host 会话控制器桩。DSH_MAIL_TEST_TRACE=1 会打印插件自身的日志。

文件覆盖
test/mime.test.mjsRFC 2047、GBK/UTF-8、引文剥离、HTML 转文本、附件识别
test/inbound.test.mjs白名单、循环抑制、新会话/续接、重复与重放判定
test/store.test.mjs原子写入、并发修改、水位语义、保留策略
test/imap.test.mjs协议解析、literal(含多次写入与多 literal)、互斥、IDLE、APPEND
test/smtp.test.mjs多行应答、AUTH 三机制、DATA 点填充、MIME 组装
test/channel.test.mjs端到端:准入 → 建会话 → 回执 → 结果、打断、附件、重放、Sent 回写
test/tls-fixture.test.mjs测试用的自签证书夹具本身(本机名可用、他机名被拒)

已知限制

  • IDLE 单次等待有上限:最长等 imap.maxIdleMs(默认 240 s)再重新进入;轮询间隔的有效下限是 2000 ms。
  • IDLE 期间无法发信:APPEND 需要空闲连接,若正卡在 IDLE 会等一个 IDLE 周期。当前实现里发信前会复用空闲连接,最坏情况下 Sent 回写延迟一个周期。
  • 配置 schema 无法离线校验:DSH 用的 schemastery 是定制版,公共包解析不了它的 schema。因此 Config 的导入是容错的(Peer 不可解析时降级为空 schema),默认值另由 withDefaults 兜底,并由 test/channel.test.mjs 覆盖。
  • 引文剥离是启发式:识别 > 引用、On ... wrote:、-----Original Message-----、Outlook/中文头部块与签名分隔符。剥离后正文为空时会退回原文并标注。
  • 附件不自动解压:zip 原样落盘。
  • 阶段汇报未实现:原本约定的「plan 的 todo 即阶段」需要订阅 todo_write 结果,尚未做。
  • 权限询问未实现:超范围权限的邮件询问与超时默认拒绝尚未做;当前依赖 DSH 自身的审批通道。

安全边界

  • 全部连接是出站的,不监听任何端口,没有回调地址。
  • 准入只认 From 精确地址;allowFrom 为空时谁都不能驱动这台机器。
  • 密码只从环境变量读(passwordEnv),配置与仓库里都不落明文。
  • 状态目录(/mail)保存的是已剥离引文的正文、Message-ID、线程映射和水位,用来 排查「这封信为什么没回」;原始 MIME 报文不落盘,附件按配置落到工作区而不是状态目录。
  • 崩溃恢复是「至少一次」,代价是可能重跑一封信;同一线程的同一段文本不会被重复发送。

许可

MIT,见 LICENSE。

界面配置:暂时不可用

浏览器半侧(lib/client.js)仍在仓库里,它会在侧栏的插件页注册一张「邮件通道」卡片,但目前不要指望它能用。

原因是一个被证伪的前提:DSH 的设置服务只能编辑 schema 上用 .volatile() 声明的字段,而 运行时解析到的 schemastery 是 3.18.1,其中 Schema.prototype.volatile 根本不存在:

$ node -e "…require('@deepseek-ai/schemastery')…console.log(typeof z.boolean().volatile)"
undefined

(app.asar 内附的源码副本是 3.18.4、有 .volatile(),这正是我当初误判的来源——我读了随包源码, 却没有验证运行时实际解析到的版本。)

在 schema 上声明 .volatile() 会在构造 Config 时抛 TypeError,插件行整个起不来,表现为 「插件开着但邮件完全不被处理」。因此这些声明已全部回退,配置恢复为普通字段。

没有 volatile,设置服务就不会为该条目投影出任何表单字段,页面拿不到可编辑状态。要让界面配置复活, 需要先解决版本问题(升级运行时的 schemastery,或让 DSH 的设置服务支持非 volatile 字段)。

在此之前,请直接改 cordis.patch.yml(见上面的配置示例);改完重启 DSH 生效。

stateDir 与 dshHome 即使将来能编辑也应留在文件里:它们决定状态库的位置,运行时改动会让通道与 自己的历史脱钩。

配置项

键默认说明
enabledtrue关掉后插件不建立任何连接
imap.host / imap.user必填IMAP 服务器与账号
imap.port / imap.secure993 / true隐式 TLS
imap.password / imap.passwordEnv'' / DSH_MAIL_PASSWORD密码内联或环境变量名
imap.mailboxINBOX轮询的文件夹
imap.sentMailbox''留空则自动识别 \Sent,再按常见名称兜底
imap.pollIntervalMs20000轮询间隔(有效下限 2000),IDLE 不可用时按它轮询
imap.idletrue首选 IDLE,不可用时自动退回轮询
imap.maxIdleMs240000单次 IDLE 的最长等待上限
imap.rejectUnauthorizedtrue关掉即不校验 IMAP 服务器证书(仅用于自签调试)
smtp.host同 imap.host留空即复用 IMAP 主机
smtp.port / smtp.secure465 / true提交端口
smtp.password / smtp.passwordEnv'' / DSH_MAIL_PASSWORD留空则复用 IMAP 的密码解析结果
smtp.saveToSenttrue发信后回写 Sent 文件夹
smtp.rejectUnauthorizedtrue关掉即不校验 SMTP 服务器证书(仅用于自签调试)
allowFrom[]允许驱动 agent 的发件人地址(精确匹配)
selfAddresses[]额外视为「自己」的地址,用于自环检测
fromNameDSH Agent发信显示名
tagPrefix[dsh-主题标签前缀
workspaceDirprocess.cwd()新建会话的工作目录
agentPreset''邮箱会话使用的 agent 预设,留空用 profile 默认
stateDir/mail插件状态目录
dshHome$DSH_HOME覆盖状态目录推导所用的 DSH 主目录
sendReceipttrue受理后立即回执
sendResulttrue轮次结束后回结果
includeModePreambletrue在提示里注入邮件排版要求
interruptSameThreadtrue同线程回复打断正在跑的轮次
maxMessageBytes32 MiB超过则丢弃
maxResultChars60000回信正文上限,超出截断并标注
attachments.enabledtrue是否落盘附件
attachments.maxBytes / maxCount10 MiB / 10单附件与数量上限
attachments.dir/.dsh-mail/attachments附件根目录
recoveryrerun崩溃恢复:rerun 重跑未完成的信,其他值标记失败