dsh-email 0.13.0 → 0.13.2

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 CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## 中文版
6
6
 
7
+ - **0.13.2(2026-09-21)**:同名附件优先按真实 MIME 分段编号下载;旧解析元数据按一对一匹配,避免多个序号都取到第一个同名文件。保留正文分段下载和附件索引缓存,新增文件字节级回归,268 项测试通过。
8
+ - **0.13.1(2026-09-19)**:**新增**:内置一份公共客户端注册(`15dcd5aa-…`,贡献者 [gurio-wine](https://github.com/gurio-wine) 在 [PR #13](https://github.com/STARDUSTLC666/dsh-email/pull/13) 注册,并授权本项目内置使用,特此致谢)——Outlook / Exchange Online 的 OAuth2 登录**开箱即用**,不再需要每个用户自己注册应用。设置页卡片会显示当前生效的应用 ID:留空即用内置的社区应用,填入自己的 `clientId` 即覆盖(账号级仍可覆盖顶层简写)。**文档**:README 说明内置应用来自谁、代价是什么、如何换成自己的;并说明 token 与签发它的应用 ID 绑定,将来替换内置注册需要这些账号重新登录一次。测试 262 → 264 项。
7
9
  - **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
10
  - **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
11
  - **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))。
@@ -22,6 +24,7 @@
22
24
 
23
25
  ## English
24
26
 
27
+ - **0.13.2 (2026-09-21)**: download attachments by their real MIME section IDs, fixing duplicate filenames selecting the first file. Legacy parsed metadata matches sections one-to-one. Body-only reads and attachment-index caching remain intact; 268 tests pass, including downloaded-file byte checks.
25
28
  - **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
29
  - **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
30
  - **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)).
package/README.en.md CHANGED
@@ -37,12 +37,16 @@ Example:
37
37
 
38
38
  ### Changelog
39
39
 
