dsh-email 0.16.3 → 0.17.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 CHANGED
@@ -4,6 +4,16 @@
4
4
 
5
5
  [历史英文记录](docs/CHANGELOG.en.md)
6
6
 
7
+ ## 0.17.1(2026-10-18)
8
+
9
+ - 完成 [#19](https://github.com/STARDUSTLC666/dsh-email/issues/19) 的发信策略说明:全局确认开关关闭时,账号页显示中英文影响范围,明确所有账号的模型发信、回复和转发都不再逐次确认;只读设置不能编辑该开关。
10
+ - 区分按账号的可信名单与全局关闭确认,说明禁止名单、宿主权限和草稿手动确认继续生效;增加可信名单和全局关闭确认均保留宿主拒绝结果的回归。
11
+ - 修复新版宿主中切换全局设置后,空服务商字段使手填服务器账号报 `provider "" 未知` 的问题;单账号、多账号与 YAML 账号均保留原服务器和凭据。
12
+
13
+ ## 0.17.0(2026-10-17)
14
+
15
+ - 设置面板顶部新增「首次使用」分步引导:一次只讲一步,支持上一步/下一步/跳过,看完记在本机不再出现。
16
+ - 引导内容与该插件的实际用法一致;可在开发者工具里清掉 `dsh-first-run:dsh-email:v1` 重新查看。
7
17
  ## 0.16.3 (2026-10-17)
8
18
 
9
19
  - 修复未配置邮箱时,服务商示例列表被凭据脱敏器误隐藏的问题;保留真实凭据的原有脱敏规则。
package/docs/DRAFTS.en.md CHANGED
@@ -43,6 +43,14 @@ trustedRecipientsYaml: |
43
43
 
44
44
  Renaming an account does not transfer old rules. Rule changes do not reconnect the mailbox.
45
45
 
46
+ ## Global send confirmation
47
+
48
+ At the top of Settings → Email → Accounts, use “Confirm model-initiated sends.” It is on by default. Turning it off saves automatically and displays the scope of the change.
49
+
50
+ This is the global `sendApproval` setting and affects every account. With it off, model calls to `email_send` and `email_reply` (including reply-all and forward) skip the plugin's individual confirmation, including recipients outside the trusted list. Account trust rules only skip confirmation for `email_send` when every To/Cc recipient matches; they do not mean unrestricted sending.
51
+
52
+ Blocked addresses and domains still stop delivery before an SMTP connection. Host permissions still apply, and the Drafts tab still requires human review and explicit send confirmation. Keep global confirmation on and configure account trust rules to allow only selected recipients.
53
+
46
54
  ## Storage and limits
47
55
 
48
56
  Drafts live at `DSH_HOME/data/dsh-email/drafts-v1.json`, with uploads under `draft-uploads`. Without DSH_HOME the default is `~/.dsh`. Profiles sharing DSH_HOME share drafts; accounts resolve against the current profile and the actual identity appears in review.
package/docs/DRAFTS.md CHANGED
@@ -43,6 +43,14 @@ trustedRecipientsYaml: |
43
43
 
44
44
  账号改名后旧规则不会自动作用于新账号。规则变化不会重建邮箱连接。
45
45
 
46
+ ## 全局发信确认
47
+
48
+ 在「设置 → 邮件 → 邮箱账号」顶部,可切换「模型发信前确认」。默认开启;关闭会自动保存,并显示影响范围提示。
49
+
50
+ 这个开关对应全局 `sendApproval`,对所有账号生效。关闭后,模型调用 `email_send` 或 `email_reply`(含回复全部、转发)不再经过插件逐次确认,即使收件人不在可信名单中。按账号的可信名单仍只对全部 To/Cc 均匹配的 `email_send` 跳过确认,不能等同于完全开放。
51
+
52
+ 禁止地址和禁止域名继续在 SMTP 连接前拦截;宿主权限仍生效,草稿页仍需人工核对并确认发送。只想放行特定收件人时,保留全局确认并配置账号的可信规则即可。
53
+
46
54
  ## 存储与范围
47
55
 
48
56
  草稿保存于 `DSH_HOME/data/dsh-email/drafts-v1.json`,上传副本位于其下 `draft-uploads`。未指定 DSH_HOME 时使用 `~/.dsh`。同一 DSH_HOME 共用草稿,发件账号以当前 profile 配置为准,预览会显示实际身份。
@@ -1,5 +1,7 @@
1
1
  # dsh-email 验证记录
2
2
 
3
+ [0.17.1 发信策略与设置保存验收](validation/0.17.1.md)。
4
+
3
5
  [0.16.3 · DSH 0.2.1-alpha.5 浏览器操作验收](validation/0.16.3.md)。
4
6
 
5
7
  [0.16.2 Windows 令牌加密与迁移验收](validation/0.16.2.md)。
@@ -0,0 +1,29 @@
1
+ # 0.17.1 发信策略与设置保存验收
2
+
3
+ 日期:2026-10-18。对应 [Issue #19](https://github.com/STARDUSTLC666/dsh-email/issues/19)。
4
+
5
+ 环境:Windows x64、Node 24.16.0、官方 npm `@deepseek-ai/dsh@0.2.1-rc.2`,独立 `DSH_HOME`。实际安装候选 tarball;两个账号的 SMTP 均指向 `127.0.0.1` 测试服务,地址使用 `.invalid`。
6
+
7
+ ## 改动
8
+
9
+ - 账号页已有全局 `sendApproval` 开关;关闭后新增中英文影响范围提示,只读设置不能编辑该开关。可信名单仍是按账号、所有 To/Cc 均匹配才跳过 `email_send` 确认。
10
+ - 修复新版宿主直接保存空服务商后,自定义 IMAP/SMTP 账号报 `provider "" 未知`。空选择与旧设置投影、YAML 序列化一致地按未设置处理,未知的非空服务商仍报错。
11
+
12
+ ## 验证结果
13
+
14
+ | 检查 | 结果 |
15
+ | :-- | :-- |
16
+ | 空服务商回归 | 修复前新增测试失败;修复后单账号、账号映射、YAML、账号级空值均保留服务器与凭据 |
17
+ | 本地全套测试 | 349 项通过,0 失败、0 跳过 |
18
+ | 真实宿主允许分支 | 可信 To/Cc 实际提交到本机 SMTP;全局关闭确认后两个账号均可向未列入可信名单的地址提交 |
19
+ | 拒绝优先与宿主权限 | 被禁止的 Cc 覆盖可信规则;全局关闭确认时,禁止地址和真实 `tools.guard()` 拒绝仍阻止 SMTP 提交 |
20
+ | 实际页面保存后发送 | 浏览器切换开关并自动保存后,两张自定义账号卡片仍可解析,后续工具发信获得本机 SMTP 回执 |
21
+ | 页面与持久化 | 中文提示可见;重载保留开关与提示;切换英文正常;重新开启后提示消失并保存 |
22
+ | 页面布局与可访问性 | 提示由复选框的 `aria-describedby` 关联;检查时内容宽度与滚动宽度均为 605px,无控制台错误 |
23
+ | 草稿确认 | 在真实宿主中关闭全局确认后,草稿预览成功,未明确确认的发送仍返回 400 |
24
+
25
+ ## 未验证范围
26
+
27
+ 本轮没有运行真实模型回合中的逐次审批弹框。宿主工具 API 在缺少活跃模型回合时不能完成该交互;默认确认、混合名单、其他账号、子域名与回复策略另有自动契约测试,不把非回合调用的失败算成交互审批验收。
28
+
29
+ 未连接个人邮箱、真实 IMAP 或外部 SMTP,也未验证 OAuth 登录、实际收件箱投递或原生 Desktop 窗口操作。
package/lib/client.js CHANGED
@@ -9,6 +9,71 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  const React = require("react");
10
10
  const { useState, useEffect, useCallback, useRef } = React;
11
11
  const h = React.createElement;
12
+ const __FR_KEY = 'dsh-first-run:dsh-email:v1';
13
+ const __FR_TITLE = '邮件 · 首次使用(4 步)';
14
+ const __FR_STEPS = [
15
+ { title: '先把账号填上', body: '下面填 IMAP / SMTP 地址与授权码(QQ 邮箱用「授权码」,不是登录密码)。保存后这里会显示连接状态。' },
16
+ { title: '看一眼收件箱', body: '连接成功后,下方会列出最近邮件的标题与发件人;点开任意一封可读正文。' },
17
+ { title: '让它替你整理', body: '在对话里直接说你要做的事,比在页面里逐个点更快。', tryIt: '把今天的未读邮件按主题归纳一下' },
18
+ { title: '发信前一定先看草稿', body: '涉及发信时默认先生成草稿给你确认,确认后才真正发送;收件人规则在下方「发信规则」里配。' },
19
+ ];
20
+
21
+ // ---- first-run guide (shared snippet, keep in sync across plugins) ----
22
+ const __FR_STYLE_ID = 'dsh-first-run-css';
23
+ const __FR_CSS = [
24
+ '.fr-card{border:1px solid var(--dsw-border,rgba(0,0,0,.10));border-radius:12px;padding:14px 16px;margin:0 0 14px;background:linear-gradient(180deg,rgba(59,130,246,.06),rgba(59,130,246,.02))}',
25
+ '.fr-head{display:flex;align-items:center;gap:8px;margin-bottom:6px}',
26
+ '.fr-dot{width:6px;height:6px;border-radius:50%;background:var(--dsw-accent,#3b82f6)}',
27
+ '.fr-title{font-weight:650;font-size:13.5px}',
28
+ '.fr-count{margin-left:auto;font-size:11px;color:var(--dsw-text-weak,#6b7280);font-variant-numeric:tabular-nums}',
29
+ '.fr-step{font-weight:600;font-size:13px;margin:2px 0 4px}',
30
+ '.fr-body{color:var(--dsw-text-weak,#4b5563);font-size:12.5px;line-height:1.65}',
31
+ '.fr-try{margin-top:8px;padding:7px 9px;border-radius:8px;background:rgba(59,130,246,.09);font-size:12px;display:flex;gap:8px}',
32
+ '.fr-try code{font-family:ui-monospace,monospace;white-space:pre-wrap;word-break:break-word}',
33
+ '.fr-bar{display:flex;gap:8px;align-items:center;margin-top:12px}',
34
+ '.fr-progress{display:flex;gap:4px;margin-right:auto}',
35
+ '.fr-seg{width:16px;height:3px;border-radius:2px;background:var(--dsw-border,rgba(0,0,0,.12))}',
36
+ '.fr-seg.on{background:var(--dsw-accent,#3b82f6)}',
37
+ '.fr-btn{border:1px solid var(--dsw-border,rgba(0,0,0,.14));background:transparent;color:inherit;border-radius:8px;padding:4px 10px;font-size:12px;cursor:pointer}',
38
+ '.fr-btn:hover{background:rgba(0,0,0,.04)}',
39
+ '.fr-btn.primary{background:var(--dsw-accent,#3b82f6);border-color:transparent;color:#fff}',
40
+ '.fr-btn.ghost{border-color:transparent;color:var(--dsw-text-weak,#6b7280)}',
41
+ ].join('\n');
42
+ function __frEnsureCss() {
43
+ if (document.getElementById(__FR_STYLE_ID)) return;
44
+ const el = document.createElement('style');
45
+ el.id = __FR_STYLE_ID;
46
+ el.textContent = __FR_CSS;
47
+ document.head.appendChild(el);
48
+ }
49
+ function __frRead() { try { return localStorage.getItem(__FR_KEY) === 'done'; } catch { return false; } }
50
+ function __frWrite() { try { localStorage.setItem(__FR_KEY, 'done'); } catch {} }
51
+ function __FirstRunCard() {
52
+ const [done, setDone] = React.useState(__frRead);
53
+ const [index, setIndex] = React.useState(0);
54
+ React.useEffect(() => { __frEnsureCss(); }, []);
55
+ if (done) return null;
56
+ const step = __FR_STEPS[index];
57
+ const last = index >= __FR_STEPS.length - 1;
58
+ const close = () => { __frWrite(); setDone(true); };
59
+ return h('div', { className: 'fr-card' },
60
+ h('div', { className: 'fr-head' }, h('span', { className: 'fr-dot' }), h('span', { className: 'fr-title' }, __FR_TITLE), h('span', { className: 'fr-count' }, (index + 1) + ' / ' + __FR_STEPS.length)),
61
+ h('div', { className: 'fr-step' }, step.title),
62
+ h('div', { className: 'fr-body' }, step.body),
63
+ step.tryIt ? h('div', { className: 'fr-try' }, h('span', null, '可以直接说'), h('code', null, '「' + step.tryIt + '」')) : null,
64
+ h('div', { className: 'fr-bar' },
65
+ h('div', { className: 'fr-progress' }, __FR_STEPS.map((_, i) => h('span', { key: i, className: 'fr-seg' + (i <= index ? ' on' : '') }))),
66
+ h('button', { className: 'fr-btn ghost', type: 'button', onClick: close }, '跳过'),
67
+ index > 0 ? h('button', { className: 'fr-btn', type: 'button', onClick: () => setIndex(index - 1) }, '上一步') : null,
68
+ h('button', { className: 'fr-btn primary', type: 'button', onClick: () => { if (last) close(); else setIndex(index + 1); } }, last ? '开始使用' : '下一步')));
69
+ }
70
+ function __withFirstRun(Component) {
71
+ return function FirstRunWrapped(props) {
72
+ return h(React.Fragment, null, h(__FirstRunCard, null), h(Component, props));
73
+ };
74
+ }
75
+ // ---- end first-run guide ----
76
+
12
77
 
13
78
  // Harness supplies document.baseURI for reverse-proxy mounts and in-page navigation.
14
79
  const ROUTE = "_dsh/dsh-email/settings";
@@ -293,6 +358,7 @@ var UI = {
293
358
 
294
359
  // 全局设置
295
360
  "global.sendApproval": "模型发信前确认(建议保留;可信规则可按账号跳过)",
361
+ "global.sendApprovalRisk": "关闭后,所有账号的模型发信、回复和转发将不再逐次询问,未列入可信名单的收件人也可能直接收到邮件。禁止收件人规则和宿主权限继续生效;草稿页仍需手动确认发送。",
296
362
  "global.downloadDirLabel": "附件下载目录(默认保存在当前工作区)",
297
363
  "global.downloadDirPlaceholder": "留空使用默认",
298
364
  "global.hint": "以上设置对所有账号统一生效。需要插件确认的工具发信在 Full Access 下不会弹框;可改用草稿页手动核对并确认。禁止收件人规则始终生效。",
@@ -474,6 +540,7 @@ var UI = {
474
540
 
475
541
  // 全局设置
476
542
  "global.sendApproval": "Confirm model-initiated sends (recommended; saved trust can skip per account)",
543
+ "global.sendApprovalRisk": "With this off, model-initiated sends, replies and forwards skip this plugin's confirmation for every account, including recipients outside the trusted list. Blocked-recipient rules and host permissions still apply. Drafts still require manual send confirmation.",
477
544
  "global.downloadDirLabel": "Attachment download directory (defaults to the current workspace)",
478
545
  "global.downloadDirPlaceholder": "Leave empty for the default",
479
546
  "global.hint": "These settings apply to every account. Tool sends that still require plugin confirmation cannot show a dialog under Full Access; use the Drafts tab to review and confirm manually. Blocked-recipient rules always apply.",
@@ -2620,10 +2687,15 @@ function EmailSettingsSection() {
2620
2687
  h("input", {
2621
2688
  type: "checkbox",
2622
2689
  checked: draft.sendApproval === true,
2690
+ disabled: snapshot?.writable === false,
2691
+ "aria-describedby": draft.sendApproval === false ? "dshe-send-approval-risk" : undefined,
2623
2692
  onChange: (e) => update({ sendApproval: e.target.checked }),
2624
2693
  }),
2625
2694
  t("global.sendApproval"),
2626
2695
  ]),
2696
+ draft.sendApproval === false
2697
+ ? h("div", { id: "dshe-send-approval-risk", className: "dshe-alert info", role: "status" }, t("global.sendApprovalRisk"))
2698
+ : null,
2627
2699
  h("div", { className: "dshe-field" }, [
2628
2700
  h("label", null, t("global.downloadDirLabel")),
2629
2701
  fieldInput("text", draft.downloadDir, (v) => update({ downloadDir: v }), t("global.downloadDirPlaceholder")),
@@ -2857,9 +2929,10 @@ function apply(ctx) {
2857
2929
  return UI[lang]["nav.title"];
2858
2930
  },
2859
2931
  inject: () => ({}),
2860
- }, EmailSettingsSection));
2932
+ }, __withFirstRun(EmailSettingsSection)));
2861
2933
  }
