dsh-email 0.11.0 → 0.13.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/CHANGELOG.md +36 -0
- package/README.en.md +8 -12
- package/README.md +8 -15
- package/lib/client.js +109 -22
- package/lib/config.d.ts +20 -0
- package/lib/config.js +16 -4
- package/lib/mail-client.d.ts +41 -5
- package/lib/mail-client.js +284 -52
- package/lib/parse.js +4 -2
- package/lib/runtime.d.ts +1 -1
- package/lib/runtime.js +33 -15
- package/lib/tool-contract.d.ts +3 -0
- package/lib/tool-contract.js +20 -2
- package/lib/tools.d.ts +1 -0
- package/lib/tools.js +15 -1
- package/lib/types.d.ts +12 -0
- package/lib/web.d.ts +23 -0
- package/lib/web.js +62 -6
- package/package.json +4 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
> 完整历史(含详细改动说明)。README 只保留最近几个版本的一句话摘要。
|
|
4
|
+
|
|
5
|
+
## 中文版
|
|
6
|
+
|
|
7
|
+
- **0.13.0(2026-09-18)**:**修复**:①长正文截断会把整段正文丢成空(无断点时硬切,保留正文);②`email_watch` / 网页弹窗的游标会永久漏报一次超过 `limit` 条的新邮件(改为最旧优先分批、游标只推进到实际返回那批);③附件索引缓存未绑定 UIDVALIDITY,服务器重编号后可能按旧索引写出错误附件(缓存键并入 uidValidity)。**性能**:10 个工具声明 `timeoutMs`(search/watch 120s,其余 60s);watch/弹窗轮询只 FETCH 本轮要报的条数(此前每轮最多 FETCH 100 封未读信封);读信与正文搜索只下载 text/* 分段,不再把整封(含附件)拉下来。**口径**:搜索回退路径明确标注为扫描口径(「本页 N 条(仅扫描最近 X 封)」,不再冒充全文件夹匹配数)。**行为变化**:`email_read` 的 `attachments` 只列可下载的 `disposition=attachment`(内嵌图片不再列出);回退扫描改为逐封下载文本分段(省流量、往返略增)。测试 237 → 262 项。
|
|
8
|
+
- **0.12.0(2026-09-18)**:**新增发送别名**(`senderName` / `authUser` / `authPassword`):`user` 只作为发件地址与信箱身份,登录名与登录密码可以另填——Gmail / Workspace 的别名发信、以及「登录账号 ≠ From 地址」的 SMTP 中继不再被 `535 Username and Password not accepted` 拒绝,From 也能带显示名。设置页账号卡片新增「发件显示名 / 登录账号 / 登录账号的密码」三栏(与授权码同一套三态:留空保留已存值、清空即删除)。**新增** `email_search` 的 `offset`,命中多于一页时可翻页。**修复与优化**:文件夹 UIDVALIDITY 变化时重建 `email_watch` / 弹窗的增量基线(不再把重编号后的整箱当成新邮件);搜索的命中复核与结果列表合并为一次 FETCH;`email_attachment` 复用 `email_read` 已解析的 MIME 索引,不再把整封邮件(含附件)重下一遍;`email_folders` 结果缓存 60 秒;网页端新邮件弹窗在标签页不可见时暂停轮询、失败指数退避、皮肤快照降频。**工程**:CI 增加「`lib/` 与 `src/` 不允许漂移」和客户端 bundle 语法检查。测试 237 → 252 项。发送别名的方向来自 [@TianLanDaoRen](https://github.com/TianLanDaoRen) 的 [PR #8](https://github.com/STARDUSTLC666/dsh-email/pull/8)(本实现按 0.11.0 之后的代码重写)。
|
|
9
|
+
- **0.11.0(2026-09-18)**:合入 gurio-wine 的设置页四连([PR #11](https://github.com/STARDUSTLC666/dsh-email/pull/11)–[#14](https://github.com/STARDUSTLC666/dsh-email/pull/14)),并在评审后修掉其中若干问题。**新增**:①多账号卡片编辑器(增删改 / 改名 / 设默认 / 按账号单独测试连接,编辑即保存,不再需要点「保存并应用」)与服务器预设管理(`serverPresets`,自定义服务商端点,不含凭证);②Outlook / Exchange Online 的 OAuth2 设备码登录(IMAP 与 SMTP 双端,access token 自动刷新,密码认证账号完全不受影响);③设置面板文案中英双语,跟随宿主 Settings → General 的语言实时切换;④账号可显式钉住 `authKind`(自动 / oauth2 / password),给仍能用应用密码连 Exchange Online 的混合或本地租户留退路。**评审修复**:SMTP 的 OAuth2 认证形状原本一封也发不出去(nodemailer 的 `XOAuth2` 只读 `accessToken`、从不读 `pass`,实测报 `EAUTH`);保存面板不再无条件抹掉账号手写的 imap/smtp 端点(运行时解析以账号自己的值优先,原行为会把自建服务器账号静默改指预设,无 provider 的账号则直接失去连接信息);改名保留授权码与高级键,且不允许顶掉同名账号;设置路由增加 Host / Origin / Content-Type 同源校验(此前任意网页都能跨源改设置,DNS rebinding 还能读走含明文授权码的快照);响应不再回显解析后的账号映射(那是一份含明文密码、前端从不读取的副本);服务器原始报错经凭据脱敏后才展示(IMAP/SMTP 会回显被拒的认证串,其中含 access token);删除账号即清理其 token,未提交的保存不清;版本冲突自动重基,而不是拿旧 revision 反复重试。**不内置任何第三方 OAuth2 应用注册**:OAuth2 账号需自带 `clientId`,见下文「Outlook OAuth2」。测试 81 → 237 项。**修复 `email_search`**:QQ 这类服务器会对任意关键词返回同一批无关 UID,现在服务器命中会先用 envelope 复核(subject/from/to/cc),核实不到就回退本地正文扫描,不会再出现「不存在的关键词也匹配 40 条」([#15](https://github.com/STARDUSTLC666/dsh-email/issues/15))。
|
|
10
|
+
- **0.10.8(2026-09-16)**:合入 GUODnuli 的 [PR #9](https://github.com/STARDUSTLC666/dsh-email/pull/9),将设置页及新邮件弹窗的文字、边框引用改为官方主题变量,修复深色主题文字不可读;复验官方 Harness 0.1.5-rc.2 和 0.1.6-alpha.1。
|
|
11
|
+
- **0.10.7(2026-09-11)**:复验官方 Harness 0.1.5-rc.1,更新整套同载与真实服务验证记录;运行时代码未变。
|
|
12
|
+
- **0.10.6(2026-09-10)**:修复单账号设置页授权码留空时,空字符串遮蔽 `DSH_EMAIL_PASSWORD`,导致“测试连接”和保存后工具调用报未配置的问题;显式密码仍优先,多账号不会借用该环境变量。更新设置页工具数量、多账号说明,并补充真实 QQ 邮箱验证结果。
|
|
13
|
+
- **0.10.5(2026-09-08)**:补充官方 Harness 0.1.3-alpha.2 的安装、工具注册及 Web 设置接口验证,更新 Node 版本要求,明确 `email_health` 只检查配置;运行时代码与 0.10.4 相同。
|
|
14
|
+
- **0.10.4(2026-09-07)**:将 `mailparser` 最低版本提升到 `3.9.22` 并更新锁文件,使用 `html-to-text 10.0.1 → deepmerge-ts 8.0.2` 的修复链处理 [CVE-2026-40345](https://github.com/RebeccaStevens/deepmerge-ts/security/advisories/GHSA-ggr8-5vv4-36mx)。不依赖插件作为下游依赖安装时不生效的根级 `pnpm.overrides`;新增真实依赖链与 HTML 邮件解析回归测试。依赖告警不等于已证实邮件输入可触发该漏洞。
|
|
15
|
+
- **0.10.1**:补发制品——已发布的 0.10.0 打包时只含 `email_mark`,本版同时包含 `email_mark` 与 `email_reply`,代码与 0.10.0 的 main 一致。
|
|
16
|
+
- **0.10.0**:新增 `email_mark`(已读/未读/星标/移动文件夹,补齐收发闭环的整理侧)与 `email_reply`(回复/回复全部/转发,自动线程头+引文,走发信审批门);连接池按读/写模式分别管理邮箱打开状态。
|
|
17
|
+
- **0.9.1**:修复设置页空主机遮蔽 provider 预设(#3/#6);IMAP 连接超时不再杀死整个 DSH 进程(#4);暗色模式输入控件可见(#2);密码栏提示环境变量 `DSH_EMAIL_PASSWORD` 免明文方案(#5)。
|
|
18
|
+
- **0.9.0**:新增 `email_watch` 增量新邮件检查工具(游标式,适合定时提醒);Web 端新增「鲸鱼娘递信」新邮件弹窗(本地皮肤素材运行时读取 + 内置回退图)。
|
|
19
|
+
- **0.8.2**:`since` / `until` 参数描述与其余参数统一为英文,方便多语言 agent 理解。
|
|
20
|
+
- **0.8.0/0.8.1**:`email_list` / `email_search` 新增 `since` / `until` 日期范围过滤;新增 `email_health` 账号配置自检;适配 harness 0.1.2(清理已删除的客户端注入声明)。
|
|
21
|
+
- **0.6.2**:服务器端搜索补齐 `cc`,搜索范围真正覆盖主题 / 发件人 / 收件人 / 抄送;正文回退扫描也匹配 `to` / `cc`,单封解析失败不中断整批;列表强制 UID 降序「最新在前」;`email_send` 附件参数严格校验。
|
|
22
|
+
|
|
23
|
+
## English
|
|
24
|
+
|
|
25
|
+
- **0.13.0 (2026-09-18)**: **Fixes**: (1) long bodies could be truncated to nothing (now hard-cut with the body preserved); (2) the `email_watch` / popup cursor permanently skipped new mail beyond one `limit` batch (now oldest-first batching, cursor only advances over what was actually returned); (3) the attachment index cache ignored UIDVALIDITY, so a renumbered mailbox could download the wrong attachment (cache key now includes uidValidity). **Performance**: all ten tools declare `timeoutMs` (120s for search/watch, 60s otherwise); watch/popup polls FETCH only the rows they report (previously up to 100 unread envelopes per poll); reads and body search download text/* parts only instead of the whole message with its attachments. **Semantics**: the body-scan fallback now labels itself as such ("this page only, scanning the newest N messages") instead of claiming a folder-wide match count. **Behaviour change**: `email_read` now lists only downloadable `disposition=attachment` entries (inline images are no longer listed); the fallback scan fetches per message (fewer bytes, slightly more round trips). Tests 237 → 262.
|
|
26
|
+
- **0.12.0 (2026-09-18)**: **send-as alias** (`senderName` / `authUser` / `authPassword`): `user` is now only the From address and the mailbox identity, while the login user and password can differ — Gmail / Workspace aliases and SMTP relays where the login is not the From address no longer fail with `535 Username and Password not accepted`, and the From header can carry a display name. The account card gained three fields (same three-state contract as the authorization code: empty keeps the stored value, clearing deletes the key). **New**: `offset` for `email_search`, so results beyond the first page are reachable. **Fixes and optimisations**: a changed folder UIDVALIDITY re-seeds the `email_watch` / popup baseline instead of reporting the renumbered mailbox as new; search verification and the result rows share one FETCH; `email_attachment` reuses the MIME index `email_read` already parsed instead of downloading the whole message again; `email_folders` is cached for 60s; the web new-mail popup pauses while the tab is hidden, backs off on failures and refreshes its skin snapshot less often. **Engineering**: CI now rejects `lib/` drift against `src/` and syntax-checks the hand-written client bundle. Tests 237 → 252. The send-as direction came from [@TianLanDaoRen](https://github.com/TianLanDaoRen)'s [PR #8](https://github.com/STARDUSTLC666/dsh-email/pull/8) (this implementation is a rewrite on top of 0.11.0).
|
|
27
|
+
- **0.11.0 (2026-09-18)**: merge gurio-wine's four settings-page PRs ([#11](https://github.com/STARDUSTLC666/dsh-email/pull/11)–[#14](https://github.com/STARDUSTLC666/dsh-email/pull/14)) with post-review fixes. **Added**: ① visual multi-account card editor (add/edit/delete, rename, set-default, per-account connection test — edits auto-save; no "Save & Apply" button) and server-preset management (`serverPresets`, custom provider endpoints, no credentials); ② OAuth2 device-code login for Outlook / Exchange Online (IMAP and SMTP share one token; automatic refresh; password-auth accounts unaffected); ③ bilingual settings-panel copy that follows the host's Settings → General language in real time; ④ accounts can pin `authKind` (auto / oauth2 / password), giving hybrid or on-premises tenants that still accept app passwords an escape hatch. **Review fixes**: SMTP OAuth2 could never send (nodemailer's `XOAuth2` reads only `accessToken`, never `pass` — confirmed `EAUTH`); saving no longer unconditionally wipes account-level hand-written imap/smtp endpoints (runtime prefers the account's own host; the old behavior silently re-pointed custom-server accounts to presets, and accounts without a provider lost connection info entirely); rename preserves stored auth codes and advanced keys and refuses to overwrite an existing account name; settings routes now enforce Host / Origin / Content-Type same-origin checks (previously any web page could cross-origin-write settings; DNS rebinding could read snapshots containing plaintext auth codes); responses no longer echo the resolved account map (a plaintext-password copy the front end never reads); raw server errors are credential-scrubbed before display (IMAP/SMTP echo rejected auth strings containing access tokens); deleting an account cleans its tokens (uncommitted saves do not); version conflicts auto-rebase instead of retrying with a stale revision. **No third-party OAuth2 app registration is bundled**: OAuth2 accounts must supply their own `clientId` — see "Outlook OAuth2" below. Tests: 81 → 237. **`email_search` fix**: servers like QQ answer any keyword with the same unrelated uid list; hits are now re-verified against the envelopes (subject/from/to/cc) and fall back to the local body scan when none survive, so an impossible keyword no longer "matches" 40 messages ([#15](https://github.com/STARDUSTLC666/dsh-email/issues/15)).
|
|
28
|
+
- **0.10.8 (2026-09-16)**: integrate GUODnuli's [PR #9](https://github.com/STARDUSTLC666/dsh-email/pull/9), replacing nonexistent text and border variables in settings and notifications with official theme tokens; revalidate Harness 0.1.5-rc.2 and 0.1.6-alpha.1.
|
|
29
|
+
- **0.10.7 (2026-09-11)**: revalidate official Harness 0.1.5-rc.1 and refresh suite co-load and live-service evidence; runtime code is unchanged.
|
|
30
|
+
- **0.10.6 (2026-09-10)**: fix an empty authorization-code field shadowing `DSH_EMAIL_PASSWORD` in single-account connection tests and saved settings. Explicit passwords still take precedence; named accounts cannot borrow this environment variable. Refresh the settings tool count, multi-account guidance and real QQ mailbox validation notes.
|
|
31
|
+
- **0.10.5 (2026-09-08)**: document installation, tool registration and the Web settings endpoint in official Harness 0.1.3-alpha.2; update Node requirements and clarify that `email_health` checks configuration only. Runtime code is unchanged from 0.10.4.
|
|
32
|
+
- **0.10.4 (2026-09-07)**: raise the minimum `mailparser` version to `3.9.22` and update the lockfile to use the patched `html-to-text 10.0.1 → deepmerge-ts 8.0.2` dependency chain for [CVE-2026-40345](https://github.com/RebeccaStevens/deepmerge-ts/security/advisories/GHSA-ggr8-5vv4-36mx). This does not rely on root-only `pnpm.overrides`, which cannot fix consumers installing this plugin as a dependency. Add runtime dependency-chain and HTML-message parsing regression tests. An affected dependency is not proof that mail input can trigger this vulnerability.
|
|
33
|
+
- **0.9.0**: new `email_watch` incremental new-mail tool (cursor-based, ideal for scheduled notifications); new "whale-girl courier" new-mail popup in the web UI (local skin artwork read at runtime + built-in fallback).
|
|
34
|
+
- **0.8.2**: `since` / `until` parameter descriptions unified to English, consistent with the other parameters, so multilingual agents read them correctly.
|
|
35
|
+
- **0.8.0/0.8.1**: `email_list` / `email_search` gained `since` / `until` date-range filters; new `email_health` account-configuration self-check; adapted to harness 0.1.2 (removed the deleted client-injection declaration).
|
|
36
|
+
- **0.6.2**: server-side search covers `cc` (subject / sender / recipients / CC); the body fallback scan also matches `to` / `cc` and one malformed message no longer aborts the batch; lists are UID-descending (newest first); `email_send` strictly validates attachment paths.
|
package/README.en.md
CHANGED
|
@@ -37,18 +37,10 @@ Example:
|
|
|
37
37
|
|
|
38
38
|
### Changelog
|
|
39
39
|
|
|
40
|
-
- **0.
|
|
41
|
-
- **0.
|
|
42
|
-
- **0.
|
|
43
|
-
- **0.10.
|
|
44
|
-
- **0.10.5 (2026-09-08)**: document installation, tool registration and the Web settings endpoint in official Harness 0.1.3-alpha.2; update Node requirements and clarify that `email_health` checks configuration only. Runtime code is unchanged from 0.10.4.
|
|
45
|
-
- **0.10.4 (2026-09-07)**: raise the minimum `mailparser` version to `3.9.22` and update the lockfile to use the patched `html-to-text 10.0.1 → deepmerge-ts 8.0.2` dependency chain for [CVE-2026-40345](https://github.com/RebeccaStevens/deepmerge-ts/security/advisories/GHSA-ggr8-5vv4-36mx). This does not rely on root-only `pnpm.overrides`, which cannot fix consumers installing this plugin as a dependency. Add runtime dependency-chain and HTML-message parsing regression tests. An affected dependency is not proof that mail input can trigger this vulnerability.
|
|
46
|
-
- **0.9.0**: new `email_watch` incremental new-mail tool (cursor-based, ideal for scheduled notifications); new "whale-girl courier" new-mail popup in the web UI (local skin artwork read at runtime + built-in fallback).
|
|
47
|
-
- **0.8.2**: `since` / `until` parameter descriptions unified to English, consistent with the other parameters, so multilingual agents read them correctly.
|
|
48
|
-
- **0.8.0/0.8.1**: `email_list` / `email_search` gained `since` / `until` date-range filters; new `email_health` account-configuration self-check; adapted to harness 0.1.2 (removed the deleted client-injection declaration).
|
|
49
|
-
- **0.6.2**: server-side search covers `cc` (subject / sender / recipients / CC); the body fallback scan also matches `to` / `cc` and one malformed message no longer aborts the batch; lists are UID-descending (newest first); `email_send` strictly validates attachment paths.
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
- **0.13.0 (2026-09-18)**: fixes for bodies truncated to nothing, `email_watch` skipping new mail, and the attachment cache ignoring UIDVALIDITY; all ten tools declare a timeout; reads and body search download text parts only; the scan fallback labels its own semantics. 262 tests.
|
|
41
|
+
- **0.12.0 (2026-09-18)**: send-as alias (`senderName` / `authUser` / `authPassword`) and `offset` paging for `email_search`; QQ match-everything searches no longer trusted; popup polling pauses while the tab is hidden.
|
|
42
|
+
- **0.11.0 (2026-09-18)**: gurio-wine’s four settings-page PRs (card editor / OAuth2 device-code login / bilingual panel / pinned `authKind`) plus the review fixes for SMTP OAuth2 and same-origin settings routes.
|
|
43
|
+
- **0.10.8 and earlier**: see [CHANGELOG.md](CHANGELOG.md).
|
|
52
44
|
## Compatibility
|
|
53
45
|
|
|
54
46
|
Co-load verification was performed on 2026-09-16 with official source builds of Harness `0.1.5-rc.2` and `0.1.6-alpha.1`: all 18 components load alongside ModLens, with passing tool schemas, skill registration and offline read-only calls.
|
|
@@ -161,6 +153,9 @@ The settings page's "Server presets" fold-out edits these presets visually, and
|
|
|
161
153
|
| `provider` | — | Preset name; auto-fills imap/smtp addresses. Explicitly written host/port/secure take precedence |
|
|
162
154
|
| `user` | required | Login email address |
|
|
163
155
|
| `password` | required* | Authorization code / app-specific password; *can also use the env var `DSH_EMAIL_PASSWORD` |
|
|
156
|
+
| `senderName` | — | Display name for the From header; the address itself stays `user` |
|
|
157
|
+
| `authUser` | = `user` | Login account. Alias / SMTP-relay setups: `user` is the address mail is sent from, this is the account IMAP/SMTP authenticates with |
|
|
158
|
+
| `authPassword` | = `password` | Password for `authUser`; only needed when the login account differs from `user` and has its own password |
|
|
164
159
|
| `imap.host/port/secure` | per preset | Incoming server (also `connectionTimeoutMs` / `socketTimeoutMs` for timeouts) |
|
|
165
160
|
| `smtp.host/port/secure` | per preset | Outgoing server |
|
|
166
161
|
| `inboxFolder` | `INBOX` | Default folder for read/send tools |
|
|
@@ -222,6 +217,7 @@ Microsoft has disabled username+password basic auth for Exchange Online: persona
|
|
|
222
217
|
## Known limitations
|
|
223
218
|
|
|
224
219
|
- **OAuth2 covers Outlook / Exchange Online only, and requires your own app ID**: device-code login supports both IMAP and SMTP, but the plugin **bundles no third-party app registration** — OAuth2 accounts must supply their own `clientId` (free to register; see "Outlook OAuth2" above). Other environments that mandate OAuth (e.g. Google Workspace) remain unusable; use the provider's app-specific password / authorization code instead.
|
|
220
|
+
- **Search match counts**: server hits are re-checked against the envelopes (see `email_search` above); when they hold up, "N matches" is the count the server reported while every listed row really carries the keyword. The local body-scan fallback only looked at the newest `bodySearchLimit` messages, so it renders "N rows on this page (only the newest N scanned)" instead of "N matches".
|
|
225
221
|
- **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`).
|
|
226
222
|
- **Attachments**: inline images aren't downloadable separately yet; a failed attachment match errors instead of downloading the wrong file (safe default).
|
|
227
223
|
- **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.
|
package/README.md
CHANGED
|
@@ -45,21 +45,10 @@ IMAP/SMTP email tools for DeepSeek Harness, with replies, forwarding, mailbox or
|
|
|
45
45
|
|
|
46
46
|
### 版本记录
|
|
47
47
|
|
|
48
|
-
- **0.
|
|
49
|
-
- **0.
|
|
50
|
-
- **0.
|
|
51
|
-
- **0.10.
|
|
52
|
-
- **0.10.5(2026-09-08)**:补充官方 Harness 0.1.3-alpha.2 的安装、工具注册及 Web 设置接口验证,更新 Node 版本要求,明确 `email_health` 只检查配置;运行时代码与 0.10.4 相同。
|
|
53
|
-
- **0.10.4(2026-09-07)**:将 `mailparser` 最低版本提升到 `3.9.22` 并更新锁文件,使用 `html-to-text 10.0.1 → deepmerge-ts 8.0.2` 的修复链处理 [CVE-2026-40345](https://github.com/RebeccaStevens/deepmerge-ts/security/advisories/GHSA-ggr8-5vv4-36mx)。不依赖插件作为下游依赖安装时不生效的根级 `pnpm.overrides`;新增真实依赖链与 HTML 邮件解析回归测试。依赖告警不等于已证实邮件输入可触发该漏洞。
|
|
54
|
-
- **0.10.1**:补发制品——已发布的 0.10.0 打包时只含 `email_mark`,本版同时包含 `email_mark` 与 `email_reply`,代码与 0.10.0 的 main 一致。
|
|
55
|
-
- **0.10.0**:新增 `email_mark`(已读/未读/星标/移动文件夹,补齐收发闭环的整理侧)与 `email_reply`(回复/回复全部/转发,自动线程头+引文,走发信审批门);连接池按读/写模式分别管理邮箱打开状态。
|
|
56
|
-
- **0.9.1**:修复设置页空主机遮蔽 provider 预设(#3/#6);IMAP 连接超时不再杀死整个 DSH 进程(#4);暗色模式输入控件可见(#2);密码栏提示环境变量 `DSH_EMAIL_PASSWORD` 免明文方案(#5)。
|
|
57
|
-
- **0.9.0**:新增 `email_watch` 增量新邮件检查工具(游标式,适合定时提醒);Web 端新增「鲸鱼娘递信」新邮件弹窗(本地皮肤素材运行时读取 + 内置回退图)。
|
|
58
|
-
- **0.8.2**:`since` / `until` 参数描述与其余参数统一为英文,方便多语言 agent 理解。
|
|
59
|
-
- **0.8.0/0.8.1**:`email_list` / `email_search` 新增 `since` / `until` 日期范围过滤;新增 `email_health` 账号配置自检;适配 harness 0.1.2(清理已删除的客户端注入声明)。
|
|
60
|
-
- **0.6.2**:服务器端搜索补齐 `cc`,搜索范围真正覆盖主题 / 发件人 / 收件人 / 抄送;正文回退扫描也匹配 `to` / `cc`,单封解析失败不中断整批;列表强制 UID 降序「最新在前」;`email_send` 附件参数严格校验。
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
- **0.13.0(2026-09-18)**:修复长正文截断成空、`email_watch` 永久漏报新邮件、附件缓存跨 UIDVALIDITY 失效;10 个工具声明超时;读信/搜索只下正文分段;搜索回退标明扫描口径。测试 262 项。
|
|
49
|
+
- **0.12.0(2026-09-18)**:新增发送别名(`senderName` / `authUser` / `authPassword`)与 `email_search` 的 `offset` 翻页;修复 QQ 搜索假命中;弹窗轮询按页面可见性节流。
|
|
50
|
+
- **0.11.0(2026-09-18)**:合入 gurio-wine 的设置页四连(卡片编辑器 / OAuth2 设备码登录 / 双语面板 / `authKind` 钉住),并修掉评审发现的 SMTP OAuth2、设置路由同源校验等问题。
|
|
51
|
+
- **0.10.8 及更早**:见 [CHANGELOG.md](CHANGELOG.md)。
|
|
63
52
|
## 兼容性
|
|
64
53
|
|
|
65
54
|
2026-09-16 曾在官方源码构建的 Harness `0.1.5-rc.2` 和 `0.1.6-alpha.1` 上完成同载验证:18 个组件与 ModLens 同载,工具 schema、技能注册及离线只读调用检查通过。
|
|
@@ -172,6 +161,9 @@ dsh plugin --profile web remove dsh-email
|
|
|
172
161
|
| `provider` | 无 | 预设名,自动填 imap/smtp 地址;显式写的 host/port/secure 优先 |
|
|
173
162
|
| `user` | 必填 | 登录邮箱地址 |
|
|
174
163
|
| `password` | 必填* | 授权码/应用专用密码;*也可用环境变量 `DSH_EMAIL_PASSWORD` |
|
|
164
|
+
| `senderName` | 无 | 发件显示名:只改收件人看到的名称,发件地址仍是 `user` |
|
|
165
|
+
| `authUser` | = `user` | 登录账号。别名 / SMTP 中继场景:`user` 是发件地址,这里填真正用于 IMAP/SMTP 认证的账号 |
|
|
166
|
+
| `authPassword` | = `password` | `authUser` 对应的密码;只有登录账号与 `user` 不同、且密码也不一样时才需要 |
|
|
175
167
|
| `imap.host/port/secure` | 按预设 | 收信服务器(另有 connectionTimeoutMs/socketTimeoutMs 可调超时) |
|
|
176
168
|
| `smtp.host/port/secure` | 按预设 | 发信服务器 |
|
|
177
169
|
| `inboxFolder` | `INBOX` | 收发工具默认使用的文件夹 |
|
|
@@ -233,6 +225,7 @@ dsh plugin --profile web remove dsh-email
|
|
|
233
225
|
## 已知限制
|
|
234
226
|
|
|
235
227
|
- **OAuth2 仅覆盖 Outlook / Exchange Online,且需自带应用 ID**:设备码登录已支持 IMAP 与 SMTP 双端,但插件**不内置任何第三方应用注册**,OAuth2 账号必须填自己的 `clientId`(免费注册,见上文「Outlook OAuth2」)。Google Workspace 等其它强制 OAuth 的环境仍不可用,只能用服务商的应用专用密码 / 授权码。
|
|
228
|
+
- **搜索的匹配数**:服务器命中会先用信封复核(见上文 `email_search`);复核通过时「共 N 条匹配」沿用服务器给出的条数,而列出的每一行都保证真的带关键词。正文回退扫描只看了最近 `bodySearchLimit` 封,不知道全文件夹匹配数,因此渲染为「本页 N 条(仅扫描最近 N 封)」而不是「共 N 条」。
|
|
236
229
|
- **正文搜索**:服务器端只搜 subject / from / to / cc;多数服务器(如 QQ)的 IMAP `TEXT` / `HEADER` 搜索不可靠,无结果时回退到最近 `bodySearchLimit` 封的正文扫描(较慢,可用 `bodySearchFallback` 关闭)。
|
|
237
230
|
- **附件**:内嵌图片暂不支持单独下载;附件定位失败会直接报错而不是下载错误文件(安全默认)。
|
|
238
231
|
- **密码落盘**:设置页保存的授权码以明文写在本机 `settings.yaml`(secret 标记只保证它不进日志 / 导出 / 诊断,不做磁盘加密)。请勿把 `settings.yaml` 交给不信任的人。
|
package/lib/client.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// NOTE(维护者请看这里):这个文件就是插件网页端的**源码**,不经过 tsc —— 仓库里没有
|
|
2
|
+
// src/client.ts,`pnpm run build` 也不会重写它。改设置面板 / 鲸鱼娘弹窗请直接改这里,
|
|
3
|
+
// 改完用 `node --check lib/client.js` 自检(CI 里也会跑这一步)。
|
|
1
4
|
window.__ModuleLoader__.load({ id: "dsh-email", factory: (require) => {
|
|
2
5
|
var module = { exports: {} }; var exports = module.exports;
|
|
3
6
|
"use strict";
|
|
@@ -311,6 +314,15 @@ var UI = {
|
|
|
311
314
|
"card.passwordPlaceholder": "留空保持不变",
|
|
312
315
|
"card.passwordSaved": "已存有授权码:留空保持不变,清空后填内容即覆盖。",
|
|
313
316
|
"card.passwordHint": "留空即不写入 password 键。",
|
|
317
|
+
"card.senderNameLabel": "发件显示名(可选)",
|
|
318
|
+
"card.senderNamePlaceholder": "只影响收件人看到的名称",
|
|
319
|
+
"card.senderNameHint": "只改收件人看到的显示名,发件地址仍是上面的邮箱地址。",
|
|
320
|
+
"card.authUserLabel": "登录账号(与邮箱地址不同时填)",
|
|
321
|
+
"card.authUserPlaceholder": "留空 = 用上面的邮箱地址登录",
|
|
322
|
+
"card.authUserHint": "别名或中继场景:上面填信件发出的地址,这里填真正用于 IMAP/SMTP 登录的账号。留空即与邮箱地址相同。",
|
|
323
|
+
"card.authPasswordLabel": "登录账号的密码(可选)",
|
|
324
|
+
"card.authPasswordSaved": "已存有登录密码:留空保持不变,清空后填内容即覆盖。",
|
|
325
|
+
"card.authPasswordHint": "留空即沿用上面那栏的授权码 / 应用专用密码;只有登录账号与邮箱地址不同、且密码也不一样时才需要单独填。",
|
|
314
326
|
"card.inboxLabel": "收件文件夹(默认 INBOX)",
|
|
315
327
|
"card.autoSaveHint": "改动会自动保存。",
|
|
316
328
|
"card.autoSaveHintNoDefault": " 现在有多个账号但没有默认账号,必须先指定一个。",
|
|
@@ -480,6 +492,15 @@ var UI = {
|
|
|
480
492
|
"card.passwordPlaceholder": "Leave empty to keep it",
|
|
481
493
|
"card.passwordSaved": "A password is already stored: leave empty to keep it, or clear the field and type to overwrite.",
|
|
482
494
|
"card.passwordHint": "Leave it empty and the password key is not written.",
|
|
495
|
+
"card.senderNameLabel": "Sender display name (optional)",
|
|
496
|
+
"card.senderNamePlaceholder": "Only the name recipients see",
|
|
497
|
+
"card.senderNameHint": "Changes only the name recipients see; the address stays the one above.",
|
|
498
|
+
"card.authUserLabel": "Login user (when it differs from the address)",
|
|
499
|
+
"card.authUserPlaceholder": "Empty = log in with the address above",
|
|
500
|
+
"card.authUserHint": "Alias or relay: the address above is what mail is sent from, this is the account IMAP/SMTP authenticates. Empty means they are the same.",
|
|
501
|
+
"card.authPasswordLabel": "Password for the login user (optional)",
|
|
502
|
+
"card.authPasswordSaved": "A login password is stored: leave it empty to keep it, or type to replace it.",
|
|
503
|
+
"card.authPasswordHint": "Leave empty to reuse the authorization code / app password above; only needed when the login user has a password of its own.",
|
|
483
504
|
"card.inboxLabel": "Inbox folder (default INBOX)",
|
|
484
505
|
"card.autoSaveHint": "Changes save automatically.",
|
|
485
506
|
"card.autoSaveHintNoDefault": " There are several accounts but no default account yet; you must pick one first.",
|
|
@@ -737,6 +758,11 @@ function draftFromCard(card) {
|
|
|
737
758
|
provider: card.provider === undefined || card.provider === null ? "" : String(card.provider),
|
|
738
759
|
user: card.user === undefined || card.user === null ? "" : String(card.user),
|
|
739
760
|
password: "",
|
|
761
|
+
// 登录密码与 password 同一套三态契约:卡片永远拿不到明文,只有"是否已存"。
|
|
762
|
+
authPassword: "",
|
|
763
|
+
hasAuthPassword: card.hasAuthPassword === true,
|
|
764
|
+
senderName: card.senderName === undefined || card.senderName === null ? "" : String(card.senderName),
|
|
765
|
+
authUser: card.authUser === undefined || card.authUser === null ? "" : String(card.authUser),
|
|
740
766
|
clientId: card.clientId === undefined || card.clientId === null ? "" : String(card.clientId),
|
|
741
767
|
// authKind 是生效裁决(决定显示登录区还是密码框),authKindSetting 是用户钉住
|
|
742
768
|
// 了什么('' = 自动)。两者必须分开:都从裁决读的话,「自动」与「显式密码」在
|
|
@@ -834,6 +860,10 @@ function draftToInput(draft) {
|
|
|
834
860
|
};
|
|
835
861
|
if (draft.provider) input.provider = draft.provider;
|
|
836
862
|
if (!isOauthDraft(draft) && draft.password) input.password = draft.password;
|
|
863
|
+
// 显示名/登录名是普通三态字段(留空 = 清掉该键),密码与 password 同规:只有真打了字才写。
|
|
864
|
+
if (draft.senderName !== undefined) input.senderName = String(draft.senderName || "").trim();
|
|
865
|
+
if (!isOauthDraft(draft) && draft.authUser !== undefined) input.authUser = String(draft.authUser || "").trim();
|
|
866
|
+
if (!isOauthDraft(draft) && draft.authPassword) input.authPassword = draft.authPassword;
|
|
837
867
|
// Only an OAuth2 card models the application id; for any other account the
|
|
838
868
|
// field stays undefined, which the backend reads as「没说」and leaves the
|
|
839
869
|
// stored key alone. An empty string here is the user clearing it on purpose.
|
|
@@ -1699,6 +1729,11 @@ function AccountCardsEditor(props) {
|
|
|
1699
1729
|
h("label", null, t("card.addressLabel")),
|
|
1700
1730
|
fieldInput("text", draft.user, (v) => patchDraft(name, { user: v }), "you@example.com"),
|
|
1701
1731
|
]),
|
|
1732
|
+
h("div", { className: "dshe-field" }, [
|
|
1733
|
+
h("label", null, t("card.senderNameLabel")),
|
|
1734
|
+
fieldInput("text", draft.senderName, (v) => patchDraft(name, { senderName: v }), t("card.senderNamePlaceholder")),
|
|
1735
|
+
h("div", { className: "dshe-hint" }, t("card.senderNameHint")),
|
|
1736
|
+
]),
|
|
1702
1737
|
// 认证方式只在「可能是 OAuth2」的账号上出现:给 QQ/163 用户多一个
|
|
1703
1738
|
// 下拉框只是噪音。它是那条逃生舱的唯一面板入口——仍能用应用密码连
|
|
1704
1739
|
// Exchange Online 的租户(混合/本地部署、SMTP AUTH 未关)靠它才不用
|
|
@@ -1738,19 +1773,41 @@ function AccountCardsEditor(props) {
|
|
|
1738
1773
|
onCancel: () => cancelOauth(name),
|
|
1739
1774
|
onClientId: (v) => patchDraft(name, { clientId: v }),
|
|
1740
1775
|
})
|
|
1741
|
-
: h(
|
|
1742
|
-
h("
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1776
|
+
: h(React.Fragment, null, [
|
|
1777
|
+
h("div", { className: "dshe-field" }, [
|
|
1778
|
+
h("label", null, t("card.passwordLabel")),
|
|
1779
|
+
h("input", {
|
|
1780
|
+
type: "password",
|
|
1781
|
+
value: draft.password,
|
|
1782
|
+
disabled: busy !== "",
|
|
1783
|
+
placeholder: t("card.passwordPlaceholder"),
|
|
1784
|
+
onChange: (e) => patchDraft(name, { password: e.target.value }),
|
|
1785
|
+
}),
|
|
1786
|
+
h("div", { className: "dshe-hint" },
|
|
1787
|
+
view.hasPassword
|
|
1788
|
+
? t("card.passwordSaved")
|
|
1789
|
+
: t("card.passwordHint")),
|
|
1790
|
+
]),
|
|
1791
|
+
// 别名/中继:登录名与登录密码都只在密码类账号上有意义(OAuth2 用 token 登录)。
|
|
1792
|
+
h("div", { className: "dshe-field" }, [
|
|
1793
|
+
h("label", null, t("card.authUserLabel")),
|
|
1794
|
+
fieldInput("text", draft.authUser, (v) => patchDraft(name, { authUser: v }), t("card.authUserPlaceholder")),
|
|
1795
|
+
h("div", { className: "dshe-hint" }, t("card.authUserHint")),
|
|
1796
|
+
]),
|
|
1797
|
+
h("div", { className: "dshe-field" }, [
|
|
1798
|
+
h("label", null, t("card.authPasswordLabel")),
|
|
1799
|
+
h("input", {
|
|
1800
|
+
type: "password",
|
|
1801
|
+
value: draft.authPassword,
|
|
1802
|
+
disabled: busy !== "",
|
|
1803
|
+
placeholder: t("card.passwordPlaceholder"),
|
|
1804
|
+
onChange: (e) => patchDraft(name, { authPassword: e.target.value }),
|
|
1805
|
+
}),
|
|
1806
|
+
h("div", { className: "dshe-hint" },
|
|
1807
|
+
view.hasAuthPassword
|
|
1808
|
+
? t("card.authPasswordSaved")
|
|
1809
|
+
: t("card.authPasswordHint")),
|
|
1810
|
+
]),
|
|
1754
1811
|
]),
|
|
1755
1812
|
h("div", { className: "dshe-field" }, [
|
|
1756
1813
|
h("label", null, t("card.inboxLabel")),
|
|
@@ -2428,27 +2485,57 @@ function startWhaleWidget() {
|
|
|
2428
2485
|
hideTimer = setTimeout(closePopup, 12000);
|
|
2429
2486
|
};
|
|
2430
2487
|
|
|
2488
|
+
// 轮询策略:页面不可见时完全不打扰邮箱服务器;失败指数退避(上限 10 分钟),
|
|
2489
|
+
// 成功立刻回到 30s;快照(皮肤 / 账号列表)变化很慢,最多 10 分钟刷新一次。
|
|
2490
|
+
const POLL_MS = 30000;
|
|
2491
|
+
const SNAPSHOT_MS = 600000;
|
|
2492
|
+
const MAX_BACKOFF_MS = 600000;
|
|
2493
|
+
let failures = 0;
|
|
2494
|
+
let lastSnapshot = 0;
|
|
2495
|
+
let hasAccounts = true;
|
|
2496
|
+
|
|
2431
2497
|
const tick = async () => {
|
|
2498
|
+
if (document.hidden) return;
|
|
2432
2499
|
try {
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2500
|
+
if (!hasAccounts || Date.now() - lastSnapshot > SNAPSHOT_MS) {
|
|
2501
|
+
const snap = await api();
|
|
2502
|
+
lastSnapshot = Date.now();
|
|
2503
|
+
if (snap && snap.whale) {
|
|
2504
|
+
whaleUrl = snap.whale.url || "";
|
|
2505
|
+
whaleCredit = snap.whale.credit || "";
|
|
2506
|
+
}
|
|
2507
|
+
hasAccounts = !!(snap && snap.accounts && snap.accounts.length > 0);
|
|
2437
2508
|
}
|
|
2438
|
-
if (!
|
|
2509
|
+
if (!hasAccounts) return;
|
|
2439
2510
|
const value = await api("watch", { limit: 5 });
|
|
2440
2511
|
if (value && value.newCount > 0) showPopup(value);
|
|
2512
|
+
failures = 0;
|
|
2441
2513
|
} catch (e) {
|
|
2442
|
-
// Not configured or transient error: stay silent
|
|
2514
|
+
// Not configured or transient error: stay silent and back off.
|
|
2515
|
+
failures = Math.min(failures + 1, 8);
|
|
2443
2516
|
}
|
|
2444
2517
|
};
|
|
2445
2518
|
|
|
2446
|
-
|
|
2447
|
-
|
|
2519
|
+
const schedule = () => {
|
|
2520
|
+
clearTimeout(pollTimer);
|
|
2521
|
+
if (document.hidden) return; // 回到前台时 onVisibility 会立刻补一次
|
|
2522
|
+
const delay = failures === 0 ? POLL_MS : Math.min(POLL_MS * Math.pow(2, failures - 1), MAX_BACKOFF_MS);
|
|
2523
|
+
pollTimer = setTimeout(() => { void tick().then(schedule); }, delay);
|
|
2524
|
+
};
|
|
2525
|
+
|
|
2526
|
+
const onVisibility = () => {
|
|
2527
|
+
clearTimeout(pollTimer);
|
|
2528
|
+
if (document.hidden) return;
|
|
2529
|
+
failures = 0;
|
|
2530
|
+
void tick().then(schedule);
|
|
2531
|
+
};
|
|
2532
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
2533
|
+
void tick().then(schedule);
|
|
2448
2534
|
// 弹窗是命令式 DOM,没有 React 那层重渲染:语言切换后自己重画一次(开着的才重画)。
|
|
2449
2535
|
relocalize = () => { if (current !== null && root.childElementCount > 0) showPopup(current); };
|
|
2450
2536
|
return () => {
|
|
2451
|
-
|
|
2537
|
+
clearTimeout(pollTimer);
|
|
2538
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
2452
2539
|
if (hideTimer) clearTimeout(hideTimer);
|
|
2453
2540
|
relocalize = null;
|
|
2454
2541
|
root.remove();
|
package/lib/config.d.ts
CHANGED
|
@@ -58,6 +58,20 @@ export interface AccountConfig {
|
|
|
58
58
|
provider?: ProviderRef;
|
|
59
59
|
user?: string;
|
|
60
60
|
password?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Display name for the From header. The address stays `user` — recipients
|
|
63
|
+
* must see the mailbox that owns the mail, not the login.
|
|
64
|
+
*/
|
|
65
|
+
senderName?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Login handed to IMAP/SMTP when it differs from `user`: the alias case,
|
|
68
|
+
* where `user` is the address mail is sent *from* and the server only
|
|
69
|
+
* authenticates the real account, or a relay whose login is not a mailbox
|
|
70
|
+
* at all. Defaults to `user`.
|
|
71
|
+
*/
|
|
72
|
+
authUser?: string;
|
|
73
|
+
/** Password that goes with `authUser`. Defaults to `password`. */
|
|
74
|
+
authPassword?: string;
|
|
61
75
|
/**
|
|
62
76
|
* Public-client id used by the OAuth2 device-code flow. Only read for an
|
|
63
77
|
* OAuth2 account, where it overrides OUTLOOK_OAUTH2_CLIENT_ID.
|
|
@@ -141,6 +155,12 @@ export declare const EMAIL_PASSWORD_ENV = "DSH_EMAIL_PASSWORD";
|
|
|
141
155
|
/** Fully resolved, validated configuration for one account. */
|
|
142
156
|
export interface ResolvedEmailConfig {
|
|
143
157
|
user: string;
|
|
158
|
+
/** Display name for the From header, '' when the account does not set one. */
|
|
159
|
+
senderName: string;
|
|
160
|
+
/** Login actually handed to IMAP/SMTP (== user unless authUser is set). */
|
|
161
|
+
authUser: string;
|
|
162
|
+
/** Password for authUser (== password unless authPassword is set). */
|
|
163
|
+
authPassword: string;
|
|
144
164
|
/**
|
|
145
165
|
* The app password / 授权码. Empty for an OAuth2 account — that is the point:
|
|
146
166
|
* nothing is stored, the token store holds the credential instead.
|
package/lib/config.js
CHANGED
|
@@ -319,6 +319,12 @@ function resolveAccount(name, common, acc, allowEnvPassword, providers, known) {
|
|
|
319
319
|
// The settings form uses '' for an empty password. In single-account mode
|
|
320
320
|
// that explicitly selects the environment fallback; named accounts stay isolated.
|
|
321
321
|
const password = (acc.password ?? common.password) || (allowEnvPassword ? process.env[EMAIL_PASSWORD_ENV] ?? '' : '');
|
|
322
|
+
// An alias account sends from `user` but authenticates as somebody else, and a
|
|
323
|
+
// relay may use a different password than the mailbox it delivers for. Both
|
|
324
|
+
// default to the single-account pair so nothing changes for existing setups.
|
|
325
|
+
const authUser = (acc.authUser ?? common.authUser ?? '').trim() || user;
|
|
326
|
+
const authPassword = (acc.authPassword ?? common.authPassword) || password;
|
|
327
|
+
const senderName = (acc.senderName ?? common.senderName ?? '').trim();
|
|
322
328
|
const imap = {
|
|
323
329
|
host: acc.imap?.host ?? common.imap?.host ?? preset?.imap.host,
|
|
324
330
|
port: acc.imap?.port ?? common.imap?.port ?? preset?.imap.port,
|
|
@@ -347,8 +353,9 @@ function resolveAccount(name, common, acc, allowEnvPassword, providers, known) {
|
|
|
347
353
|
// An OAuth2 account has no password on purpose: its credential is the token
|
|
348
354
|
// in the OAuth2 store, and requiring a password would demand a secret
|
|
349
355
|
// Microsoft no longer accepts for Exchange Online.
|
|
350
|
-
if (!oauth2 &&
|
|
351
|
-
problems.push(`账号 "${name}" 的 password 未填写(单账号可用环境变量 ${EMAIL_PASSWORD_ENV})`);
|
|
356
|
+
if (!oauth2 && authPassword === '') {
|
|
357
|
+
problems.push(`账号 "${name}" 的 ${authUser === user ? 'password' : 'authPassword'} 未填写(单账号可用环境变量 ${EMAIL_PASSWORD_ENV})`);
|
|
358
|
+
}
|
|
352
359
|
if (imap.host === undefined || imap.host === '')
|
|
353
360
|
problems.push(`账号 "${name}" 的 imap.host 未填写(可填 provider 预设:${known.join('/')})`);
|
|
354
361
|
if (smtp.host === undefined || smtp.host === '')
|
|
@@ -359,8 +366,13 @@ function resolveAccount(name, common, acc, allowEnvPassword, providers, known) {
|
|
|
359
366
|
const clientId = (acc.clientId ?? common.clientId ?? '').trim();
|
|
360
367
|
return {
|
|
361
368
|
user,
|
|
362
|
-
|
|
363
|
-
//
|
|
369
|
+
senderName,
|
|
370
|
+
// OAuth2 logs in with the token's own account, so the alias login pair only
|
|
371
|
+
// exists for password accounts; and an OAuth2 account never carries a
|
|
372
|
+
// password at all — a stale one left in the YAML from before the provider
|
|
373
|
+
// changed must not travel into the pool.
|
|
374
|
+
authUser: oauth2 ? user : authUser,
|
|
375
|
+
authPassword: oauth2 ? '' : authPassword,
|
|
364
376
|
password: oauth2 ? '' : password,
|
|
365
377
|
authKind: oauth2 ? 'oauth2' : 'password',
|
|
366
378
|
...(clientId !== '' ? { clientId } : {}),
|
package/lib/mail-client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ImapFlow } from 'imapflow';
|
|
2
2
|
import type { ResolvedEmailConfig, ResolvedEmailSettings } from './config.js';
|
|
3
|
-
import type { AddressEntry, EmailAttachmentMeta, EmailAttachmentResult, EmailFoldersResult, EmailListResult, EmailMarkAction, EmailMarkResult, EmailReadResult, EmailReplyMode, EmailReplyResult, EmailSearchResult, EmailSendResult } from './types.js';
|
|
3
|
+
import type { AddressEntry, EmailAttachmentMeta, EmailAttachmentResult, EmailFoldersResult, EmailListResult, EmailMarkAction, EmailMarkResult, EmailReadResult, EmailReplyMode, EmailReplyResult, EmailSearchResult, EmailSendResult, ListedMessage } from './types.js';
|
|
4
4
|
export declare class MailError extends Error {
|
|
5
5
|
constructor(message: string);
|
|
6
6
|
}
|
|
@@ -46,12 +46,12 @@ export type SmtpAuth = {
|
|
|
46
46
|
* `accessToken` (imapflow then runs AUTHENTICATE XOAUTH2) and a password
|
|
47
47
|
* account with `pass`, exactly as before.
|
|
48
48
|
*/
|
|
49
|
-
export declare function imapAuthOf(cfg: Pick<ResolvedEmailConfig, '
|
|
49
|
+
export declare function imapAuthOf(cfg: Pick<ResolvedEmailConfig, 'authUser' | 'authPassword' | 'authKind'>, accessToken?: string): ImapAuth;
|
|
50
50
|
/**
|
|
51
51
|
* Nodemailer consumes an OAuth2 token through accessToken, not pass.
|
|
52
52
|
* Refresh remains owned by this plugin; no refresh credentials leave here.
|
|
53
53
|
*/
|
|
54
|
-
export declare function smtpAuthOf(cfg: Pick<ResolvedEmailConfig, '
|
|
54
|
+
export declare function smtpAuthOf(cfg: Pick<ResolvedEmailConfig, 'authUser' | 'authPassword' | 'authKind'>, accessToken?: string): SmtpAuth;
|
|
55
55
|
/** The message an OAuth2 account gets when the mailbox has to be logged into again. */
|
|
56
56
|
export declare const OAUTH2_RELOGIN_MESSAGE = "\u90AE\u7BB1\u767B\u5F55\u5931\u8D25\uFF1A\u8BF7\u5230\u8BBE\u7F6E\u9875\u91CD\u65B0\u767B\u5F55\uFF08Microsoft \u8D26\u53F7\u4F7F\u7528\u8BBE\u5907\u7801\u767B\u5F55\uFF0C\u4E0D\u4F7F\u7528\u6388\u6743\u7801\uFF09";
|
|
57
57
|
/**
|
|
@@ -106,7 +106,7 @@ export declare function extractMessageIds(source: Buffer): {
|
|
|
106
106
|
* be tested without a connection: recipients exclude the sending account,
|
|
107
107
|
* subject prefixes never stack, the original text is quoted underneath.
|
|
108
108
|
*/
|
|
109
|
-
export declare function buildReplyMessage(original: OriginalDigest, mode: EmailReplyMode, selfAddress: string, text: string, forwardTo?: string): BuiltReply;
|
|
109
|
+
export declare function buildReplyMessage(original: OriginalDigest, mode: EmailReplyMode, selfAddress: string | readonly string[], text: string, forwardTo?: string): BuiltReply;
|
|
110
110
|
/**
|
|
111
111
|
* One mailbox pool for the whole plugin: pooled IMAP connections per
|
|
112
112
|
* account plus pooled SMTP transporters, with idle sweep and error eviction.
|
|
@@ -122,6 +122,18 @@ export declare class EmailPool {
|
|
|
122
122
|
resolveName(name?: string): string;
|
|
123
123
|
/** Serialize operations per account: one IMAP connection serves one op at a time. */
|
|
124
124
|
private enqueue;
|
|
125
|
+
private readonly readCache;
|
|
126
|
+
private readonly folderCache;
|
|
127
|
+
/** UIDVALIDITY 变了以后同一 uid 可能指向另一封邮件,缓存键必须带上它。 */
|
|
128
|
+
private uidValidityOf;
|
|
129
|
+
/** Remember a parsed attachment index so email_attachment can skip the refetch. */
|
|
130
|
+
private rememberRead;
|
|
131
|
+
/**
|
|
132
|
+
* The attachment index for one message: the cached one when email_read already
|
|
133
|
+
* produced it, otherwise a fresh parse of the full source plus its bodyStructure.
|
|
134
|
+
*/
|
|
135
|
+
private attachmentIndexOf;
|
|
136
|
+
private recallRead;
|
|
125
137
|
withImap<T>(accountName: string | undefined, folder: string | null, run: (client: ImapFlow) => Promise<T>, readOnly?: boolean, signal?: AbortSignal): Promise<T>;
|
|
126
138
|
private createImap;
|
|
127
139
|
/**
|
|
@@ -158,8 +170,32 @@ export declare class EmailPool {
|
|
|
158
170
|
* single attempt they always had.
|
|
159
171
|
*/
|
|
160
172
|
private sendMail;
|
|
173
|
+
/**
|
|
174
|
+
* Download one MIME part through imapflow's decode pipeline: transfer
|
|
175
|
+
* encoding and charset are handled there, maxBytes caps what is fetched.
|
|
176
|
+
*/
|
|
177
|
+
private downloadPartText;
|
|
178
|
+
/**
|
|
179
|
+
* The message body without its attachments. undefined when the structure has
|
|
180
|
+
* no usable text part or the server refuses the part fetch, so the caller can
|
|
181
|
+
* fall back to the full-source path for that one message.
|
|
182
|
+
*/
|
|
183
|
+
private bodyTextFromParts;
|
|
161
184
|
list(accountName: string | undefined, folder: string, limit: number, offset: number, unreadOnly: boolean, since?: Date, until?: Date, signal?: AbortSignal): Promise<EmailListResult>;
|
|
162
|
-
|
|
185
|
+
/**
|
|
186
|
+
* The uid index behind email_watch: SEARCH UNSEEN only, no envelopes and no
|
|
187
|
+
* bodies. The caller decides which uids it actually needs to report.
|
|
188
|
+
*/
|
|
189
|
+
unseenUids(accountName: string | undefined, folder: string, signal?: AbortSignal): Promise<{
|
|
190
|
+
account: string;
|
|
191
|
+
folder: string;
|
|
192
|
+
uidValidity: number;
|
|
193
|
+
count: number;
|
|
194
|
+
uids: number[];
|
|
195
|
+
}>;
|
|
196
|
+
/** Fetch the envelopes for one uid batch: the rows email_watch will report. */
|
|
197
|
+
fetchByUids(accountName: string | undefined, folder: string, uids: number[], signal?: AbortSignal): Promise<ListedMessage[]>;
|
|
198
|
+
search(accountName: string | undefined, query: string, folder: string, limit: number, offset: number, since?: Date, until?: Date, signal?: AbortSignal): Promise<EmailSearchResult>;
|
|
163
199
|
/**
|
|
164
200
|
* Confirm server-side hits against the mailbox itself: fetch the envelopes
|
|
165
201
|
* of the newest candidates — the same window the body-scan fallback looks at
|