40
+ - **0.13.2 (2026-09-21)**: download attachments by their real MIME section IDs, fixing duplicate filenames selecting the first file. Legacy parsed metadata matches sections one-to-one. Body-only reads and attachment-index caching remain intact; 268 tests pass, including downloaded-file byte checks.
41
+ - **0.13.1 (2026-09-19)**: ships a community public-client registration (thanks [gurio-wine](https://github.com/gurio-wine)), so Outlook / Exchange Online works out of the box; supply your own `clientId` to override it — the card shows which application is in effect. 264 tests.
40
42
  - **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
43
  - **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
44
  - **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
45
  - **0.10.8 and earlier**: see [CHANGELOG.md](CHANGELOG.md).
44
46
  ## Compatibility
45
47
 
48
+ 2026-09-21: the current release package was installed through the official CLI in an isolated profile and co-loaded with the other two most-downloaded plugins on source-built Harness `0.1.6-alpha.2`. All 18 plugin tools registered; calendar/email configuration checks, PPT theme listing and 17-row table generation passed. The host is based on the official alpha.2 release plus the tool-scheduler `Symbol.for` fix (`93badd88`). This run did not connect to live mail or calendar services.
49
+
46
50
  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.
47
51
 
48
52
  **0.11.0 co-load verification (2026-09-18, locally built Harness `0.1.5-rc.2`, `web` profile)**: the plugin mounted without errors; the settings route answered GET with 200 and no `raw` field in the response; a `text/plain` POST was refused with **415**, proving the same-origin guard holds in the real host; under a `application/json` POST the card projection was correct, and an account pinning `authKind: password` reported both `authKindDeclared` and `authKind` as `password`; the panel actually rendered account cards, the eight provider presets with localized labels, the three-way authentication selector, the application (client) ID field with its hint, the missing-ID warning banner (so `--dsw-alias-state-warn-primary` is genuinely defined in the real host) and the Sign in with Microsoft button; the browser console was clean; save worked end to end, and afterwards `accountsYaml` was restored to empty with the original account intact. Offline tests: 231 green. **Still not done**: an end-to-end OAuth2 run against a real Outlook tenant (the device-code flow needs a human to authorize in a browser) and real sending; the `clientId` paths are covered only against a fake authority. Uses the `cordis.patch.yml` + `dsh.bundle.patch` bundle model. Node requirements are 22.19 or later within 22.x, or 24 or later. Live external-service workflows require separate configuration and validation.
@@ -57,7 +61,7 @@ Follows the official [plugin packaging and installation requirements](https://gi
57
61
  dsh plugin --profile web add dsh-email
58
62
  ```
59
63
 
60
- (Or install from GitHub: `dsh plugin --profile web add github:your-account/dsh-email#<commit>`, then follow the prompt to authorize the `prepare` build in the profile's `pnpm-workspace.yaml`.)
64
+ (Or install a specific GitHub commit: `dsh plugin --profile web add github:STARDUSTLC666/dsh-email#<commit>`. The repository includes prebuilt `lib/` artifacts; no `prepare` build is required.)
61
65
 
62
66
  After installing, restart `dsh web`. The plugin ships with an empty config and **won't crash startup**; calling any email tool before configuration returns a clear configuration hint.
63
67
 
@@ -163,7 +167,7 @@ The settings page's "Server presets" fold-out edits these presets visually, and
163
167
  | `maxBodyChars` | `20000` | Body truncation limit for `email_read` (1000–200000) |
164
168
  | `accounts` | — | Named account map; account-level fields override top-level shorthand |
165
169
  | `accountsYaml` | — | YAML text of the account map, written by the settings-page card editor; overrides `accounts` when non-empty |
166
- | `clientId` | — | Application (client) ID for OAuth2 accounts; account-level overrides top-level shorthand. The plugin bundles no third-party registration — required for Outlook / Exchange Online OAuth2 (see "Outlook OAuth2" below) |
170
+ | `clientId` | built-in community app (below) | Application (client) ID for OAuth2 accounts: empty uses the plugin's built-in public client, a value overrides it (account-level also overrides the top-level shorthand) |
167
171
  | `authKind` | derived from provider | Authentication method override: `oauth2` / `password`. When omitted, derived from provider and IMAP host; hybrid or on-premises tenants that still accept app passwords for Exchange Online can pin `password`. The card's "Authentication method" selector writes this key |
168
172
  | `serverPresets` | — | YAML text of custom provider presets (key = preset name, value has optional `label` + `imap`/`smtp`); endpoints only, no credentials. The settings-page dropdown lists preset names and pre-fills endpoints into account cards; editing a preset does not reconnect established connections |
169
173
  | `defaultAccount` | auto (single account) | Account used when the `account` argument is omitted (required for multi-account) |
@@ -193,9 +197,9 @@ Every provider requires an authorization code / app-specific password instead of
193
197
 
194
198
  Microsoft has disabled username+password basic auth for Exchange Online: personal outlook.com accounts and most tenants now require OAuth2. This plugin supports the device-code flow — IMAP and SMTP share a single token with automatic refresh.
195
199
 
196
- **Why no bundled application ID**: a client ID is "someone's app registration." If the plugin shipped one, the Microsoft consent screen would show another party's app name (enterprise security teams typically deny it outright), sign-in logs and telemetry would land in that party's tenant (including your UPN), and if they ever deleted the app every user's login would break simultaneously — with only a cryptic "clientId may be wrong" error. Therefore this plugin **carries no third-party registration**; please register your own (free, ~10 minutes).
200
+ **Where the bundled application ID comes from**: the device-code flow needs an app registration, and making every user register one is a wall nobody should have to climb — so the plugin ships one: `15dcd5aa-00dd-487f-82d7-1d2b2c299e14`, registered by contributor [gurio-wine](https://github.com/gurio-wine) in [PR #13](https://github.com/STARDUSTLC666/dsh-email/pull/13) and used here with their permission — thank you. The cost is stated plainly: the Microsoft consent screen names **their** application (enterprise security teams may refuse it), and the sign-in logs and telemetry land in **their** tenant, including your UPN. If they ever delete the app, every account that did not bring its own id stops signing in at once, with no better error than "clientId may be wrong". **Registering your own (free, ~10 minutes) keeps you independent — paste it into the card to override the built-in value; leaving the field empty keeps using the bundled one.**
197
201
 
198
- **Registering a public client**:
202
+ **Using your own application instead (optional, free, ~10 minutes)**:
199
203
 
200
204
  1. Open the [Entra admin center](https://entra.microsoft.com/) → **App registrations** → **New registration**.
201
205
  2. Under **Supported account types**, select "Accounts in any organizational directory and personal Microsoft accounts" — this determines whether personal outlook.com accounts can sign in. Choosing incorrectly yields `AADSTS700016` or `AADSTS50020`.
@@ -204,19 +208,19 @@ Microsoft has disabled username+password basic auth for Exchange Online: persona
204
208
  5. In the left sidebar, go to **Authentication** → scroll to the bottom → set **Allow public client flows** to **Yes** and save. Without this, login fails with an `AADSTS700028`-style "device-code flow not enabled" error.
205
209
  6. In the left sidebar, go to **API permissions** → Add a permission → Microsoft Graph → **Delegated permissions** → check `IMAP.AccessAsUser.All`, `SMTP.Send`, and `offline_access` (the last one is essential for obtaining a refresh token — without it, every expiry forces a fresh login). Personal tenants generally need no admin consent; enterprise tenants may require an admin to click "Grant admin consent" once.
206
210
 
207
- **Filling it into the plugin**: Settings → Mail (dsh-email) → the account card's "Application (client) ID" field; or in YAML (account-level `clientId`, which can also be set at the top level as a default for all accounts).
211
+ **Filling it into the plugin (overrides the built-in value)**: Settings → Mail (dsh-email) → the account card's "Application (client) ID" field (the card shows which application is in effect; empty keeps the bundled community app); or in YAML (account-level `clientId`, which can also be set at the top level as a default for all accounts).
208
212
 
209
213
  **Login flow**: click "Sign in to Microsoft account" on the card → the panel shows a `microsoft.com/devicelogin` link and a code → open the link in a browser, enter the code, and complete authorization → the panel polls until it shows "Signed in: your@email". Both receiving and sending then use this token.
210
214
 
211
215
  **Caveats**:
212
216
 
213
217
  - Enterprise tenants may additionally require an admin to enable **IMAP** and **SMTP AUTH** for the mailbox in the Exchange admin center. The typical symptom of SMTP AUTH being off: receiving works fine, sending is rejected.
214
- - The token is bound to the application ID that issued it: changing `clientId` is treated as "switched apps" and requires re-login (this is intentional — it prevents using the old app's credentials against the new one).
218
+ - The token is bound to the application ID that issued it: changing `clientId` is treated as "switched apps" and requires re-login (this is intentional — it prevents using the old app's credentials against the new one). The bundled app is shared by every account that names none: a future release that replaces it with the project's own registration will ask those accounts to sign in once more.
215
219
  - If your tenant is hybrid or on-premises and SMTP AUTH is still enabled, app passwords work: select "Password / authorization code" in the card's "Authentication method" selector — no OAuth2 needed.
216
220
 
217
221
  ## Known limitations
218
222
 
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.
223
+ - **OAuth2 covers Outlook / Exchange Online only, and works out of the box (your own app ID optional)**: device-code login supports both IMAP and SMTP and defaults to the bundled community application (see "Outlook OAuth2" above), so no registration is needed to sign in; if your organisation refuses third-party apps, paste your own `clientId` into the card to override it. Other environments that mandate OAuth (e.g. Google Workspace) remain unusable; use the provider's app-specific password / authorization code instead.
220
224
  - **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".
221
225
  - **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`).
222
226
  - **Attachments**: inline images aren't downloadable separately yet; a failed attachment match errors instead of downloading the wrong file (safe default).
package/README.md CHANGED
@@ -45,12 +45,16 @@ IMAP/SMTP email tools for DeepSeek Harness, with replies, forwarding, mailbox or
45
45
 
46
46
  ### 版本记录
47
47
 
48
+ - **0.13.2(2026-09-21)**:同名附件优先按真实 MIME 分段编号下载;旧解析元数据按一对一匹配,避免多个序号都取到第一个同名文件。保留正文分段下载和附件索引缓存,新增文件字节级回归,268 项测试通过。
49
+ - **0.13.1(2026-09-19)**:内置一份社区公共客户端注册(感谢 [gurio-wine](https://github.com/gurio-wine)),Outlook / Exchange Online 的 OAuth2 登录开箱即用;想用自己的应用仍可填 `clientId` 覆盖,设置页会显示当前生效的是哪个应用。测试 264 项。
48
50
  - **0.13.0(2026-09-18)**:修复长正文截断成空、`email_watch` 永久漏报新邮件、附件缓存跨 UIDVALIDITY 失效;10 个工具声明超时;读信/搜索只下正文分段;搜索回退标明扫描口径。测试 262 项。
49
51
  - **0.12.0(2026-09-18)**:新增发送别名(`senderName` / `authUser` / `authPassword`)与 `email_search` 的 `offset` 翻页;修复 QQ 搜索假命中;弹窗轮询按页面可见性节流。
50
52
  - **0.11.0(2026-09-18)**:合入 gurio-wine 的设置页四连(卡片编辑器 / OAuth2 设备码登录 / 双语面板 / `authKind` 钉住),并修掉评审发现的 SMTP OAuth2、设置路由同源校验等问题。
51
53
  - **0.10.8 及更早**:见 [CHANGELOG.md](CHANGELOG.md)。
52
54
  ## 兼容性
53
55
 
56
+ 2026-09-21:当前发布包经官方 CLI 安装到隔离 profile,在源码构建的 Harness `0.1.6-alpha.2` 上与另外两个下载量前三插件共同加载,18 个插件工具注册正常;日历/邮件配置自检、PPT 主题查询和 17 行表格生成通过。测试本体基于官方 alpha.2 发布提交,另含工具调度器 `Symbol.for` 修复(`93badd88`)。本轮未连接真实邮箱或日历服务。
57
+
54
58
  2026-09-16 曾在官方源码构建的 Harness `0.1.5-rc.2` 和 `0.1.6-alpha.1` 上完成同载验证:18 个组件与 ModLens 同载,工具 schema、技能注册及离线只读调用检查通过。
55
59
 
56
60
  **0.11.0 的同载验证(2026-09-18,本地构建的 Harness `0.1.5-rc.2`,`web` profile)**:插件挂载无报错;设置路由 GET 返回 200 且响应中已无 `raw` 字段;用 `text/plain` 发 POST 被 **415** 拒绝(同源守卫在真实宿主下生效);`application/json` 的 POST 下卡片投影正确,账号钉住 `authKind: password` 后 `authKindDeclared` 与 `authKind` 均为 `password`;设置面板实际渲染出账号卡片、8 个服务商预设的中文下拉、「认证方式」三态选择器、「应用(客户端)ID」输入格与提示、未填 ID 时的警示条(说明 `--dsw-alias-state-warn-primary` 在真实宿主下确有定义)与「登录 Microsoft 账号」按钮;浏览器控制台无报错;save 全链路可用,验证结束后已把 `accountsYaml` 还原为空、原有账号恢复。离线测试 231 项全绿。**仍未做**:真实 Outlook 租户的 OAuth2 端到端(设备码流程要真人在浏览器完成授权)与真实发信未测,`clientId` 相关路径目前只有假 authority 的用例覆盖。采用 `cordis.patch.yml` + `dsh.bundle.patch` 组合包模型。Node 要求为 22.19 及以上的 22.x,或 24 及以上。外部服务的实际业务操作需按各组件配置单独验证。
@@ -65,7 +69,7 @@ IMAP/SMTP email tools for DeepSeek Harness, with replies, forwarding, mailbox or
65
69
  dsh plugin --profile web add dsh-email
66
70
  ```
67
71
 
68
- (或从 GitHub 安装:`dsh plugin --profile web add github:你的账号/dsh-email#<commit>`,随后按提示在 profile 的 `pnpm-workspace.yaml` 里授权 `prepare` 构建。)
72
+ (或从 GitHub 安装指定提交:`dsh plugin --profile web add github:STARDUSTLC666/dsh-email#<commit>`。仓库已包含 `lib/` 构建产物,无需 `prepare` 构建。)
69
73
 
70
74
  装好后重启 `dsh web`。插件自带空配置,**不会弄崩启动**;配置前调用任何 email 工具都会返回明确的配置提示。
71
75
 
@@ -171,7 +175,7 @@ dsh plugin --profile web remove dsh-email
171
175
  | `maxBodyChars` | `20000` | email_read 正文截断上限(1000–200000) |
172
176
  | `accounts` | 无 | 具名账号表;账号级字段覆盖顶层简写 |
173
177
  | `accountsYaml` | 无 | 账号映射的 YAML 文本,由设置页的卡片编辑器写入;非空时覆盖 accounts |
174
- | `clientId` | 无 | OAuth2 账号的应用(客户端)ID;账号级可覆盖顶层简写。插件不内置任何第三方注册,Outlook / Exchange Online 走 OAuth2 时必填(见「Outlook OAuth2」) |
178
+ | `clientId` | 内置社区应用(见下) | OAuth2 账号的应用(客户端)ID:留空即用插件内置的公共客户端,填了则覆盖内置值(账号级也可覆盖顶层简写) |
175
179
  | `authKind` | 按 provider 派生 | 认证方式覆盖,取值 `oauth2` / `password`。缺省时按 provider 与 IMAP 主机派生;仍能用应用密码连 Exchange Online 的混合或本地租户可钉 `password`。设置页卡片的「认证方式」选择器即写此键 |
176
180
  | `serverPresets` | 无 | 自定义服务商预设的 YAML 文本(键=预设名,值含 `label?`/`imap`/`smtp`);只存端点、不含凭证,设置页下拉会列出预设名并把端点预填进账号卡片,改预设不会重连已建立的连接 |
177
181
  | `defaultAccount` | 单账号时自动 | 工具省略 account 参数时使用的账号(多账号必填) |
@@ -201,9 +205,9 @@ dsh plugin --profile web remove dsh-email
201
205
 
202
206
  微软已经对 Exchange Online 关闭了用户名+密码的 basic auth:个人 outlook.com 与绝大多数租户现在只能用 OAuth2。本插件支持设备码(device code)流程,IMAP 与 SMTP 双端共用同一份 token,过期自动刷新。
203
207
 
204
- **为什么不内置一个应用 ID**:客户端 ID 是"某个人的应用注册"。如果插件自带一个,微软同意屏上显示的会是别人的应用名(企业安全团队通常直接拒授权),登录日志与 telemetry 会归到对方租户(含你的 UPN),而对方哪天删掉这个应用,所有用户的登录会同时失败——报出来的还只是一句"clientId 可能填错了"。所以本插件**不携带任何第三方注册**,请用自己的(免费,约 10 分钟)。
208
+ **内置的应用 ID 是哪来的**:设备码登录必须先有一个"应用注册",而让每个用户自己注册一次实在太麻烦——所以插件内置了一份:`15dcd5aa-00dd-487f-82d7-1d2b2c299e14`,由贡献者 [gurio-wine](https://github.com/gurio-wine) 在 [PR #13](https://github.com/STARDUSTLC666/dsh-email/pull/13) 注册,并授权本项目内置使用,在此致谢。代价也要说清楚:微软同意屏上显示的是**他的应用名**(企业安全团队可能因此拒绝授权),登录日志与 telemetry 会归到**他的租户**(含你的 UPN);哪天他删掉这个应用,所有没填自己 ID 的账号会同时登不上,报错还只是一句"clientId 可能填错了"。**想完全自主就注册一个自己的(免费,约 10 分钟)填进卡片覆盖内置值;留空则一直用内置的。**
205
209
 
206
- **注册一个公共客户端**:
210
+ **换成自己的应用(可选,免费,约 10 分钟)**:
207
211
 
208
212
  1. 打开 [Entra 管理中心](https://entra.microsoft.com/) → **应用注册(App registrations)** → **新注册**。
209
213
  2. **受支持的账户类型**选「任何组织目录中的账户 **以及** 个人 Microsoft 账户」——这一项决定了个人 outlook.com 能不能登录,选错会报 `AADSTS700016` 或 `AADSTS50020`。
@@ -212,19 +216,19 @@ dsh plugin --profile web remove dsh-email
212
216
  5. 左侧 **身份验证** → 页面最下方 **允许公共客户端流** 设为 **是** 并保存。不开这一项,登录会报 `AADSTS700028` 之类的"未开启设备码流"错误。
213
217
  6. 左侧 **API 权限** → 添加权限 → Microsoft Graph → **委托的权限**,勾上 `IMAP.AccessAsUser.All`、`SMTP.Send`、`offline_access`(最后这个是拿到 refresh token 的关键,少了它每次过期都要重新登录)。个人租户一般无需管理员同意;企业租户可能需要管理员点一次「授予同意」。
214
218
 
215
- **填进插件**:设置页 → 邮件 (dsh-email) → 该账号卡片的「应用(客户端)ID」栏;或者写在 YAML 里(账号级 `clientId`,也可写在顶层作为所有账号的默认)。
219
+ **填进插件(覆盖内置值)**:设置页 → 邮件 (dsh-email) → 该账号卡片的「应用(客户端)ID」栏(卡片会显示当前生效的是哪个应用;留空即继续用内置的社区应用);或者写在 YAML 里(账号级 `clientId`,也可写在顶层作为所有账号的默认)。
216
220
 
217
221
  **登录**:卡片上点「登录 Microsoft 账号」→ 面板给出一个 `microsoft.com/devicelogin` 链接和一段代码 → 在浏览器打开链接、输入代码、完成授权 → 面板轮询到成功后即显示「已登录:你的地址」。之后收信与发信都用这份 token。
218
222
 
219
223
  **注意事项**:
220
224
 
221
225
  - 企业租户可能还需要管理员在 Exchange 管理中心开启该邮箱的 **IMAP** 与 **SMTP AUTH**。没开 SMTP AUTH 时的典型症状是:收信一切正常,发信被拒。
222
- - token 与签发它的应用 ID 绑定:换了 `clientId` 会被判为"换了应用",需要重新登录(这是有意的,避免拿旧应用的凭据去撞新应用)。
226
+ - token 与签发它的应用 ID 绑定:换了 `clientId` 会被判为"换了应用",需要重新登录(这是有意的,避免拿旧应用的凭据去撞新应用)。内置应用是所有没填 `clientId` 的账号共用的一份:将来某个版本把它换成项目自己的注册时,这些账号也会需要重新登录一次。
223
227
  - 如果你的租户是混合或本地部署、SMTP AUTH 仍然开着,用应用密码也能连:在卡片的「认证方式」里选「密码 / 授权码」即可,不必走 OAuth2。
224
228
 
225
229
  ## 已知限制
226
230
 
227
- - **OAuth2 仅覆盖 Outlook / Exchange Online,且需自带应用 ID**:设备码登录已支持 IMAP 与 SMTP 双端,但插件**不内置任何第三方应用注册**,OAuth2 账号必须填自己的 `clientId`(免费注册,见上文「Outlook OAuth2」)。Google Workspace 等其它强制 OAuth 的环境仍不可用,只能用服务商的应用专用密码 / 授权码。
231
+ - **OAuth2 仅覆盖 Outlook / Exchange Online(开箱即用,也可自带应用 ID)**:设备码登录已支持 IMAP 与 SMTP 双端,默认使用插件内置的社区应用(见上文「Outlook OAuth2」),无需自己注册即可登录;企业策略不接受第三方应用时,在卡片里填自己的 `clientId` 覆盖。Google Workspace 等其它强制 OAuth 的环境仍不可用,只能用服务商的应用专用密码 / 授权码。
228
232
  - **搜索的匹配数**:服务器命中会先用信封复核(见上文 `email_search`);复核通过时「共 N 条匹配」沿用服务器给出的条数,而列出的每一行都保证真的带关键词。正文回退扫描只看了最近 `bodySearchLimit` 封,不知道全文件夹匹配数,因此渲染为「本页 N 条(仅扫描最近 N 封)」而不是「共 N 条」。
229
233
  - **正文搜索**:服务器端只搜 subject / from / to / cc;多数服务器(如 QQ)的 IMAP `TEXT` / `HEADER` 搜索不可靠,无结果时回退到最近 `bodySearchLimit` 封的正文扫描(较慢,可用 `bodySearchFallback` 关闭)。
230
234
  - **附件**:内嵌图片暂不支持单独下载;附件定位失败会直接报错而不是下载错误文件(安全默认)。
package/lib/client.js CHANGED
@@ -332,8 +332,9 @@ var UI = {
332
332
  // OAuth2 登录区
333
333
  "oauth.clientIdLabel": "应用(客户端)ID",
334
334
  "oauth.clientIdPlaceholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
335
- "oauth.clientIdHint": "本插件不内置任何第三方应用注册,请用自己注册的 Azure(Entra)公共客户端 ID:免费,需勾选 IMAP/SMTP 委托权限并开启设备码流,步骤见 README 的「Outlook OAuth2」一节。改动它需要重新登录。",
336
- "oauth.clientIdMissing": "还没填应用(客户端)ID,设备码登录无法开始:按下面的提示注册一个,填进来即可。",
335
+ "oauth.clientIdHint": "留空即用插件内置的社区应用(免费、开箱即用):微软同意屏上显示的是贡献者的应用名,登录日志也归到对方租户。想换成自己的 Azure(Entra)公共客户端 ID(需勾选 IMAP/SMTP 委托权限并开启设备码流)就填进来,改动它需要重新登录。",
336
+ "oauth.clientIdMissing": "当前构建没有内置应用(客户端)ID,设备码登录无法开始:按 README 的「Outlook OAuth2」一节注册一个,填进来即可。",
337
+ "oauth.clientIdBuiltIn": "将使用插件内置的社区应用:{id}(贡献者 gurio-wine 注册并授权本项目内置使用)。企业策略不接受第三方应用时,请换成自己的。",
337
338
  "oauth.passwordless": "这个服务商使用 OAuth2 授权,不需要密码{label}。",
338
339
  "oauth.passwordlessLabel": "({label})",
339
340
  "oauth.signIn.outlook": "登录 Microsoft 账号",
@@ -510,8 +511,9 @@ var UI = {
510
511
  // OAuth2 登录区
511
512
  "oauth.clientIdLabel": "Application (client) ID",
512
513
  "oauth.clientIdPlaceholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
513
- "oauth.clientIdHint": "This plugin ships no third-party application registration, so use your own Azure (Entra) public client ID: free, with the IMAP/SMTP delegated permissions and the device-code flow enabled. The README's「Outlook OAuth2」section walks through it. Changing it requires signing in again.",
514
- "oauth.clientIdMissing": "No application (client) ID yet, so the device-code login cannot start: register one as described below and paste it here.",
514
+ "oauth.clientIdHint": "Leave this empty to log in with the plugin's built-in community application (free, nothing to set up): the Microsoft consent screen names the contributor's app and the sign-in logs land in their tenant. Paste your own Azure (Entra) public client ID here (with the IMAP/SMTP delegated permissions and the device-code flow enabled) to use that instead; changing it requires signing in again.",
515
+ "oauth.clientIdMissing": "This build carries no built-in application (client) ID, so the device-code login cannot start: register one as described in the README's「Outlook OAuth2」section and paste it here.",
516
+ "oauth.clientIdBuiltIn": "Signing in through the plugin's built-in community application: {id} (registered by contributor gurio-wine, used with permission). Use your own application if third-party apps are not allowed.",
515
517
  "oauth.passwordless": "This provider authorizes over OAuth2, so no password is needed{label}.",
516
518
  "oauth.passwordlessLabel": " ({label})",
517
519
  "oauth.signIn.outlook": "Sign in with Microsoft",
@@ -764,6 +766,11 @@ function draftFromCard(card) {
764
766
  senderName: card.senderName === undefined || card.senderName === null ? "" : String(card.senderName),
765
767
  authUser: card.authUser === undefined || card.authUser === null ? "" : String(card.authUser),
766
768
  clientId: card.clientId === undefined || card.clientId === null ? "" : String(card.clientId),
769
+ // 卡片只在账号自己的 clientId 为空时报内置值:它是「留空会用哪个应用」的答案,
770
+ // 不是账号的字段,所以既不进 YAML,也不参与三态写入。
771
+ defaultClientId: card.oauthDefaultClientId === undefined || card.oauthDefaultClientId === null
772
+ ? ""
773
+ : String(card.oauthDefaultClientId),
767
774
  // authKind 是生效裁决(决定显示登录区还是密码框),authKindSetting 是用户钉住
768
775
  // 了什么('' = 自动)。两者必须分开:都从裁决读的话,「自动」与「显式密码」在
769
776
  // 编辑器里长得一样,那条给混合/本地租户留的退路就没法从面板操作。
@@ -1180,9 +1187,11 @@ function OauthLoginPanel(props) {
1180
1187
  const disabled = props.disabled === true;
1181
1188
  const label = props.providerLabel || draft.provider || "OAuth2";
1182
1189
 
1183
- // 插件不内置任何第三方应用注册,所以「用哪个应用登录」是账号自己的配置项,
1184
- // 面板是用户唯一的常规入口。它不是机密(公共客户端 ID 每次都随请求发出),
1185
- // 因此可以直接回填、直接编辑。
1190
+ // 「用哪个应用登录」是账号自己的配置项,面板是用户唯一的常规入口。它不是机密
1191
+ // (公共客户端 ID 每次都随请求发出),因此可以直接回填、直接编辑。留空不是
1192
+ // 「没配置」:插件自带一份社区注册,所以这里必须说清生效的到底是哪个应用 ——
1193
+ // 同意屏上写的名字不是账号主人自己的,这件事不该等点了登录才发现。
1194
+ const builtInClientId = String(draft.defaultClientId || "").trim();
1186
1195
  const clientIdField = () => h("div", { className: "dshe-field dshe-oauth-client" }, [
1187
1196
  h("label", null, t("oauth.clientIdLabel")),
1188
1197
  h("input", {
@@ -1190,10 +1199,13 @@ function OauthLoginPanel(props) {
1190
1199
  value: String(draft.clientId || ""),
1191
1200
  disabled: disabled,
1192
1201
  spellcheck: "false",
1193
- placeholder: t("oauth.clientIdPlaceholder"),
1202
+ placeholder: builtInClientId !== "" ? builtInClientId : t("oauth.clientIdPlaceholder"),
1194
1203
  onChange: (e) => { if (props.onClientId) props.onClientId(e.target.value); },
1195
1204
  }),
1196
1205
  h("div", { className: "dshe-hint" }, t("oauth.clientIdHint")),
1206
+ String(draft.clientId || "").trim() === "" && builtInClientId !== ""
1207
+ ? h("div", { className: "dshe-hint dshe-oauth-builtin" }, t("oauth.clientIdBuiltIn", { id: builtInClientId }))
1208
+ : null,
1197
1209
  ]);
1198
1210
 
1199
1211
  if (state === "pending") {
@@ -1263,7 +1275,7 @@ function OauthLoginPanel(props) {
1263
1275
  h("div", { className: "dshe-oauth-head" },
1264
1276
  t("oauth.passwordless", { label: label ? t("oauth.passwordlessLabel", { label }) : "" })),
1265
1277
  clientIdField(),
1266
- String(draft.clientId || "").trim() === ""
1278
+ String(draft.clientId || "").trim() === "" && builtInClientId === ""
1267
1279
  ? h("div", { className: "dshe-alert warn" }, t("oauth.clientIdMissing"))
1268
1280
  : null,
1269
1281
  h("div", { className: "dshe-oauth-actions" }, [
package/lib/config.d.ts CHANGED
@@ -18,23 +18,27 @@ export declare const OUTLOOK_PROVIDER = "outlook";
18
18
  /** The Exchange Online IMAP host. Any account dialling it is an OAuth2 account. */
19
19
  export declare const OUTLOOK_IMAP_HOST = "outlook.office365.com";
20
20
  /**
21
- * The built-in default client id for the device-code flow: deliberately empty.
21
+ * The built-in client id for the device-code flow: a community registration.
22
22
  *
23
- * An earlier revision shipped a registration belonging to a contributor. That
24
- * cannot be right for a package thousands of strangers install: the Microsoft
25
- * consent screen would name someone else's application (which enterprise
26
- * security teams refuse), the sign-in logs and telemetry would land in their
27
- * tenant along with the user's UPN, and their deleting the app would break
28
- * every login at once — with an error that only says the client id「可能填错了」,
29
- * so no user could diagnose it.
23
+ * Requiring every Outlook user to register an application of their own puts a
24
+ * setup wall in front of the one provider where OAuth2 cannot be avoided, so
25
+ * the plugin ships one. The id below is the public-client registration
26
+ * contributed by gurio-wine (PR #13) and used with their permission; the README
27
+ * credits them and states the two things that follow from using somebody
28
+ * else's application: the consent screen names *their* app, and the sign-in
29
+ * logs land in *their* tenant along with the user's UPN.
30
30
  *
31
- * Nothing third-party is baked in, so an account supplies its own `clientId`
32
- * (a free Entra public-client registration; the README walks through it) and
33
- * `startDeviceFlow` refuses with an actionable message until one is set. A
34
- * maintainer who registers an application for this project restores the
35
- * out-of-box experience by filling in this one constant.
31
+ * An account that wants neither — an enterprise that refuses third-party apps,
32
+ * or a day when this registration is gone — sets its own `clientId` (a free
33
+ * Entra public-client registration) and that value wins over this constant.
34
+ * The settings card shows which application is in effect either way, so nobody
35
+ * consents to an app they were not told about.
36
+ *
37
+ * Replacing this constant is the whole change a maintainer makes to ship the
38
+ * project's own application instead. Tokens are bound to the id that issued
39
+ * them, so such a release asks every built-in account to log in one more time.
36
40
  */
37
- export declare const OUTLOOK_OAUTH2_CLIENT_ID = "";
41
+ export declare const OUTLOOK_OAUTH2_CLIENT_ID = "15dcd5aa-00dd-487f-82d7-1d2b2c299e14";
38
42
  /**
39
43
  * How an account proves who it is. `password` covers every existing provider
40
44
  * (an app password / 授权码) and is the default, so nothing about them changes.
package/lib/config.js CHANGED
@@ -13,23 +13,27 @@ export const OUTLOOK_PROVIDER = 'outlook';
13
13
  /** The Exchange Online IMAP host. Any account dialling it is an OAuth2 account. */
14
14
  export const OUTLOOK_IMAP_HOST = 'outlook.office365.com';
15
15
  /**
16
- * The built-in default client id for the device-code flow: deliberately empty.
16
+ * The built-in client id for the device-code flow: a community registration.
17
17
  *
18
- * An earlier revision shipped a registration belonging to a contributor. That
19
- * cannot be right for a package thousands of strangers install: the Microsoft
20
- * consent screen would name someone else's application (which enterprise
21
- * security teams refuse), the sign-in logs and telemetry would land in their
22
- * tenant along with the user's UPN, and their deleting the app would break
23
- * every login at once — with an error that only says the client id「可能填错了」,
24
- * so no user could diagnose it.
18
+ * Requiring every Outlook user to register an application of their own puts a
19
+ * setup wall in front of the one provider where OAuth2 cannot be avoided, so
20
+ * the plugin ships one. The id below is the public-client registration
21
+ * contributed by gurio-wine (PR #13) and used with their permission; the README
22
+ * credits them and states the two things that follow from using somebody
23
+ * else's application: the consent screen names *their* app, and the sign-in
24
+ * logs land in *their* tenant along with the user's UPN.
25
25
  *
26
- * Nothing third-party is baked in, so an account supplies its own `clientId`
27
- * (a free Entra public-client registration; the README walks through it) and
28
- * `startDeviceFlow` refuses with an actionable message until one is set. A
29
- * maintainer who registers an application for this project restores the
30
- * out-of-box experience by filling in this one constant.
26
+ * An account that wants neither — an enterprise that refuses third-party apps,
27
+ * or a day when this registration is gone — sets its own `clientId` (a free
28
+ * Entra public-client registration) and that value wins over this constant.
29
+ * The settings card shows which application is in effect either way, so nobody
30
+ * consents to an app they were not told about.
31
+ *
32
+ * Replacing this constant is the whole change a maintainer makes to ship the
33
+ * project's own application instead. Tokens are bound to the id that issued
34
+ * them, so such a release asks every built-in account to log in one more time.
31
35
  */
32
- export const OUTLOOK_OAUTH2_CLIENT_ID = '';
36
+ export const OUTLOOK_OAUTH2_CLIENT_ID = '15dcd5aa-00dd-487f-82d7-1d2b2c299e14';
33
37
  export const PROVIDER_PRESETS = {
34
38
  qq: { imap: { host: 'imap.qq.com', port: 993, secure: true }, smtp: { host: 'smtp.qq.com', port: 465, secure: true } },
35
39
  '163': { imap: { host: 'imap.163.com', port: 993, secure: true }, smtp: { host: 'smtp.163.com', port: 465, secure: true } },
package/lib/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export type Config = EmailConfig;
6
6
  export declare function apply(ctx: any, config?: Config): void;
7
7
  export { clampInt, defaultDownloadDir, EMAIL_PASSWORD_ENV, isOAuth2Account, OUTLOOK_OAUTH2_CLIENT_ID, OUTLOOK_PROVIDER, parseAccountsYaml, parseServerPresets, presetNamesIn, providerNames, PROVIDER_NAMES, resolveEmailConfig, resolveEmailSettings, serializeAccountsYaml } from './config.js';
8
8
  export type { AuthKind, ResolvedEmailConfig } from './config.js';
9
- export { ACCESS_TOKEN_MARGIN_MS, classifyOAuthFailure, clearTokenFor, DEVICE_CODE_URL, getFreshAccessToken, mapAadstsMessage, NO_CLIENT_ID_MESSAGE, NOT_LOGGED_IN_MESSAGE, oauth2StateOf, oauth2TokenFile, OAUTH2_SCOPES, OAuth2Error, pollDeviceFlow, readTokenStore, startDeviceFlow, TOKEN_URL, writeTokenStore, } from './oauth2.js';
9
+ export { ACCESS_TOKEN_MARGIN_MS, classifyOAuthFailure, clearTokenFor, clientIdOf, DEVICE_CODE_URL, getFreshAccessToken, mapAadstsMessage, NO_CLIENT_ID_MESSAGE, NOT_LOGGED_IN_MESSAGE, oauth2StateOf, oauth2TokenFile, OAUTH2_SCOPES, OAuth2Error, pollDeviceFlow, readTokenStore, startDeviceFlow, TOKEN_URL, writeTokenStore, } from './oauth2.js';
10
10
  export type { DeviceFlowStart, OAuth2PollResult, OAuth2State, OAuth2TokenEntry, OAuth2TokenStore } from './oauth2.js';
11
11
  export { buildReplyMessage, EmailPool, extractMessageIds, imapAuthOf, looksLikeAuthFailure, MailError, messageMatchesQuery, messageOf, OAUTH2_RELOGIN_MESSAGE, redactCredentials, selectAttachmentPart, smtpAuthOf, validateAttachmentPaths } from './mail-client.js';
12
12
  export { flattenAddresses, parseRawMessage, sanitizeFilename, stripHtml, truncateText } from './parse.js';
package/lib/index.js CHANGED
@@ -15,7 +15,7 @@ export function apply(ctx, config = {}) {
15
15
  installSendApproval(ctx, runtime);
16
16
  }
17
17
  export { clampInt, defaultDownloadDir, EMAIL_PASSWORD_ENV, isOAuth2Account, OUTLOOK_OAUTH2_CLIENT_ID, OUTLOOK_PROVIDER, parseAccountsYaml, parseServerPresets, presetNamesIn, providerNames, PROVIDER_NAMES, resolveEmailConfig, resolveEmailSettings, serializeAccountsYaml } from './config.js';
18
- export { ACCESS_TOKEN_MARGIN_MS, classifyOAuthFailure, clearTokenFor, DEVICE_CODE_URL, getFreshAccessToken, mapAadstsMessage, NO_CLIENT_ID_MESSAGE, NOT_LOGGED_IN_MESSAGE, oauth2StateOf, oauth2TokenFile, OAUTH2_SCOPES, OAuth2Error, pollDeviceFlow, readTokenStore, startDeviceFlow, TOKEN_URL, writeTokenStore, } from './oauth2.js';
18
+ export { ACCESS_TOKEN_MARGIN_MS, classifyOAuthFailure, clearTokenFor, clientIdOf, DEVICE_CODE_URL, getFreshAccessToken, mapAadstsMessage, NO_CLIENT_ID_MESSAGE, NOT_LOGGED_IN_MESSAGE, oauth2StateOf, oauth2TokenFile, OAUTH2_SCOPES, OAuth2Error, pollDeviceFlow, readTokenStore, startDeviceFlow, TOKEN_URL, writeTokenStore, } from './oauth2.js';
19
19
  export { buildReplyMessage, EmailPool, extractMessageIds, imapAuthOf, looksLikeAuthFailure, MailError, messageMatchesQuery, messageOf, OAUTH2_RELOGIN_MESSAGE, redactCredentials, selectAttachmentPart, smtpAuthOf, validateAttachmentPaths } from './mail-client.js';
20
20
  export { flattenAddresses, parseRawMessage, sanitizeFilename, stripHtml, truncateText } from './parse.js';
21
21
  export { EmailSettingsSchema, SETTINGS_NAMESPACE, toEmailConfig, toSettingsBase, validateSettingsValue } from './settings.js';
@@ -69,9 +69,9 @@ interface AttachmentPart {
69
69
  }
70
70
  /**
71
71
  * Map the index in the mailparser attachment list (what email_read showed the
72
- * model) onto a bodyStructure part. Name first, then type + tolerant size;
73
- * an inline image that our walk excludes simply fails instead of downloading
74
- * the wrong part.
72
+ * model) onto a bodyStructure part. Real IMAP section ids are authoritative.
73
+ * Legacy mailparser indexes need one-to-one name/type/size matching so duplicate
74
+ * filenames cannot repeatedly select the first attachment.
75
75
  */
76
76
  export declare function selectAttachmentPart(readAttachments: EmailAttachmentMeta[], parts: AttachmentPart[], index: number): AttachmentPart | undefined;
77
77
  /** Case-insensitive match of a query against subject/from/body text. */
@@ -103,20 +103,38 @@ function collectAttachmentParts(node, out = []) {
103
103
  }
104
104
  /**
105
105
  * Map the index in the mailparser attachment list (what email_read showed the
106
- * model) onto a bodyStructure part. Name first, then type + tolerant size;
107
- * an inline image that our walk excludes simply fails instead of downloading
108
- * the wrong part.
106
+ * model) onto a bodyStructure part. Real IMAP section ids are authoritative.
107
+ * Legacy mailparser indexes need one-to-one name/type/size matching so duplicate
108
+ * filenames cannot repeatedly select the first attachment.
109
109
  */
110
110
  export function selectAttachmentPart(readAttachments, parts, index) {
111
111
  const meta = readAttachments[index];
112
112
  if (meta === undefined)
113
113
  return undefined;
114
- const byName = parts.find(part => part.filename === meta.filename || sanitizeFilename(part.filename) === meta.filename);
115
- if (byName !== undefined)
116
- return byName;
117
- const tolerance = Math.max(64, Math.ceil(meta.size * 0.5));
118
- const byTypeAndSize = parts.find(part => part.contentType === meta.contentType && Math.abs(part.size - meta.size) <= tolerance);
119
- return byTypeAndSize;
114
+ const hasSectionId = (item) => /^\d+(?:\.\d+)*$/.test(item.part);
115
+ if (hasSectionId(meta))
116
+ return parts.find(part => part.part === meta.part);
117
+ const used = new Set();
118
+ // Reserve exact identities even if mixed with older parsed metadata.
119
+ for (const item of readAttachments)
120
+ if (hasSectionId(item))
121
+ used.add(item.part);
122
+ for (let position = 0; position <= index; position++) {
123
+ const item = readAttachments[position];
124
+ if (hasSectionId(item))
125
+ continue;
126
+ const available = parts.filter(part => !used.has(part.part));
127
+ const sameName = (part) => part.filename === item.filename || sanitizeFilename(part.filename) === item.filename;
128
+ const tolerance = Math.max(64, Math.ceil(item.size * 0.5));
129
+ const sameTypeAndSize = (part) => part.contentType === item.contentType && Math.abs(part.size - item.size) <= tolerance;
130
+ const match = available.find(part => sameName(part) && sameTypeAndSize(part))
131
+ ?? available.find(sameName) ?? available.find(sameTypeAndSize);
132
+ if (position === index)
133
+ return match;
134
+ if (match !== undefined)
135
+ used.add(match.part);
136
+ }
137
+ return undefined;
120
138
  }
121
139
  /**
122
140
  * Leaf text/* parts that can carry the message body, attachment parts excluded.
package/lib/oauth2.d.ts CHANGED
@@ -17,11 +17,12 @@ export declare const OAUTH2_REQUEST_TIMEOUT_MS = 15000;
17
17
  /** The one message every「no token yet」path reports, so the fix is always the same. */
18
18
  export declare const NOT_LOGGED_IN_MESSAGE = "\u5C1A\u672A\u767B\u5F55\uFF1A\u8BF7\u5148\u5728\u8BBE\u7F6E\u9875\u5B8C\u6210\u8BBE\u5907\u7801\u767B\u5F55";
19
19
  /**
20
- * Reported when an OAuth2 account has no application to log in through. The
21
- * plugin ships no third-party registration, so this is a setup step, not a
22
- * failure — and it says where to go and what to type.
20
+ * Reported when an OAuth2 account has nothing to log in through. The packaged
21
+ * build carries a community registration (OUTLOOK_OAUTH2_CLIENT_ID), so this
22
+ * only surfaces in a build that blanks it, or on an account whose own id was
23
+ * cleared while the built-in one is gone — it still says what to type.
23
24
  */
24
- export declare const NO_CLIENT_ID_MESSAGE = "\u5C1A\u672A\u914D\u7F6E OAuth2 \u5E94\u7528\uFF1A\u8BF7\u5728\u8BBE\u7F6E\u9875\u8BE5\u8D26\u53F7\u7684\u300C\u5E94\u7528\uFF08\u5BA2\u6237\u7AEF\uFF09ID\u300D\u91CC\u586B\u5165\u4F60\u81EA\u5DF1\u6CE8\u518C\u7684 Azure \u516C\u5171\u5BA2\u6237\u7AEF ID\uFF08\u514D\u8D39\uFF0C\u6CE8\u518C\u6B65\u9AA4\u89C1 README \u7684\u300COutlook OAuth2\u300D\u4E00\u8282\uFF09\u3002\u672C\u63D2\u4EF6\u4E0D\u5185\u7F6E\u4EFB\u4F55\u7B2C\u4E09\u65B9\u5E94\u7528\u6CE8\u518C\uFF0C\u56E0\u6B64\u6CA1\u6709\u5B83\u5C31\u65E0\u6CD5\u5F00\u59CB\u8BBE\u5907\u7801\u767B\u5F55";
25
+ export declare const NO_CLIENT_ID_MESSAGE = "\u5C1A\u672A\u914D\u7F6E OAuth2 \u5E94\u7528\uFF1A\u5F53\u524D\u6784\u5EFA\u6CA1\u6709\u5185\u7F6E\u516C\u5171\u5BA2\u6237\u7AEF ID\uFF0C\u8BF7\u5728\u8BBE\u7F6E\u9875\u8BE5\u8D26\u53F7\u7684\u300C\u5E94\u7528\uFF08\u5BA2\u6237\u7AEF\uFF09ID\u300D\u91CC\u586B\u5165\u4E00\u4E2A\uFF08\u514D\u8D39\u6CE8\u518C\uFF0C\u6B65\u9AA4\u89C1 README \u7684\u300COutlook OAuth2\u300D\u4E00\u8282\uFF09\uFF0C\u5426\u5219\u65E0\u6CD5\u5F00\u59CB\u8BBE\u5907\u7801\u767B\u5F55";
25
26
  /** Where the refresh/access tokens live. Kept out of the settings namespace on purpose. */
26
27
  export declare function oauth2TokenFile(): string;
27
28
  export interface OAuth2TokenEntry {
package/lib/oauth2.js CHANGED
@@ -44,11 +44,12 @@ export const OAUTH2_REQUEST_TIMEOUT_MS = 15000;
44
44
  /** The one message every「no token yet」path reports, so the fix is always the same. */
45
45
  export const NOT_LOGGED_IN_MESSAGE = '尚未登录:请先在设置页完成设备码登录';
46
46
  /**
47
- * Reported when an OAuth2 account has no application to log in through. The
48
- * plugin ships no third-party registration, so this is a setup step, not a
49
- * failure — and it says where to go and what to type.
47
+ * Reported when an OAuth2 account has nothing to log in through. The packaged
48
+ * build carries a community registration (OUTLOOK_OAUTH2_CLIENT_ID), so this
49
+ * only surfaces in a build that blanks it, or on an account whose own id was
50
+ * cleared while the built-in one is gone — it still says what to type.
50
51
  */
51
- export const NO_CLIENT_ID_MESSAGE = '尚未配置 OAuth2 应用:请在设置页该账号的「应用(客户端)ID」里填入你自己注册的 Azure 公共客户端 ID(免费,注册步骤见 README 的「Outlook OAuth2」一节)。本插件不内置任何第三方应用注册,因此没有它就无法开始设备码登录';
52
+ export const NO_CLIENT_ID_MESSAGE = '尚未配置 OAuth2 应用:当前构建没有内置公共客户端 ID,请在设置页该账号的「应用(客户端)ID」里填入一个(免费注册,步骤见 README 的「Outlook OAuth2」一节),否则无法开始设备码登录';
52
53
  /** Where the refresh/access tokens live. Kept out of the settings namespace on purpose. */
53
54
  export function oauth2TokenFile() {
54
55
  const home = process.env.DSH_HOME ?? join(homedir(), '.dsh');
package/lib/web.d.ts CHANGED
@@ -36,10 +36,18 @@ export interface AccountCardData {
36
36
  * The application (client) id this account logs in through. Unlike a password
37
37
  * this is not a secret — a public-client id travels in every device-code
38
38
  * request — so the card carries the value itself and the editor can prefill
39
- * it. Omitted when the account has none, which for an OAuth2 account is
40
- * exactly the state that has to be fixed before login can start.
39
+ * it. Omitted when the account names none, which means the built-in
40
+ * registration below is the application that will be used.
41
41
  */
42
42
  clientId?: string;
43
+ /**
44
+ * The application the login falls back to when the account names none: the
45
+ * community registration the plugin ships. Carried on the card so the editor
46
+ * can show which app the consent screen is about to name, instead of leaving
47
+ * the user to guess — and so「no id of its own」stops looking like an error
48
+ * that has to be fixed before a login can even start.
49
+ */
50
+ oauthDefaultClientId?: string;
43
51
  /** Display name for the From header, when the account sets one. */
44
52
  senderName?: string;
45
53
  /** Login user when it differs from the visible address (`user`). */
@@ -79,8 +87,8 @@ export interface AccountCardInput {
79
87
  password?: string | number | boolean;
80
88
  /**
81
89
  * OAuth2 应用(客户端)ID,三态契约与 password 相同:undefined = 本卡片没提供
82
- * (保留 YAML 里已存的 clientId 键),'' = 明确清除,非空 = 写入。插件不内置任何
83
- * 第三方应用注册,所以这是 OAuth2 账号的必填项,而设置面板是用户唯一的常规入口。
90
+ * (保留 YAML 里已存的 clientId 键),'' = 明确清除(回到内置的社区应用),非空 =
91
+ * 写入。留空不等于不能用:内置注册就是默认值,这一栏是把默认值换成自己的应用。
84
92
  */
85
93
  clientId?: string;
86
94
  /**
package/lib/web.js CHANGED
@@ -3,7 +3,7 @@ import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { isMap, parseDocument } from 'yaml';
5
5
  import { SETTINGS_NAMESPACE, toEmailConfig, validateSettingsValue } from './settings.js';
6
- import { isOAuth2Account, parseAccountsYaml, parseServerPresets, presetNamesIn, PROVIDER_PRESETS, resolveEmailSettings, serializeAccountsYaml, } from './config.js';
6
+ import { isOAuth2Account, OUTLOOK_OAUTH2_CLIENT_ID, parseAccountsYaml, parseServerPresets, presetNamesIn, PROVIDER_PRESETS, resolveEmailSettings, serializeAccountsYaml, } from './config.js';
7
7
  import { clearTokenFor, getFreshAccessToken, oauth2StateOf, pollDeviceFlow, startDeviceFlow, } from './oauth2.js';
8
8
  import { EmailPool, messageOf, redactCredentials } from './mail-client.js';
9
9
  /** Same-origin route the browser settings section talks to. */
@@ -84,9 +84,10 @@ function buildAccountCards(raw, defaultAccount, presets, tokens = () => ({ state
84
84
  : isOAuth2Account(providerName, imap.host) ? 'oauth2' : 'password';
85
85
  const user = typeof account.user === 'string' ? account.user : '';
86
86
  // A public-client id is not a secret, so unlike the password it is handed
87
- // back for the editor to prefill: an OAuth2 account without one cannot
88
- // start a device-code login, and the card is where that gets fixed.
87
+ // back for the editor to prefill: an account that names no application logs
88
+ // in with the built-in one, and the card has to say which that is.
89
89
  const clientId = typeof account.clientId === 'string' ? account.clientId.trim() : '';
90
+ const oauthDefaultClientId = authKind === 'oauth2' && clientId === '' ? OUTLOOK_OAUTH2_CLIENT_ID : '';
90
91
  // The display name and the login user are not secrets, so — like clientId —
91
92
  // the card hands them back for the editor to prefill. A separate login
92
93
  // password is a secret and only ever reported as a boolean.
@@ -102,6 +103,7 @@ function buildAccountCards(raw, defaultAccount, presets, tokens = () => ({ state
102
103
  authKind,
103
104
  ...(pinned === 'oauth2' || pinned === 'password' ? { authKindDeclared: pinned } : {}),
104
105
  ...(clientId !== '' ? { clientId } : {}),
106
+ ...(oauthDefaultClientId !== '' ? { oauthDefaultClientId } : {}),
105
107
  ...(senderName !== '' ? { senderName } : {}),
106
108
  ...(authUser !== '' ? { authUser } : {}),
107
109
  ...(typeof account.authPassword === 'string' && account.authPassword !== '' ? { hasAuthPassword: true } : {}),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-email",
3
- "version": "0.13.0",
4
- "description": "DSH 邮件插件:IMAP/SMTP 收发搜索、回复转发、附件与整理,多账号卡片设置页 + Outlook OAuth2 + 发信审批。",
3
+ "version": "0.13.2",
4
+ "description": "DSH 邮件插件:IMAP/SMTP 收发搜索、回复转发、附件与整理,多账号卡片设置页、Outlook OAuth2 登录与发信审批。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",