2862
2934
 
2935
+ exports.__firstRunCard = __FirstRunCard;
2863
2936
  exports.apply = apply;
2864
2937
  exports.inject = inject;
2865
2938
 
package/lib/config.js CHANGED
@@ -365,7 +365,9 @@ export function mergeAccountDefaults(common, account = {}) {
365
365
  }
366
366
  /** Merge one account over shared defaults and its selected provider, then validate it. */
367
367
  function resolveAccount(name, common, acc, allowEnvPassword, providers, known) {
368
- const requested = acc.provider ?? common.provider;
368
+ // Public settings persist the form's empty custom-server selection directly.
369
+ // Normalize it like the legacy projection and YAML writer before preset lookup.
370
+ const requested = (acc.provider === '' ? undefined : acc.provider) ?? (common.provider === '' ? undefined : common.provider);
369
371
  const preset = requested === undefined ? undefined : providers[requested];
370
372
  if (requested !== undefined && preset === undefined) {
371
373
  throw new Error(`dsh-email:账号 "${name}" 的 provider "${requested}" 未知,可选:${known.join('/')};或省略 provider 直接填 imap.host 与 smtp.host`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-email",
3
- "version": "0.16.3",
3
+ "version": "0.17.1",
4
4
  "description": "在 DSH 中收发、搜索和整理邮件,支持多个邮箱账号。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",