dsh-email 0.13.2 → 0.14.1
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 +4 -0
- package/README.en.md +12 -2
- package/README.md +12 -2
- package/lib/client.js +50 -28
- package/lib/config.d.ts +3 -1
- package/lib/config.js +16 -5
- package/lib/host-config.d.ts +6 -0
- package/lib/host-config.js +30 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +11 -0
- package/lib/legacy-settings.d.ts +4 -0
- package/lib/legacy-settings.js +64 -0
- package/lib/runtime.d.ts +10 -1
- package/lib/runtime.js +32 -4
- package/lib/settings.d.ts +57 -58
- package/lib/web.d.ts +2 -0
- package/lib/web.js +22 -8
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
package/README.en.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# dsh-email
|
|
2
2
|
|
|
3
|
+
## 0.14.1 update (2026-09-27)
|
|
4
|
+
|
|
5
|
+
Incomplete account and server-preset drafts display an unsaved status and are protected against accidental navigation. Connection tests validate the selected account independently and identify the relevant settings field.
|
|
6
|
+
|
|
7
|
+
Validation host: Harness 0.1.7-rc.2 built from official sources, retaining the local tool-scheduler fix. Build and automated checks pass; interactive coverage and external-service limits are recorded in this release round.
|
|
8
|
+
|
|
3
9
|
   
