dsh-email 0.13.1 → 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,7 @@
4
4
 
5
5
  ## 中文版
6
6
 
7
+ - **0.13.2(2026-09-21)**:同名附件优先按真实 MIME 分段编号下载;旧解析元数据按一对一匹配,避免多个序号都取到第一个同名文件。保留正文分段下载和附件索引缓存,新增文件字节级回归,268 项测试通过。
7
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 项。
8
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 项。
9
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 之后的代码重写)。
@@ -23,6 +24,7 @@
23
24
 
24
25
  ## English
25
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.
26
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.
27
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).
28
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,6 +37,7 @@ 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.
40
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.
41
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.
42
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.
@@ -44,6 +45,8 @@ Example:
44
45
  - **0.10.8 and earlier**: see [CHANGELOG.md](CHANGELOG.md).
45
46
  ## Compatibility
46
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
+
47
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.
48
51
 
49
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.
@@ -58,7 +61,7 @@ Follows the official [plugin packaging and installation requirements](https://gi
58
61
  dsh plugin --profile web add dsh-email
59
62
  ```
60
63
 
61
- (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.)
62
65
 
63
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.
64
67
 
package/README.md CHANGED
@@ -45,6 +45,7 @@ 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 项测试通过。
48
49
  - **0.13.1(2026-09-19)**:内置一份社区公共客户端注册(感谢 [gurio-wine](https://github.com/gurio-wine)),Outlook / Exchange Online 的 OAuth2 登录开箱即用;想用自己的应用仍可填 `clientId` 覆盖,设置页会显示当前生效的是哪个应用。测试 264 项。
49
50
  - **0.13.0(2026-09-18)**:修复长正文截断成空、`email_watch` 永久漏报新邮件、附件缓存跨 UIDVALIDITY 失效;10 个工具声明超时;读信/搜索只下正文分段;搜索回退标明扫描口径。测试 262 项。
50
51
  - **0.12.0(2026-09-18)**:新增发送别名(`senderName` / `authUser` / `authPassword`)与 `email_search` 的 `offset` 翻页;修复 QQ 搜索假命中;弹窗轮询按页面可见性节流。
@@ -52,6 +53,8 @@ IMAP/SMTP email tools for DeepSeek Harness, with replies, forwarding, mailbox or
52
53
  - **0.10.8 及更早**:见 [CHANGELOG.md](CHANGELOG.md)。
53
54
  ## 兼容性
54
55
 
56
+ 2026-09-21:当前发布包经官方 CLI 安装到隔离 profile,在源码构建的 Harness `0.1.6-alpha.2` 上与另外两个下载量前三插件共同加载,18 个插件工具注册正常;日历/邮件配置自检、PPT 主题查询和 17 行表格生成通过。测试本体基于官方 alpha.2 发布提交,另含工具调度器 `Symbol.for` 修复(`93badd88`)。本轮未连接真实邮箱或日历服务。
57
+
55
58
  2026-09-16 曾在官方源码构建的 Harness `0.1.5-rc.2` 和 `0.1.6-alpha.1` 上完成同载验证:18 个组件与 ModLens 同载,工具 schema、技能注册及离线只读调用检查通过。
56
59
 
57
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 及以上。外部服务的实际业务操作需按各组件配置单独验证。
@@ -66,7 +69,7 @@ IMAP/SMTP email tools for DeepSeek Harness, with replies, forwarding, mailbox or
66
69
  dsh plugin --profile web add dsh-email
67
70
  ```
68
71
 
69
- (或从 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` 构建。)
70
73
 
71
74
  装好后重启 `dsh web`。插件自带空配置,**不会弄崩启动**;配置前调用任何 email 工具都会返回明确的配置提示。
72
75
 
@@ -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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-email",
3
- "version": "0.13.1",
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",