|
|
4
10
|
|
|
5
11
|
[](https://awesome-dsh-plugin.com)
|
|
@@ -37,6 +43,8 @@ Example:
|
|
|
37
43
|
|
|
38
44
|
### Changelog
|
|
39
45
|
|
|
46
|
+
- **0.14.0 (2026-09-23)**: supports Harness 0.1.7 settings. Existing mailbox settings migrate automatically and appear as editable cards; edits auto-save and survive refresh and restart. Advanced options and saved passwords are preserved; removing all cards no longer resurrects a hidden legacy account.
|
|
47
|
+
|
|
40
48
|
- **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
49
|
- **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.
|
|
42
50
|
- **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.
|
|
@@ -45,6 +53,8 @@ Example:
|
|
|
45
53
|
- **0.10.8 and earlier**: see [CHANGELOG.md](CHANGELOG.md).
|
|
46
54
|
## Compatibility
|
|
47
55
|
|
|
56
|
+
The current baseline is official-source Harness **0.1.7-alpha.2** (2026-09-23, with a local `Symbol.for` tool-scheduler fix). All 18 plugins load together. Legacy migration, editing and automatic saving in the real Web UI, refresh, revision conflicts and restart persistence have been checked with isolated fixture accounts. No live mailbox connection or sending was exercised in this round.
|
|
57
|
+
|
|
48
58
|
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
59
|
|
|
50
60
|
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.
|
|
@@ -53,7 +63,7 @@ Co-load verification was performed on 2026-09-16 with official source builds of
|
|
|
53
63
|
|
|
54
64
|
On 2026-09-10, npm `dsh-email@0.10.6` passed real QQ mailbox folder/list/read/search calls, the settings page's connection test and Save & Apply, and separate SMTP authentication. An empty authorization-code field correctly used `DSH_EMAIL_PASSWORD`. This recheck did not connect to a real mailbox or send, modify or delete mail.
|
|
55
65
|
|
|
56
|
-
Follows the official [plugin packaging and installation requirements](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md): an ESM entry point, prebuilt `lib/`, `dsh.bundle.patch` and a `cordis.patch.yml` layer. The plugin explicitly injects its required services and supplies JSON Schema parameters, canonical output and rendering,
|
|
66
|
+
Follows the official [plugin packaging and installation requirements](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md): an ESM entry point, prebuilt `lib/`, `dsh.bundle.patch` and a `cordis.patch.yml` layer. The plugin explicitly injects its required services and supplies JSON Schema parameters, canonical output and rendering, without importing host-internal services; configuration uses the public `@deepseek-ai/schemastery` package. Use Node 22.19 or later within 22.x, or Node 24 or later. Harness is evolving rapidly; the version above is the tested baseline.
|
|
57
67
|
|
|
58
68
|
## Installation
|
|
59
69
|
|
|
@@ -74,7 +84,7 @@ The whole settings page follows DSH's light and dark themes: every panel style r
|
|
|
74
84
|
|
|
75
85
|
Multiple accounts can be edited visually in the settings page: account cards add, edit and delete accounts, rename them, pick the default, and run "Test connection" per account name; a half-filled account never blocks saving — it just gets an "incomplete" badge. Card edits are debounced and auto-saved; there is no longer a "write to YAML text, then click save" step. Version conflicts (settings changed elsewhere) are automatically rebased and re-saved once, rather than repeatedly failing with a stale revision. When a card is saved, an already-stored authorization code is kept by default (leave the password field empty to keep it, type into it to overwrite); comments in the YAML are preserved in place where possible — with an explicit notice when they cannot be. Rename re-keys in place, preserving auth codes and advanced keys, and refuses to overwrite an existing account name. Account-level hand-written imap/smtp endpoints are only cleaned when the **provider actually changes** — runtime resolution prefers the account's own host, so a routine save never silently re-points the connection target.
|
|
76
86
|
|
|
77
|
-
|
|
87
|
+
On Harness 0.1.7, the page saves directly to the current profile’s `tool-email` entry and applies changes live. Installations using the default entry automatically import the retired `dsh-email` section from `settings.yaml` or `settings.yaml.imported` once. Existing profile values win, and the original file is preserved. Older hosts retain their original settings storage. Authorization-code fields are marked secret, but saving a filled field still writes its value to the local settings file. For a single account, set `DSH_EMAIL_PASSWORD` and leave the authorization-code field empty to avoid saving it; the environment value is not copied into settings.
|
|
78
88
|
|
|
79
89
|
## Uninstall
|
|
80
90
|
|
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# dsh-email
|
|
4
4
|
|
|
5
|
+
## 0.14.1 更新(2026-09-27)
|
|
6
|
+
|
|
7
|
+
未补全的账号和服务器预设会显示未保存提示;避免草稿丢失和错误的“已保存”状态。测试连接只校验所选账号,错误直接指向设置页中的对应字段。
|
|
8
|
+
|
|
9
|
+
验证宿主:官方源码构建的 Harness 0.1.7-rc.2(保留本地工具调度器修复)。构建与自动测试通过;实际操作和外部服务限制见本轮验收记录。
|
|
10
|
+
|
|
5
11
|
> **让 agent 协助处理邮件**:收发、搜索、回复转发、附件、邮件整理与新邮件提醒,支持八种常见邮箱服务预设。
|
|
6
12
|
|
|
7
13
|
   
|
|
@@ -45,6 +51,8 @@ IMAP/SMTP email tools for DeepSeek Harness, with replies, forwarding, mailbox or
|
|
|
45
51
|
|
|
46
52
|
### 版本记录
|
|
47
53
|
|
|
54
|
+
- **0.14.0(2026-09-23)**:适配 Harness 0.1.7 设置接口;自动导入旧版邮箱设置,旧账号直接显示为可编辑卡片。编辑即时保存,刷新后保留;保留高级参数与已有密码,清空账号后不再重新出现。
|
|
55
|
+
|
|
48
56
|
- **0.13.2(2026-09-21)**:同名附件优先按真实 MIME 分段编号下载;旧解析元数据按一对一匹配,避免多个序号都取到第一个同名文件。保留正文分段下载和附件索引缓存,新增文件字节级回归,268 项测试通过。
|
|
49
57
|
- **0.13.1(2026-09-19)**:内置一份社区公共客户端注册(感谢 [gurio-wine](https://github.com/gurio-wine)),Outlook / Exchange Online 的 OAuth2 登录开箱即用;想用自己的应用仍可填 `clientId` 覆盖,设置页会显示当前生效的是哪个应用。测试 264 项。
|
|
50
58
|
- **0.13.0(2026-09-18)**:修复长正文截断成空、`email_watch` 永久漏报新邮件、附件缓存跨 UIDVALIDITY 失效;10 个工具声明超时;读信/搜索只下正文分段;搜索回退标明扫描口径。测试 262 项。
|
|
@@ -53,6 +61,8 @@ IMAP/SMTP email tools for DeepSeek Harness, with replies, forwarding, mailbox or
|
|
|
53
61
|
- **0.10.8 及更早**:见 [CHANGELOG.md](CHANGELOG.md)。
|
|
54
62
|
## 兼容性
|
|
55
63
|
|
|
64
|
+
当前验证基线为官方源码构建的 Harness **0.1.7-alpha.2**(2026-09-23,含本地工具调度器 `Symbol.for` 修复)。18 个插件共同加载;邮件设置已通过旧配置迁移、真实网页编辑与自动保存、刷新恢复、修订冲突和重启持久化检查。本轮使用隔离测试账号,未连接真实邮箱或发送邮件。
|
|
65
|
+
|
|
56
66
|
2026-09-21:当前发布包经官方 CLI 安装到隔离 profile,在源码构建的 Harness `0.1.6-alpha.2` 上与另外两个下载量前三插件共同加载,18 个插件工具注册正常;日历/邮件配置自检、PPT 主题查询和 17 行表格生成通过。测试本体基于官方 alpha.2 发布提交,另含工具调度器 `Symbol.for` 修复(`93badd88`)。本轮未连接真实邮箱或日历服务。
|
|
57
67
|
|
|
58
68
|
2026-09-16 曾在官方源码构建的 Harness `0.1.5-rc.2` 和 `0.1.6-alpha.1` 上完成同载验证:18 个组件与 ModLens 同载,工具 schema、技能注册及离线只读调用检查通过。
|
|
@@ -61,7 +71,7 @@ IMAP/SMTP email tools for DeepSeek Harness, with replies, forwarding, mailbox or
|
|
|
61
71
|
|
|
62
72
|
2026-09-10,npm `dsh-email@0.10.6` 曾通过真实 QQ 邮箱目录、列表、读取和搜索,以及设置页“测试连接”“保存并应用”检查;授权码留空时能继续使用 `DSH_EMAIL_PASSWORD`。独立 SMTP 登录认证也已通过。此次复验未连接真实邮箱,未发送、修改或删除邮件。
|
|
63
73
|
|
|
64
|
-
遵循官方[插件打包与安装要求](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md):ESM 入口、预构建 `lib/`、`dsh.bundle.patch` 和 `cordis.patch.yml` 配置层;显式注入所需服务,提供 JSON Schema
|
|
74
|
+
遵循官方[插件打包与安装要求](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md):ESM 入口、预构建 `lib/`、`dsh.bundle.patch` 和 `cordis.patch.yml` 配置层;显式注入所需服务,提供 JSON Schema 参数、规范化输出和渲染函数,运行时不导入宿主内部服务;配置声明使用官方 `@deepseek-ai/schemastery` 公共包。使用 Node 22.19 及以上的 22.x 或 Node 24 及以上版本;Harness 仍在快速迭代,上述版本是实测基线。
|
|
65
75
|
|
|
66
76
|
## 安装
|
|
67
77
|
|
|
@@ -82,7 +92,7 @@ dsh plugin --profile web add dsh-email
|
|
|
82
92
|
|
|
83
93
|
多账号可以在设置页可视化编辑:账号卡片支持增删改账号、改名、设默认、按账号名单独「测试连接」;没填完的账号不阻断保存,只标一个「未完成」。卡片改动即时防抖落盘,不再有"先写 YAML 文本、再点一次保存"这一步;版本冲突(别处也改了设置)会自动重基后重存一次,而不是拿旧版本号反复失败。保存卡片时,已存的授权码默认保持(密码栏留空 = 不变,填内容 = 覆盖);YAML 里的注释尽量原地保留,实在保不住时会明确提示。改名走的是原地改键,授权码与高级键一并保留,且不允许改成已有账号名(那会顶掉另一个账号)。账号自己手写的 imap/smtp 端点只在**服务商真的换了**时才清洗——运行时以账号自己的 host 优先,普通保存不会悄悄改动连接目标。
|
|
84
94
|
|
|
85
|
-
|
|
95
|
+
Harness 0.1.7 中,设置页直接保存到当前 profile 的 `tool-email` 配置行,修改立即生效。使用默认插件行的旧安装会自动导入 `settings.yaml` 或 `settings.yaml.imported` 中的 `dsh-email` 设置;已有 profile 配置优先,原文件保留且只迁移一次,无需重新输入账号。旧版宿主仍使用原 settings 存储。授权码字段标记为 secret,但填写后保存仍会写入本机配置文件。单账号如需避免保存授权码,可设置 `DSH_EMAIL_PASSWORD` 并将授权码栏留空;环境变量不会被复制进设置文件。
|
|
86
96
|
|
|
87
97
|
## 卸载
|
|
88
98
|
|
package/lib/client.js
CHANGED
|
@@ -10,7 +10,8 @@ const React = require("react");
|
|
|
10
10
|
const { useState, useEffect, useCallback, useRef } = React;
|
|
11
11
|
const h = React.createElement;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// Harness supplies document.baseURI for reverse-proxy mounts and in-page navigation.
|
|
14
|
+
const ROUTE = "_dsh/dsh-email/settings";
|
|
14
15
|
/** 「编辑即保存」的防抖窗口:连续打字只落一次盘。 */
|
|
15
16
|
const SAVE_DEBOUNCE_MS = 800;
|
|
16
17
|
|
|
@@ -207,6 +208,7 @@ const CSS = [
|
|
|
207
208
|
".dshe-auto-status{font-size:12px;font-weight:600;align-self:center;color:var(--dsw-alias-label-tertiary,#77736d)}",
|
|
208
209
|
".dshe-auto-status.is-saved{color:var(--dsw-alias-state-success-primary,#267d52)}",
|
|
209
210
|
".dshe-auto-status.is-error{color:var(--dsw-alias-state-error-primary,#aa3939)}",
|
|
211
|
+
".dshe-auto-status.is-draft{color:var(--dsw-alias-state-warning-primary,#986000)}",
|
|
210
212
|
".dshe-acc-badge.is-oauth{border-color:transparent;background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#267d52) 12%,transparent);color:var(--dsw-alias-state-success-primary,#267d52)}",
|
|
211
213
|
".dshe-oauth{display:grid;gap:8px;padding:10px 11px;border:1px dashed var(--dsw-alias-border-l2,#dedbd5);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#fff)}",
|
|
212
214
|
".dshe-oauth.is-logged-in{border-style:solid;border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#267d52) 34%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#267d52) 8%,transparent)}",
|
|
@@ -245,8 +247,8 @@ var UI = {
|
|
|
245
247
|
// 导航 / 段标题
|
|
246
248
|
"nav.title": "邮件 (dsh-email)",
|
|
247
249
|
"section.title": "邮件",
|
|
248
|
-
"section.intro": "
|
|
249
|
-
"section.readonly": "
|
|
250
|
+
"section.intro": "添加邮箱后,就可以让助手帮你查收、搜索和回复邮件。选择服务商并填写邮箱地址与授权码,修改会自动保存。",
|
|
251
|
+
"section.readonly": "当前配置为只读,可以查看,但无法保存修改。",
|
|
250
252
|
"section.activeAccounts": "当前生效账号:{accounts}",
|
|
251
253
|
|
|
252
254
|
// 内置服务商名(PROVIDERS 里存的就是这些 key)
|
|
@@ -259,18 +261,19 @@ var UI = {
|
|
|
259
261
|
"provider.outlook": "Outlook",
|
|
260
262
|
"provider.icloud": "iCloud",
|
|
261
263
|
"provider.custom": "自定义",
|
|
262
|
-
"provider.noneInYaml": "
|
|
264
|
+
"provider.noneInYaml": "请选择邮箱服务商",
|
|
263
265
|
"provider.undefinedPresetSuffix": "(未定义预设)",
|
|
264
266
|
|
|
265
267
|
// 全局设置
|
|
266
268
|
"global.sendApproval": "发信前弹确认(强烈建议保留;Full Access 模式下会被自动拒绝)",
|
|
267
|
-
"global.downloadDirLabel": "
|
|
269
|
+
"global.downloadDirLabel": "附件下载目录(默认保存在当前工作区)",
|
|
268
270
|
"global.downloadDirPlaceholder": "留空使用默认",
|
|
269
271
|
"global.hint": "以上两项对所有账号统一生效。",
|
|
270
272
|
|
|
271
273
|
// 自动保存状态条
|
|
272
274
|
"status.saved": "已保存",
|
|
273
275
|
"status.saving": "保存中…",
|
|
276
|
+
"status.draft": "还有未保存的内容,请补全下方标记的字段。",
|
|
274
277
|
"status.saveFailed": "保存失败:{message}(点击重试)",
|
|
275
278
|
|
|
276
279
|
// 账号卡片
|
|
@@ -299,9 +302,9 @@ var UI = {
|
|
|
299
302
|
"card.errReservedName": "账号名不能是 defaultAccount(该键保留给默认账号),这份改动先不保存。",
|
|
300
303
|
"card.errNoDefault": "有多个账号还没指定默认账号,这份改动先不保存。",
|
|
301
304
|
"card.yamlParseFailed": "accountsYaml 解析失败:{error}",
|
|
302
|
-
"card.confirmDelete": "删除账号「{name}
|
|
303
|
-
"card.nameLabel": "
|
|
304
|
-
"card.nameHint": "
|
|
305
|
+
"card.confirmDelete": "删除账号「{name}」会同时移除已保存的登录信息,不会删除邮箱里的邮件。",
|
|
306
|
+
"card.nameLabel": "账号名称",
|
|
307
|
+
"card.nameHint": "用来区分多个邮箱,例如「工作邮箱」。修改名称会保留原有设置;名称不能为 defaultAccount。",
|
|
305
308
|
"card.reservedNameWarn": "defaultAccount 是保留键(表示默认账号),请换一个名字。",
|
|
306
309
|
"card.providerLabel": "服务商",
|
|
307
310
|
"card.addressLabel": "邮箱地址",
|
|
@@ -313,7 +316,7 @@ var UI = {
|
|
|
313
316
|
"card.passwordLabel": "授权码 / 应用专用密码",
|
|
314
317
|
"card.passwordPlaceholder": "留空保持不变",
|
|
315
318
|
"card.passwordSaved": "已存有授权码:留空保持不变,清空后填内容即覆盖。",
|
|
316
|
-
"card.passwordHint": "
|
|
319
|
+
"card.passwordHint": "请填写邮箱服务商提供的授权码或应用专用密码,然后点击「测试连接」。",
|
|
317
320
|
"card.senderNameLabel": "发件显示名(可选)",
|
|
318
321
|
"card.senderNamePlaceholder": "只影响收件人看到的名称",
|
|
319
322
|
"card.senderNameHint": "只改收件人看到的显示名,发件地址仍是上面的邮箱地址。",
|
|
@@ -424,7 +427,7 @@ var UI = {
|
|
|
424
427
|
// 导航 / 段标题
|
|
425
428
|
"nav.title": "Email (dsh-email)",
|
|
426
429
|
"section.title": "Email",
|
|
427
|
-
"section.intro": "
|
|
430
|
+
"section.intro": "Add a mailbox so your assistant can read, search and reply to mail. Choose a provider and enter your email address and app password. Changes save automatically.",
|
|
428
431
|
"section.readonly": "The settings store is read-only right now: you can view the settings but not save them.",
|
|
429
432
|
"section.activeAccounts": "Active accounts: {accounts}",
|
|
430
433
|
|
|
@@ -438,18 +441,19 @@ var UI = {
|
|
|
438
441
|
"provider.outlook": "Outlook",
|
|
439
442
|
"provider.icloud": "iCloud",
|
|
440
443
|
"provider.custom": "Custom",
|
|
441
|
-
"provider.noneInYaml": "
|
|
444
|
+
"provider.noneInYaml": "Choose your email provider",
|
|
442
445
|
"provider.undefinedPresetSuffix": "(undefined preset)",
|
|
443
446
|
|
|
444
447
|
// 全局设置
|
|
445
448
|
"global.sendApproval": "Ask for confirmation before sending (strongly recommended; auto-denied under Full Access)",
|
|
446
|
-
"global.downloadDirLabel": "Attachment download directory (defaults to
|
|
449
|
+
"global.downloadDirLabel": "Attachment download directory (defaults to the current workspace)",
|
|
447
450
|
"global.downloadDirPlaceholder": "Leave empty for the default",
|
|
448
451
|
"global.hint": "These two settings apply to every account.",
|
|
449
452
|
|
|
450
453
|
// 自动保存状态条
|
|
451
454
|
"status.saved": "Saved",
|
|
452
455
|
"status.saving": "Saving…",
|
|
456
|
+
"status.draft": "Some edits are not saved yet. Complete the highlighted fields below.",
|
|
453
457
|
"status.saveFailed": "Save failed: {message} (click to retry)",
|
|
454
458
|
|
|
455
459
|
// 账号卡片
|
|
@@ -478,9 +482,9 @@ var UI = {
|
|
|
478
482
|
"card.errReservedName": "An account cannot be named defaultAccount (that key is reserved for the default account); this change will not be saved.",
|
|
479
483
|
"card.errNoDefault": "Several accounts exist but no default account was chosen; this change will not be saved.",
|
|
480
484
|
"card.yamlParseFailed": "accountsYaml failed to parse: {error}",
|
|
481
|
-
"card.confirmDelete": "Deleting account “{name}” removes
|
|
482
|
-
"card.nameLabel": "Account name
|
|
483
|
-
"card.nameHint": "
|
|
485
|
+
"card.confirmDelete": "Deleting account “{name}” also removes its saved login details. Messages in the mailbox are not deleted.",
|
|
486
|
+
"card.nameLabel": "Account name",
|
|
487
|
+
"card.nameHint": "Use a name such as “Work” to distinguish your mailboxes. Renaming keeps the existing settings; defaultAccount is reserved.",
|
|
484
488
|
"card.reservedNameWarn": "defaultAccount is a reserved key (it means the default account); pick another name.",
|
|
485
489
|
"card.providerLabel": "Provider",
|
|
486
490
|
"card.addressLabel": "Email address",
|
|
@@ -492,7 +496,7 @@ var UI = {
|
|
|
492
496
|
"card.passwordLabel": "Authorization code / app password",
|
|
493
497
|
"card.passwordPlaceholder": "Leave empty to keep it",
|
|
494
498
|
"card.passwordSaved": "A password is already stored: leave empty to keep it, or clear the field and type to overwrite.",
|
|
495
|
-
"card.passwordHint": "
|
|
499
|
+
"card.passwordHint": "Enter the authorization code or app password from your email provider, then click “Test connection”.",
|
|
496
500
|
"card.senderNameLabel": "Sender display name (optional)",
|
|
497
501
|
"card.senderNamePlaceholder": "Only the name recipients see",
|
|
498
502
|
"card.senderNameHint": "Changes only the name recipients see; the address stays the one above.",
|
|
@@ -1349,33 +1353,38 @@ function AccountCardsEditor(props) {
|
|
|
1349
1353
|
*/
|
|
1350
1354
|
const autoSave = (draftsNow, orderNow, defaultNow, immediate) => {
|
|
1351
1355
|
if (typeof props.onAutoSave !== "function") return null;
|
|
1356
|
+
const incomplete = (text) => {
|
|
1357
|
+
setNotice({ kind: "error", text });
|
|
1358
|
+
if (typeof props.onDraftStatus === "function") props.onDraftStatus(true);
|
|
1359
|
+
};
|
|
1352
1360
|
const names = orderNow.map((row) => row.key).filter((name) => Object.prototype.hasOwnProperty.call(draftsNow, name));
|
|
1353
1361
|
let built;
|
|
1354
1362
|
try {
|
|
1355
1363
|
built = names.map((name) => draftToInput(draftsNow[name]));
|
|
1356
1364
|
} catch (e) {
|
|
1357
|
-
|
|
1365
|
+
incomplete(e && e.message ? e.message : String(e));
|
|
1358
1366
|
return null;
|
|
1359
1367
|
}
|
|
1360
1368
|
if (built.some((card) => String(card.name || "").trim() === "")) {
|
|
1361
|
-
|
|
1369
|
+
incomplete(t("card.errNoName"));
|
|
1362
1370
|
return null;
|
|
1363
1371
|
}
|
|
1364
1372
|
if (names.some((name) => !draftsNow[name].provider && !(draftsNow[name].imap?.host && draftsNow[name].smtp?.host))) {
|
|
1365
1373
|
// 服务商就是账号的连接身份:没有它,后端写回时会把 provider 键删掉,这个账号
|
|
1366
1374
|
// 永远连不上。名字和它都属于「这还是一张半填的卡」,一起拦在这里。
|
|
1367
|
-
|
|
1375
|
+
incomplete(t("card.errNoProvider"));
|
|
1368
1376
|
return null;
|
|
1369
1377
|
}
|
|
1370
1378
|
if (built.some((card) => card.name === "defaultAccount")) {
|
|
1371
|
-
|
|
1379
|
+
incomplete(t("card.errReservedName"));
|
|
1372
1380
|
return null;
|
|
1373
1381
|
}
|
|
1374
1382
|
if (built.length > 1 && String(defaultNow || "") === "") {
|
|
1375
|
-
|
|
1383
|
+
incomplete(t("card.errNoDefault"));
|
|
1376
1384
|
return null;
|
|
1377
1385
|
}
|
|
1378
1386
|
setNotice(null);
|
|
1387
|
+
if (typeof props.onDraftStatus === "function") props.onDraftStatus(false);
|
|
1379
1388
|
// 返回值只给 OAuth 登录用:它必须等这份配置真的落盘(后端按账号名去读 provider
|
|
1380
1389
|
// 和端点)。其余调用方照旧忽略返回值。
|
|
1381
1390
|
return props.onAutoSave(built, String(defaultNow || ""), immediate === true);
|
|
@@ -1888,9 +1897,11 @@ function ServerPresetsEditor(props) {
|
|
|
1888
1897
|
const result = serializePresetDrafts(nextOrder.map((row) => row.key), nextDrafts);
|
|
1889
1898
|
if (result.error !== undefined) {
|
|
1890
1899
|
setNotice({ kind: "error", text: result.error });
|
|
1900
|
+
if (typeof props.onDraftStatus === "function") props.onDraftStatus(true);
|
|
1891
1901
|
return;
|
|
1892
1902
|
}
|
|
1893
1903
|
setNotice(null);
|
|
1904
|
+
if (typeof props.onDraftStatus === "function") props.onDraftStatus(false);
|
|
1894
1905
|
props.onAutoSave(result.text);
|
|
1895
1906
|
};
|
|
1896
1907
|
|
|
@@ -2108,6 +2119,8 @@ function EmailSettingsSection() {
|
|
|
2108
2119
|
const [error, setError] = useState("");
|
|
2109
2120
|
/** 自动保存的可见状态:null 视同「已保存」(刚加载完、什么都没改)。 */
|
|
2110
2121
|
const [saveState, setSaveState] = useState(null);
|
|
2122
|
+
const [incompleteDrafts, setIncompleteDrafts] = useState({ accounts: false, presets: false });
|
|
2123
|
+
const hasIncompleteDraft = incompleteDrafts.accounts || incompleteDrafts.presets;
|
|
2111
2124
|
/** 卡片级提示(注释丢失、序列化失败…):走卡片自己的 notice 位,不打断顶部状态。 */
|
|
2112
2125
|
const [cardsNotice, setCardsNotice] = useState(null);
|
|
2113
2126
|
/**
|
|
@@ -2162,6 +2175,7 @@ function EmailSettingsSection() {
|
|
|
2162
2175
|
accountsYamlRef.current = next.accountsYaml || "";
|
|
2163
2176
|
savedSignatureRef.current = signatureOf(next, next.accountsYaml || "");
|
|
2164
2177
|
setSaveState(null);
|
|
2178
|
+
setIncompleteDrafts({ accounts: false, presets: false });
|
|
2165
2179
|
setCardsNotice(null);
|
|
2166
2180
|
setCustomNames(presetNamesOf(next.serverPresets));
|
|
2167
2181
|
setCardsDetail(snap.accountsDetail);
|
|
@@ -2183,6 +2197,7 @@ function EmailSettingsSection() {
|
|
|
2183
2197
|
* 防抖那条路交不出 promise(定时器还没到点),返回 null。
|
|
2184
2198
|
*/
|
|
2185
2199
|
const scheduleSave = (immediate) => {
|
|
2200
|
+
setSaveState({ kind: "busy" });
|
|
2186
2201
|
// 用户又改了:还在飞的那次保存结果就作废,别拿旧结果盖住新状态。
|
|
2187
2202
|
seqRef.current += 1;
|
|
2188
2203
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
@@ -2269,14 +2284,14 @@ function EmailSettingsSection() {
|
|
|
2269
2284
|
// 有没落盘的内容(保存中或保存失败)才拦离开:这正是「编辑即保存」剩下的窗口。
|
|
2270
2285
|
useEffect(() => {
|
|
2271
2286
|
const guard = (e) => {
|
|
2272
|
-
if (saveState === null || saveState.kind === "saved") return undefined;
|
|
2287
|
+
if (!hasIncompleteDraft && (saveState === null || saveState.kind === "saved")) return undefined;
|
|
2273
2288
|
e.preventDefault();
|
|
2274
2289
|
e.returnValue = "";
|
|
2275
2290
|
return "";
|
|
2276
2291
|
};
|
|
2277
2292
|
window.addEventListener("beforeunload", guard);
|
|
2278
2293
|
return () => window.removeEventListener("beforeunload", guard);
|
|
2279
|
-
}, [saveState]);
|
|
2294
|
+
}, [saveState, hasIncompleteDraft]);
|
|
2280
2295
|
|
|
2281
2296
|
/** 卡片序列化的往返序号:打字快时多个 serializeAccounts 会并发,只认最后一个。 */
|
|
2282
2297
|
const cardsSeqRef = useRef(0);
|
|
@@ -2311,6 +2326,7 @@ function EmailSettingsSection() {
|
|
|
2311
2326
|
*/
|
|
2312
2327
|
const onCardsAutoSave = async (cards, defaultAccount, immediate) => {
|
|
2313
2328
|
const seq = ++cardsSeqRef.current;
|
|
2329
|
+
setSaveState({ kind: "busy" });
|
|
2314
2330
|
try {
|
|
2315
2331
|
const result = await serializeCards(cards, defaultAccount);
|
|
2316
2332
|
if (seq !== cardsSeqRef.current) return { ok: false, reason: "superseded" }; // 更晚的一次已经在路上,别用旧结果盖新状态
|
|
@@ -2332,8 +2348,10 @@ function EmailSettingsSection() {
|
|
|
2332
2348
|
if (flushed && typeof flushed.then === "function") await flushed;
|
|
2333
2349
|
return { ok: true };
|
|
2334
2350
|
} catch (e) {
|
|
2351
|
+
if (seq !== cardsSeqRef.current) return { ok: false, reason: "superseded" };
|
|
2335
2352
|
const message = e && e.message ? e.message : String(e);
|
|
2336
2353
|
setCardsNotice({ kind: "error", text: t("notice.cardsNotWritten", { message }) });
|
|
2354
|
+
setSaveState({ kind: "error", text: message });
|
|
2337
2355
|
return { ok: false, reason: message };
|
|
2338
2356
|
}
|
|
2339
2357
|
};
|
|
@@ -2359,7 +2377,7 @@ function EmailSettingsSection() {
|
|
|
2359
2377
|
|
|
2360
2378
|
const accounts = snapshot && snapshot.accounts ? snapshot.accounts : [];
|
|
2361
2379
|
const presetsMerged = presetsWithNames((snapshot && snapshot.presets) || undefined, customNames);
|
|
2362
|
-
const statusText = saveState === null
|
|
2380
|
+
const statusText = hasIncompleteDraft ? t("status.draft") : saveState === null
|
|
2363
2381
|
? t("status.saved")
|
|
2364
2382
|
: saveState.kind === "busy"
|
|
2365
2383
|
? t("status.saving")
|
|
@@ -2367,9 +2385,11 @@ function EmailSettingsSection() {
|
|
|
2367
2385
|
? t("status.saved")
|
|
2368
2386
|
: t("status.saveFailed", { message: saveState.text });
|
|
2369
2387
|
const status = h("span", {
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2388
|
+
role: "status",
|
|
2389
|
+
"aria-live": "polite",
|
|
2390
|
+
className: "dshe-auto-status" + (hasIncompleteDraft ? " is-draft" : saveState === null ? "" : " is-" + saveState.kind),
|
|
2391
|
+
onClick: !hasIncompleteDraft && saveState !== null && saveState.kind === "error" ? () => flushSave() : undefined,
|
|
2392
|
+
style: !hasIncompleteDraft && saveState !== null && saveState.kind === "error" ? { cursor: "pointer" } : undefined,
|
|
2373
2393
|
}, statusText);
|
|
2374
2394
|
|
|
2375
2395
|
return h("div", { className: "dshe-settings" }, [
|
|
@@ -2402,6 +2422,7 @@ function EmailSettingsSection() {
|
|
|
2402
2422
|
presets: presetsMerged,
|
|
2403
2423
|
extraNotice: cardsNotice,
|
|
2404
2424
|
onAutoSave: onCardsAutoSave,
|
|
2425
|
+
onDraftStatus: (blocked) => setIncompleteDrafts((current) => ({ ...current, accounts: blocked })),
|
|
2405
2426
|
onTest: onCardTest,
|
|
2406
2427
|
// OAuth2 登录成功后重拉整份快照:卡片的 authKind/oauthState/oauthUser 是
|
|
2407
2428
|
// 服务端算出来的,只有新快照才拿得到(和加载走同一条路径)。
|
|
@@ -2412,6 +2433,7 @@ function EmailSettingsSection() {
|
|
|
2412
2433
|
h(ServerPresetsEditor, {
|
|
2413
2434
|
presets: presetsAtLoad,
|
|
2414
2435
|
onAutoSave: (text) => update({ serverPresets: typeof text === "string" ? text : "" }),
|
|
2436
|
+
onDraftStatus: (blocked) => setIncompleteDrafts((current) => ({ ...current, presets: blocked })),
|
|
2415
2437
|
}),
|
|
2416
2438
|
]),
|
|
2417
2439
|
snapshot && snapshot.writable === false
|
|
@@ -2513,7 +2535,7 @@ function startWhaleWidget() {
|
|
|
2513
2535
|
const snap = await api();
|
|
2514
2536
|
lastSnapshot = Date.now();
|
|
2515
2537
|
if (snap && snap.whale) {
|
|
2516
|
-
whaleUrl = snap.whale.url || "";
|
|
2538
|
+
whaleUrl = (snap.whale.url || "").replace(/^\/_dsh\//, "_dsh/");
|
|
2517
2539
|
whaleCredit = snap.whale.credit || "";
|
|
2518
2540
|
}
|
|
2519
2541
|
hasAccounts = !!(snap && snap.accounts && snap.accounts.length > 0);
|
package/lib/config.d.ts
CHANGED
|
@@ -256,8 +256,10 @@ export declare function serializeAccountsYaml(raw: unknown, defaultAccount?: str
|
|
|
256
256
|
* Resolve and validate the raw row config. Throws with an actionable message
|
|
257
257
|
* (in Chinese, since it is what the user and the model both read) when the
|
|
258
258
|
* account is not fully specified.
|
|
259
|
+
* An explicit account resolves only that card for connection tests, without
|
|
260
|
+
* making named accounts eligible for the single-account environment password.
|
|
259
261
|
*/
|
|
260
|
-
export declare function resolveEmailSettings(config: EmailConfig | undefined): ResolvedEmailSettings;
|
|
262
|
+
export declare function resolveEmailSettings(config: EmailConfig | undefined, onlyAccount?: string): ResolvedEmailSettings;
|
|
261
263
|
/** Every name a `provider:` may legally use, built-ins first. */
|
|
262
264
|
export declare function providerNames(custom?: Record<string, ServerPreset>): string[];
|
|
263
265
|
/**
|
package/lib/config.js
CHANGED
|
@@ -201,8 +201,10 @@ function normalizeAccountForYaml(value) {
|
|
|
201
201
|
* Resolve and validate the raw row config. Throws with an actionable message
|
|
202
202
|
* (in Chinese, since it is what the user and the model both read) when the
|
|
203
203
|
* account is not fully specified.
|
|
204
|
+
* An explicit account resolves only that card for connection tests, without
|
|
205
|
+
* making named accounts eligible for the single-account environment password.
|
|
204
206
|
*/
|
|
205
|
-
export function resolveEmailSettings(config) {
|
|
207
|
+
export function resolveEmailSettings(config, onlyAccount) {
|
|
206
208
|
const raw = config ?? {};
|
|
207
209
|
const common = {
|
|
208
210
|
provider: raw.provider,
|
|
@@ -218,6 +220,10 @@ export function resolveEmailSettings(config) {
|
|
|
218
220
|
const entries = parsedYaml !== undefined
|
|
219
221
|
? parsedYaml.map
|
|
220
222
|
: (raw.accounts === undefined || Object.keys(raw.accounts).length === 0 ? undefined : raw.accounts);
|
|
223
|
+
const available = entries === undefined ? ['default'] : Object.keys(entries);
|
|
224
|
+
if (onlyAccount !== undefined && !available.includes(onlyAccount)) {
|
|
225
|
+
throw new Error(`未知账号 "${onlyAccount}",可用:${available.join('、')}`);
|
|
226
|
+
}
|
|
221
227
|
// The custom preset table is read once, here, and never stored on the result:
|
|
222
228
|
// it is a lookup source for provider names, not part of the resolved config.
|
|
223
229
|
// A broken preset text degrades to "no custom presets" — the account-level
|
|
@@ -237,11 +243,16 @@ export function resolveEmailSettings(config) {
|
|
|
237
243
|
}
|
|
238
244
|
else {
|
|
239
245
|
for (const [name, acc] of Object.entries(entries)) {
|
|
246
|
+
if (onlyAccount !== undefined && name !== onlyAccount)
|
|
247
|
+
continue;
|
|
240
248
|
accounts.set(name, resolveAccount(name, common, acc ?? {}, false, providers, known));
|
|
241
249
|
}
|
|
242
250
|
}
|
|
243
251
|
let defaultName;
|
|
244
|
-
if (
|
|
252
|
+
if (onlyAccount !== undefined) {
|
|
253
|
+
defaultName = onlyAccount;
|
|
254
|
+
}
|
|
255
|
+
else if (raw.defaultAccount !== undefined && raw.defaultAccount !== '') {
|
|
245
256
|
if (!accounts.has(raw.defaultAccount)) {
|
|
246
257
|
throw new Error(`dsh-email:defaultAccount "${raw.defaultAccount}" 不存在,可用账号:${[...accounts.keys()].join('、')}`);
|
|
247
258
|
}
|
|
@@ -343,7 +354,7 @@ function resolveAccount(name, common, acc, allowEnvPassword, providers, known) {
|
|
|
343
354
|
};
|
|
344
355
|
const problems = [];
|
|
345
356
|
if (user === '')
|
|
346
|
-
problems.push(`账号 "${name}"
|
|
357
|
+
problems.push(`账号 "${name}" 的邮箱地址未填写`);
|
|
347
358
|
// An explicit `authKind` outranks the derivation: a tenant that still accepts
|
|
348
359
|
// an app password for Exchange Online, or a mailbox that worked before the
|
|
349
360
|
// derivation existed, keeps working instead of being told「尚未登录」.
|
|
@@ -358,14 +369,14 @@ function resolveAccount(name, common, acc, allowEnvPassword, providers, known) {
|
|
|
358
369
|
// in the OAuth2 store, and requiring a password would demand a secret
|
|
359
370
|
// Microsoft no longer accepts for Exchange Online.
|
|
360
371
|
if (!oauth2 && authPassword === '') {
|
|
361
|
-
problems.push(`账号 "${name}"
|
|
372
|
+
problems.push(`账号 "${name}" 的${authUser === user ? '授权码 / 应用专用密码' : '登录账号的密码'}未填写`);
|
|
362
373
|
}
|
|
363
374
|
if (imap.host === undefined || imap.host === '')
|
|
364
375
|
problems.push(`账号 "${name}" 的 imap.host 未填写(可填 provider 预设:${known.join('/')})`);
|
|
365
376
|
if (smtp.host === undefined || smtp.host === '')
|
|
366
377
|
problems.push(`账号 "${name}" 的 smtp.host 未填写(同上)`);
|
|
367
378
|
if (problems.length > 0) {
|
|
368
|
-
throw new Error(
|
|
379
|
+
throw new Error(`邮件账号配置不完整:${problems.join(';')}。请在「设置 → 邮件」补全对应账号,再点击「测试连接」。修改会自动保存并立即生效。`);
|
|
369
380
|
}
|
|
370
381
|
const clientId = (acc.clientId ?? common.clientId ?? '').trim();
|
|
371
382
|
return {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Harness 0.1.7 keeps editable fields in the profile and supplies live references. */
|
|
2
|
+
import z from '@deepseek-ai/schemastery';
|
|
3
|
+
import type { EmailConfig } from './config.js';
|
|
4
|
+
export declare const Config: z;
|
|
5
|
+
/** A live view also accepts ordinary values from older hosts and direct callers. */
|
|
6
|
+
export declare function liveConfig(config: object): EmailConfig;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Harness 0.1.7 keeps editable fields in the profile and supplies live references. */
|
|
2
|
+
import z from '@deepseek-ai/schemastery';
|
|
3
|
+
const endpoint = () => z.object({
|
|
4
|
+
host: z.string(), port: z.number().min(1).max(65535), secure: z.boolean(),
|
|
5
|
+
connectionTimeoutMs: z.number(), socketTimeoutMs: z.number(),
|
|
6
|
+
});
|
|
7
|
+
export const Config = z.object({
|
|
8
|
+
provider: z.string().volatile(), user: z.string().volatile(),
|
|
9
|
+
password: z.string().role('secret').volatile(),
|
|
10
|
+
senderName: z.string().volatile(), authUser: z.string().volatile(),
|
|
11
|
+
authPassword: z.string().role('secret').volatile(),
|
|
12
|
+
clientId: z.string().volatile(), authKind: z.union(['password', 'oauth2']).volatile(),
|
|
13
|
+
imap: endpoint().volatile(), smtp: endpoint().volatile(),
|
|
14
|
+
inboxFolder: z.string().volatile(), sendApproval: z.boolean().volatile(),
|
|
15
|
+
maxBodyChars: z.number().volatile(), downloadDir: z.string().volatile(),
|
|
16
|
+
accounts: z.dict(z.any()).role('secret').volatile(), defaultAccount: z.string().volatile(),
|
|
17
|
+
accountsYaml: z.string().role('secret').volatile(), serverPresets: z.string().volatile(),
|
|
18
|
+
maxAttachmentBytes: z.number().volatile(), bodySearchFallback: z.boolean().volatile(),
|
|
19
|
+
bodySearchLimit: z.number().volatile(), idleTimeoutMs: z.number().volatile(),
|
|
20
|
+
legacySettingsImported: z.boolean().volatile(),
|
|
21
|
+
});
|
|
22
|
+
/** A live view also accepts ordinary values from older hosts and direct callers. */
|
|
23
|
+
export function liveConfig(config) {
|
|
24
|
+
return new Proxy(config, {
|
|
25
|
+
get(target, key, receiver) {
|
|
26
|
+
const value = Reflect.get(target, key, receiver);
|
|
27
|
+
return value !== null && typeof value === 'object' && typeof value.get === 'function' ? value.get() : value;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -2,10 +2,21 @@ import { installSendApproval } from './approval.js';
|
|
|
2
2
|
import { createEmailRuntime } from './runtime.js';
|
|
3
3
|
import { buildEmailTools } from './tools.js';
|
|
4
4
|
import { EmailSettingsBackend, installEmailSettingsWeb } from './web.js';
|
|
5
|
+
import { liveConfig, Config as ConfigSchema } from './host-config.js';
|
|
6
|
+
import { installLegacySettingsImport } from './legacy-settings.js';
|
|
7
|
+
import { EmailSettingsSchema, SETTINGS_NAMESPACE } from './settings.js';
|
|
8
|
+
export const Config = ConfigSchema;
|
|
5
9
|
export const name = 'tool-email';
|
|
6
10
|
export const inject = ['settings', 'tools'];
|
|
7
11
|
/** Compose settings/pool lifecycle, tools, browser routes and the outgoing-mail gate. */
|
|
8
12
|
export function apply(ctx, config = {}) {
|
|
13
|
+
config = liveConfig(config);
|
|
14
|
+
installLegacySettingsImport(ctx, config, SETTINGS_NAMESPACE, 'tool-email', Object.keys(EmailSettingsSchema.dict));
|
|
15
|
+
if (typeof ctx.settings.configure === 'function' && typeof ctx.inject === 'function') {
|
|
16
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
17
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
9
20
|
const runtime = createEmailRuntime(ctx, config);
|
|
10
21
|
const backend = new EmailSettingsBackend(ctx, runtime.settingsScope, config);
|
|
11
22
|
backend.watchImpl = runtime.watch;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** No backup is edited or deleted; the completion marker lives beside imported values. */
|
|
2
|
+
export declare function importLegacySettings(ctx: any, config: Record<string, any>, namespace: string, entryId: string, fields: readonly string[], home?: string): Promise<boolean>;
|
|
3
|
+
/** Defer persistence until the entry and settings provider are active. */
|
|
4
|
+
export declare function installLegacySettingsImport(ctx: any, config: Record<string, any>, namespace: string, entryId: string, fields: readonly string[]): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Import only this plugin's retired settings section, preserving profile overrides. */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { parse } from 'yaml';
|
|
6
|
+
function plain(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function missing(legacy, current) {
|
|
10
|
+
const patch = {};
|
|
11
|
+
for (const [key, value] of Object.entries(legacy)) {
|
|
12
|
+
if (current[key] === undefined)
|
|
13
|
+
patch[key] = value;
|
|
14
|
+
else if (plain(value) && plain(current[key])) {
|
|
15
|
+
const nested = missing(value, current[key]);
|
|
16
|
+
if (Object.keys(nested).length)
|
|
17
|
+
patch[key] = nested;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return patch;
|
|
21
|
+
}
|
|
22
|
+
/** No backup is edited or deleted; the completion marker lives beside imported values. */
|
|
23
|
+
export async function importLegacySettings(ctx, config, namespace, entryId, fields, home) {
|
|
24
|
+
if (typeof ctx.settings.register === 'function' || config.legacySettingsImported === true)
|
|
25
|
+
return false;
|
|
26
|
+
// Old namespaces were global. Do not copy credentials into unrelated custom instances.
|
|
27
|
+
if (ctx.fiber?.entry?.options.id !== entryId)
|
|
28
|
+
return false;
|
|
29
|
+
let base = home ?? (process.env.DSH_HOME?.trim() || join(homedir(), '.dsh'));
|
|
30
|
+
if (base === '~')
|
|
31
|
+
base = homedir();
|
|
32
|
+
else if (base.startsWith('~/') || base.startsWith('~\\'))
|
|
33
|
+
base = join(homedir(), base.slice(2));
|
|
34
|
+
let document;
|
|
35
|
+
for (const file of ['settings.yaml.imported', 'settings.yaml']) {
|
|
36
|
+
try {
|
|
37
|
+
document = parse(readFileSync(resolve(base, file), 'utf8'));
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code !== 'ENOENT')
|
|
42
|
+
throw new Error('Cannot read legacy settings; original file preserved');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!plain(document) || !plain(document[namespace]))
|
|
46
|
+
return false;
|
|
47
|
+
const section = Object.fromEntries(fields.filter(key => Object.hasOwn(document[namespace], key)).map(key => [key, document[namespace][key]]));
|
|
48
|
+
if (Object.keys(section).length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
await ctx.settings.update(entryId, { ...missing(section, { ...config }), legacySettingsImported: true });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
/** Defer persistence until the entry and settings provider are active. */
|
|
54
|
+
export function installLegacySettingsImport(ctx, config, namespace, entryId, fields) {
|
|
55
|
+
if (typeof ctx.settings.register === 'function' || typeof ctx.inject !== 'function')
|
|
56
|
+
return;
|
|
57
|
+
ctx.inject(['appReady'], (readyCtx) => {
|
|
58
|
+
readyCtx.effect(() => readyCtx.appReady.onReady(() => {
|
|
59
|
+
void importLegacySettings(ctx, config, namespace, entryId, fields).catch(() => {
|
|
60
|
+
ctx.logger?.warn?.(namespace + ': legacy settings could not be imported; the original settings file is preserved.');
|
|
61
|
+
});
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
}
|
package/lib/runtime.d.ts
CHANGED
|
@@ -6,10 +6,12 @@ import type { EmailWatchResult } from './types.js';
|
|
|
6
6
|
export type EmailClient = Pick<EmailPool, 'list' | 'read' | 'mark' | 'search' | 'send' | 'reply' | 'folders' | 'downloadAttachment' | 'unseenUids' | 'fetchByUids' | 'startIdleSweep' | 'dispose'>;
|
|
7
7
|
export interface EmailSettingsScope {
|
|
8
8
|
get(): unknown;
|
|
9
|
+
namespace?: string;
|
|
10
|
+
config?: EmailConfig;
|
|
9
11
|
}
|
|
10
12
|
export interface EmailRuntimeContext {
|
|
11
13
|
settings: {
|
|
12
|
-
register(namespace: string, schema: unknown, options: {
|
|
14
|
+
register?(namespace: string, schema: unknown, options: {
|
|
13
15
|
base: Partial<EmailSettingsValue>;
|
|
14
16
|
applies: 'live';
|
|
15
17
|
validate(value: unknown): void;
|
|
@@ -19,6 +21,13 @@ export interface EmailRuntimeContext {
|
|
|
19
21
|
user?: Partial<EmailSettingsValue>;
|
|
20
22
|
}>;
|
|
21
23
|
};
|
|
24
|
+
fiber?: {
|
|
25
|
+
entry?: {
|
|
26
|
+
options: {
|
|
27
|
+
id: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
22
31
|
effect(effect: () => () => void): unknown;
|
|
23
32
|
logger?: {
|
|
24
33
|
warn?(message: string): void;
|
package/lib/runtime.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Live settings, account-pool ownership, and independent tool/web watch cursors. */
|
|
2
|
-
import { clampInt, presetNamesIn, resolveEmailSettings } from './config.js';
|
|
2
|
+
import { clampInt, presetNamesIn, resolveEmailSettings, serializeAccountsYaml } from './config.js';
|
|
3
3
|
import { EmailPool, messageOf } from './mail-client.js';
|
|
4
4
|
import { EmailSettingsSchema, SETTINGS_NAMESPACE, toEmailConfig, toSettingsBase, validateSettingsValue } from './settings.js';
|
|
5
|
+
import { liveConfig } from './host-config.js';
|
|
5
6
|
function fingerprintSettings(settings) {
|
|
6
7
|
return JSON.stringify({
|
|
7
8
|
accounts: [...settings.accounts.entries()].map(([name, account]) => [name, account]),
|
|
@@ -18,17 +19,44 @@ function fingerprintSettings(settings) {
|
|
|
18
19
|
}
|
|
19
20
|
/** Register live settings and own exactly one pool for their effective value. */
|
|
20
21
|
export function createEmailRuntime(ctx, config, createPool = settings => new EmailPool(settings)) {
|
|
21
|
-
|
|
22
|
+
config = liveConfig(config);
|
|
23
|
+
const legacy = typeof ctx.settings.register === 'function';
|
|
24
|
+
const namespace = legacy ? SETTINGS_NAMESPACE : ctx.fiber?.entry?.options.id ?? 'tool-email';
|
|
25
|
+
const settingsScope = legacy ? ctx.settings.register(SETTINGS_NAMESPACE, EmailSettingsSchema, {
|
|
22
26
|
base: toSettingsBase(config),
|
|
23
27
|
applies: 'live',
|
|
24
28
|
// The provider dropdown offers the custom preset names beside the built-ins,
|
|
25
29
|
// so validation must accept whatever the table in effect defines.
|
|
26
30
|
validate: value => validateSettingsValue(value, presetNamesIn(value?.serverPresets ?? config.serverPresets)),
|
|
27
|
-
})
|
|
31
|
+
}) : {
|
|
32
|
+
namespace,
|
|
33
|
+
config,
|
|
34
|
+
get: () => {
|
|
35
|
+
let endpointDefaults = {};
|
|
36
|
+
try {
|
|
37
|
+
const settings = resolveEmailSettings({ ...config });
|
|
38
|
+
const account = settings.accounts.get(settings.defaultAccount);
|
|
39
|
+
if (account)
|
|
40
|
+
endpointDefaults = { imap: account.imap, smtp: account.smtp };
|
|
41
|
+
}
|
|
42
|
+
catch { /* A fresh installation remains editable before credentials exist. */ }
|
|
43
|
+
const shared = Object.fromEntries(['provider', 'user', 'password', 'senderName', 'authUser', 'authPassword', 'clientId', 'authKind', 'imap', 'smtp', 'inboxFolder']
|
|
44
|
+
.filter(key => config[key] !== undefined).map(key => [key, config[key]]));
|
|
45
|
+
const cards = config.accounts && Object.keys(config.accounts).length > 0
|
|
46
|
+
? Object.fromEntries(Object.entries(config.accounts).map(([key, account]) => [key, { ...shared, ...account }]))
|
|
47
|
+
: typeof config.user === 'string' && config.user !== '' ? { default: shared } : {};
|
|
48
|
+
return EmailSettingsSchema({ ...toSettingsBase(config), ...endpointDefaults,
|
|
49
|
+
accountsYaml: config.accountsYaml ?? serializeAccountsYaml(cards, config.defaultAccount),
|
|
50
|
+
serverPresets: config.serverPresets ?? '',
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
};
|
|
28
54
|
const getSettingsValue = () => settingsScope.get();
|
|
29
55
|
const getEffectiveSettings = () => {
|
|
30
56
|
// Form defaults must not overwrite row settings or provider presets.
|
|
31
|
-
|
|
57
|
+
if (!legacy)
|
|
58
|
+
return resolveEmailSettings({ ...config });
|
|
59
|
+
const descriptor = (ctx.settings.describe?.() ?? []).find(row => row.ns === namespace);
|
|
32
60
|
const value = getSettingsValue();
|
|
33
61
|
// serverPresets is a *lookup source* for provider ids, not part of the
|
|
34
62
|
// resolved config: it is handed to resolution here and never stored on the
|
package/lib/settings.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import z from 'schemastery';
|
|
2
1
|
import { type EmailConfig } from './config.js';
|
|
3
2
|
/** Settings-document namespace this plugin owns (editable from the Web settings page). */
|
|
4
3
|
export declare const SETTINGS_NAMESPACE = "dsh-email";
|
|
@@ -7,63 +6,63 @@ export declare const SETTINGS_NAMESPACE = "dsh-email";
|
|
|
7
6
|
* The form edits the default/shorthand account; its advanced YAML area
|
|
8
7
|
* edits named accounts through accountsYaml.
|
|
9
8
|
*/
|
|
10
|
-
export declare const EmailSettingsSchema:
|
|
11
|
-
provider:
|
|
12
|
-
user:
|
|
13
|
-
password:
|
|
14
|
-
inboxFolder:
|
|
15
|
-
sendApproval:
|
|
16
|
-
maxBodyChars:
|
|
17
|
-
downloadDir:
|
|
18
|
-
accountsYaml:
|
|
19
|
-
serverPresets:
|
|
20
|
-
imap:
|
|
21
|
-
host:
|
|
22
|
-
port:
|
|
23
|
-
secure:
|
|
24
|
-
}
|
|
25
|
-
host:
|
|
26
|
-
port:
|
|
27
|
-
secure:
|
|
28
|
-
}
|
|
29
|
-
smtp:
|
|
30
|
-
host:
|
|
31
|
-
port:
|
|
32
|
-
secure:
|
|
33
|
-
}
|
|
34
|
-
host:
|
|
35
|
-
port:
|
|
36
|
-
secure:
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
provider:
|
|
40
|
-
user:
|
|
41
|
-
password:
|
|
42
|
-
inboxFolder:
|
|
43
|
-
sendApproval:
|
|
44
|
-
maxBodyChars:
|
|
45
|
-
downloadDir:
|
|
46
|
-
accountsYaml:
|
|
47
|
-
serverPresets:
|
|
48
|
-
imap:
|
|
49
|
-
host:
|
|
50
|
-
port:
|
|
51
|
-
secure:
|
|
52
|
-
}
|
|
53
|
-
host:
|
|
54
|
-
port:
|
|
55
|
-
secure:
|
|
56
|
-
}
|
|
57
|
-
smtp:
|
|
58
|
-
host:
|
|
59
|
-
port:
|
|
60
|
-
secure:
|
|
61
|
-
}
|
|
62
|
-
host:
|
|
63
|
-
port:
|
|
64
|
-
secure:
|
|
65
|
-
}
|
|
66
|
-
}
|
|
9
|
+
export declare const EmailSettingsSchema: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
10
|
+
provider: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
11
|
+
user: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
12
|
+
password: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
13
|
+
inboxFolder: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
14
|
+
sendApproval: import("@deepseek-ai/schemastery").default<boolean, boolean, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
15
|
+
maxBodyChars: import("@deepseek-ai/schemastery").default<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
16
|
+
downloadDir: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
17
|
+
accountsYaml: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
18
|
+
serverPresets: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
19
|
+
imap: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
20
|
+
host: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
21
|
+
port: import("@deepseek-ai/schemastery").default<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
22
|
+
secure: import("@deepseek-ai/schemastery").default<boolean, boolean, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
23
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
24
|
+
host: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
25
|
+
port: import("@deepseek-ai/schemastery").default<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
26
|
+
secure: import("@deepseek-ai/schemastery").default<boolean, boolean, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
27
|
+
}>>, "plain">;
|
|
28
|
+
smtp: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
29
|
+
host: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
30
|
+
port: import("@deepseek-ai/schemastery").default<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
31
|
+
secure: import("@deepseek-ai/schemastery").default<boolean, boolean, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
32
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
33
|
+
host: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
34
|
+
port: import("@deepseek-ai/schemastery").default<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
35
|
+
secure: import("@deepseek-ai/schemastery").default<boolean, boolean, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
36
|
+
}>>, "plain">;
|
|
37
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
38
|
+
provider: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
39
|
+
user: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
40
|
+
password: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
41
|
+
inboxFolder: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
42
|
+
sendApproval: import("@deepseek-ai/schemastery").default<boolean, boolean, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
43
|
+
maxBodyChars: import("@deepseek-ai/schemastery").default<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
44
|
+
downloadDir: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
45
|
+
accountsYaml: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
46
|
+
serverPresets: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
47
|
+
imap: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
48
|
+
host: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
49
|
+
port: import("@deepseek-ai/schemastery").default<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
50
|
+
secure: import("@deepseek-ai/schemastery").default<boolean, boolean, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
51
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
52
|
+
host: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
53
|
+
port: import("@deepseek-ai/schemastery").default<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
54
|
+
secure: import("@deepseek-ai/schemastery").default<boolean, boolean, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
55
|
+
}>>, "plain">;
|
|
56
|
+
smtp: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
57
|
+
host: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
58
|
+
port: import("@deepseek-ai/schemastery").default<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
59
|
+
secure: import("@deepseek-ai/schemastery").default<boolean, boolean, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
60
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
61
|
+
host: import("@deepseek-ai/schemastery").default<string, string, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
62
|
+
port: import("@deepseek-ai/schemastery").default<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
63
|
+
secure: import("@deepseek-ai/schemastery").default<boolean, boolean, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
|
|
64
|
+
}>>, "plain">;
|
|
65
|
+
}>>, "plain">;
|
|
67
66
|
export interface EmailSettingsValue {
|
|
68
67
|
provider: string;
|
|
69
68
|
user: string;
|
package/lib/web.d.ts
CHANGED
|
@@ -178,6 +178,8 @@ export declare class EmailSettingsBackend {
|
|
|
178
178
|
constructor(ctx: any, scope: any, rowConfig: EmailConfig);
|
|
179
179
|
/** Wired by apply(): the email_watch core; 'web' keeps its own cursor scope. */
|
|
180
180
|
watchImpl?: (account: string, folder: string, limit: number, scope: string) => Promise<EmailWatchResult>;
|
|
181
|
+
private get namespace();
|
|
182
|
+
private get currentConfig();
|
|
181
183
|
private userSection;
|
|
182
184
|
/** Effective config for the stored value (row + user-set fields only). */
|
|
183
185
|
private effectiveStored;
|
package/lib/web.js
CHANGED
|
@@ -757,12 +757,16 @@ export class EmailSettingsBackend {
|
|
|
757
757
|
}
|
|
758
758
|
/** Wired by apply(): the email_watch core; 'web' keeps its own cursor scope. */
|
|
759
759
|
watchImpl;
|
|
760
|
+
get namespace() { return this.scope.namespace ?? SETTINGS_NAMESPACE; }
|
|
761
|
+
get currentConfig() { return this.scope.config ?? this.rowConfig; }
|
|
760
762
|
userSection() {
|
|
761
|
-
const descriptor = (this.ctx.settings.describe?.() ?? []).find((row) => row.ns ===
|
|
763
|
+
const descriptor = (this.ctx.settings.describe?.() ?? []).find((row) => row.ns === this.namespace);
|
|
762
764
|
return descriptor?.user;
|
|
763
765
|
}
|
|
764
766
|
/** Effective config for the stored value (row + user-set fields only). */
|
|
765
767
|
effectiveStored() {
|
|
768
|
+
if (this.scope.config)
|
|
769
|
+
return { ...this.currentConfig };
|
|
766
770
|
const stored = this.scope.get();
|
|
767
771
|
const merged = { ...this.rowConfig, ...toEmailConfig(stored, this.userSection()) };
|
|
768
772
|
// toEmailConfig drops serverPresets — it must never enter the fingerprint —
|
|
@@ -771,14 +775,14 @@ export class EmailSettingsBackend {
|
|
|
771
775
|
return { ...merged, ...(typeof stored?.serverPresets === 'string' ? { serverPresets: stored.serverPresets } : {}) };
|
|
772
776
|
}
|
|
773
777
|
async snapshot() {
|
|
774
|
-
const descriptor = (this.ctx.settings.describe?.() ?? []).find((row) => row.ns ===
|
|
778
|
+
const descriptor = (this.ctx.settings.describe?.() ?? []).find((row) => row.ns === this.namespace);
|
|
775
779
|
const value = this.scope.get();
|
|
776
780
|
const whale = findWhaleAsset();
|
|
777
781
|
const presets = presetsSnapshot(value.serverPresets);
|
|
778
782
|
// The cards describe the *effective* accountsYaml — the same text the
|
|
779
783
|
// advanced editor shows, and the same source the accounts field reads.
|
|
780
784
|
const effective = this.effectiveStored();
|
|
781
|
-
const draft = readAccountsDraft(effective.accountsYaml ?? '', presets.custom, effective.defaultAccount, tokenLookup());
|
|
785
|
+
const draft = readAccountsDraft(this.scope.config ? value.accountsYaml : effective.accountsYaml ?? '', presets.custom, effective.defaultAccount, tokenLookup());
|
|
782
786
|
return {
|
|
783
787
|
settings: {
|
|
784
788
|
value,
|
|
@@ -813,7 +817,17 @@ export class EmailSettingsBackend {
|
|
|
813
817
|
// one of them is a legal choice rather than an unknown provider.
|
|
814
818
|
validateSettingsValue(value, presetNamesIn(value?.serverPresets ?? this.scope.get()?.serverPresets));
|
|
815
819
|
const before = accountNamesOf(this.scope.get());
|
|
816
|
-
|
|
820
|
+
if (this.scope.config) {
|
|
821
|
+
// The card editor takes ownership of legacy shorthand/maps. Clear only
|
|
822
|
+
// the superseded account storage, so removing the last card cannot
|
|
823
|
+
// resurrect a hidden old account on the next read.
|
|
824
|
+
const section = Object.hasOwn(value, 'accountsYaml')
|
|
825
|
+
? { ...value, accounts: {}, user: '', password: '', authUser: '', authPassword: '' }
|
|
826
|
+
: value;
|
|
827
|
+
await this.ctx.settings.update(this.namespace, section, expectedRevision);
|
|
828
|
+
}
|
|
829
|
+
else
|
|
830
|
+
await this.ctx.settings.replace(this.namespace, value, expectedRevision);
|
|
817
831
|
// A deleted account must not leave its refresh token behind: the store is
|
|
818
832
|
// keyed by account name, so the credential of a mailbox that is no longer
|
|
819
833
|
// configured would sit on disk, and a later account reusing that name would
|
|
@@ -841,12 +855,12 @@ export class EmailSettingsBackend {
|
|
|
841
855
|
// does for the stored settings (it never enters the resolved fingerprint).
|
|
842
856
|
const draft = toEmailConfig(value, null);
|
|
843
857
|
const presets = value?.serverPresets ?? this.scope.get()?.serverPresets;
|
|
858
|
+
const requested = typeof accountName === 'string' && accountName.trim() !== '' ? accountName.trim() : '';
|
|
844
859
|
const settings = resolveEmailSettings({
|
|
845
|
-
...this.
|
|
860
|
+
...this.currentConfig,
|
|
846
861
|
...draft,
|
|
847
862
|
...(typeof presets === 'string' ? { serverPresets: presets } : {}),
|
|
848
|
-
});
|
|
849
|
-
const requested = typeof accountName === 'string' && accountName.trim() !== '' ? accountName.trim() : '';
|
|
863
|
+
}, requested || undefined);
|
|
850
864
|
const available = [...settings.accounts.keys()];
|
|
851
865
|
const name = requested !== '' ? requested : settings.defaultAccount;
|
|
852
866
|
const cfg = settings.accounts.get(name);
|
|
@@ -1070,7 +1084,7 @@ export class EmailSettingsBackend {
|
|
|
1070
1084
|
const presets = typeof body.serverPresets === 'string'
|
|
1071
1085
|
? customPresetsOf(body.serverPresets).custom
|
|
1072
1086
|
: customPresetsOf(this.scope.get().serverPresets).custom;
|
|
1073
|
-
const draft = readAccountsDraft(text, presets, this.
|
|
1087
|
+
const draft = readAccountsDraft(text, presets, this.currentConfig.defaultAccount, tokenLookup());
|
|
1074
1088
|
this.responseJson(res, 200, {
|
|
1075
1089
|
ok: true,
|
|
1076
1090
|
value: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "DSH 邮件插件:IMAP/SMTP 收发搜索、回复转发、附件与整理,多账号卡片设置页、Outlook OAuth2 登录与发信审批。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
"mailparser": "^3.9.22",
|
|
66
66
|
"nodemailer": "^9.0.5",
|
|
67
67
|
"schemastery": "^3.18.0",
|
|
68
|
-
"yaml": "^2.9.0"
|
|
68
|
+
"yaml": "^2.9.0",
|
|
69
|
+
"@deepseek-ai/schemastery": "^3.18.3"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
72
|
"@types/node": "^26.2.0",
|