dsh-email 0.10.1 → 0.10.3

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/README.en.md CHANGED
@@ -42,7 +42,7 @@ Example:
42
42
 
43
43
  ## Compatibility
44
44
 
45
- Verified against `@deepseek-ai/dsh@0.1.2-alpha.3` on 2026-09-01 (settings page / asset route / email_watch fully regression-tested). Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
45
+ Verified against source-run `@deepseek-ai/dsh@0.1.2-alpha.4` on 2026-09-02 (69 tests plus a Web-profile startup smoke test). Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
46
46
 
47
47
  ## Installation
48
48
 
@@ -149,9 +149,13 @@ Every provider requires an authorization code / app-specific password instead of
149
149
  ```sh
150
150
  pnpm install
151
151
  pnpm run build # tsc → lib/
152
- pnpm test # 构建 + node --test(配置/解析/注册与审批门,44 个用例,无需真实邮箱)
152
+ pnpm test # build + offline tests; no real mailbox required
153
153
  ```
154
154
 
155
+ `src/index.ts` composes the plugin. `runtime.ts` owns live settings, account pools, and separate tool/web watch cursors. `tools.ts` wires the ten tool implementations. `tool-contract.ts` defines parameters, output schemas, and text rendering. `approval.ts` owns the outgoing-mail gate. IMAP/SMTP transport remains in `mail-client.ts`, and browser routes remain in `web.ts`.
156
+
157
+ Tests cover pool replacement after live settings changes, unload cleanup, cancellation and workspace propagation, independent tool/web cursors, and rejected approval preventing send execution. In-memory clients replace mailbox connections.
158
+
155
159
  ## License
156
160
 
157
161
  MIT. This is a community plugin, not affiliated with DeepSeek; `@deepseek-ai/*` is an officially reserved namespace.
package/README.md CHANGED
@@ -55,7 +55,7 @@ Email tools for DeepSeek Harness: list, read, search and send mail through stand
55
55
 
56
56
  ## 兼容性
57
57
 
58
- 在 `@deepseek-ai/dsh@0.1.2-alpha.3` 上实测通过(2026-09-01,设置页/素材路由/email_watch 全回归)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
58
+ 在 `@deepseek-ai/dsh@0.1.2-alpha.4` 源码模式下实测通过(2026-09-02,69 项测试及 Web profile 启动冒烟)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
59
59
 
60
60
  ## 安装
61
61
 
@@ -183,9 +183,13 @@ dsh plugin --profile web remove dsh-email
183
183
  ```sh
184
184
  pnpm install
185
185
  pnpm run build # tsc → lib/
186
- pnpm test # 构建 + node --test(配置/解析/注册与审批门,44 个用例,无需真实邮箱)
186
+ pnpm test # 构建 + 离线测试,无需真实邮箱
187
187
  ```
188
188
 
189
+ `src/index.ts` 只负责组合插件。`runtime.ts` 管理动态设置、账号连接池和网页/工具各自的监视游标;`tools.ts` 接线十个工具的执行逻辑;`tool-contract.ts` 集中维护参数、输出 schema 和中文渲染;`approval.ts` 管理发信审批。IMAP/SMTP 传输仍由 `mail-client.ts` 负责,网页路由由 `web.ts` 负责。
190
+
191
+ 测试覆盖动态配置换池、卸载释放、取消信号与工作区透传、工具/网页游标隔离,以及审批拒绝时不会进入发送执行。测试用内存客户端替代邮箱连接。
192
+
189
193
  ## 协议
190
194
 
191
195
  MIT。这是一个社区插件,与 DeepSeek 官方无关;`@deepseek-ai/*` 为官方保留命名空间。
@@ -194,4 +198,4 @@ MIT。这是一个社区插件,与 DeepSeek 官方无关;`@deepseek-ai/*`
194
198
 
195
199
  - [dsh-slack](https://github.com/STARDUSTLC666/dsh-slack) — Slack 通知/收件箱
196
200
  - [dsh-dingtalk](https://github.com/STARDUSTLC666/dsh-dingtalk) — 钉钉群通知(零依赖)
197
- - [dsh-email](https://github.com/STARDUSTLC666/dsh-email) — 邮件八件套 + Web 设置页 + 新邮件弹窗
201
+ - [dsh-email](https://github.com/STARDUSTLC666/dsh-email) — 邮件八件套 + Web 设置页 + 新邮件弹窗
@@ -0,0 +1,34 @@
1
+ /** Outgoing-mail approval is independent of tool execution. */
2
+ import type { EmailRuntime } from './runtime.js';
3
+ type ApprovalDecision = {
4
+ kind: 'allow';
5
+ } | {
6
+ kind: 'deny';
7
+ reason: string;
8
+ } | {
9
+ kind: 'ask';
10
+ reason?: string;
11
+ };
12
+ interface PendingExecution {
13
+ name: string;
14
+ arguments?: unknown;
15
+ agent?: unknown;
16
+ callId?: string;
17
+ signal?: AbortSignal;
18
+ }
19
+ interface ApprovalContext {
20
+ on(event: 'tools/pre-execute', listener: (exec: PendingExecution, next: () => Promise<ApprovalDecision>) => Promise<ApprovalDecision>, options: {
21
+ prepend: boolean;
22
+ }): unknown;
23
+ get(name: 'approval'): {
24
+ request(input: {
25
+ agent?: unknown;
26
+ toolName: string;
27
+ callId?: string;
28
+ reason: string;
29
+ signal?: AbortSignal;
30
+ }): Promise<string>;
31
+ } | undefined;
32
+ }
33
+ export declare function installSendApproval(ctx: ApprovalContext, runtime: Pick<EmailRuntime, 'getSettingsValue' | 'getEffectiveSettings'>): void;
34
+ export {};
@@ -0,0 +1,55 @@
1
+ export function installSendApproval(ctx, runtime) {
2
+ ctx.on('tools/pre-execute', async (exec, next) => {
3
+ if (exec?.name !== 'email_send' && exec?.name !== 'email_reply')
4
+ return next();
5
+ const value = runtime.getSettingsValue();
6
+ if (value.sendApproval === false)
7
+ return next();
8
+ try {
9
+ runtime.getEffectiveSettings();
10
+ }
11
+ catch {
12
+ return next(); // unconfigured: let the tool report the actionable hint
13
+ }
14
+ let reason;
15
+ if (exec.name === 'email_send') {
16
+ const args = (exec.arguments ?? {});
17
+ const attachCount = Array.isArray(args.attachments) ? args.attachments.length : 0;
18
+ reason = '发送邮件给 ' + args.to + ',主题「' + args.subject + '」' + (attachCount > 0 ? ',附件 ' + attachCount + ' 个' : '');
19
+ }
20
+ else {
21
+ const args = (exec.arguments ?? {});
22
+ const mode = typeof args.mode === 'string' && args.mode.trim() !== '' ? args.mode.trim().toLowerCase() : 'reply';
23
+ const modeLabel = mode === 'forward' ? '转发' : mode === 'reply-all' ? '回复全部' : '回复';
24
+ reason = modeLabel + '邮件(原邮件 uid=' + args.uid + ')' + (mode === 'forward' && typeof args.to === 'string' && args.to.trim() !== '' ? ',收件人 ' + args.to : '');
25
+ }
26
+ // Gate-owned approval: we run the approval round-trip ourselves so the
27
+ // denial reason is always honest and actionable — including the Full
28
+ // Access case where the harness policy answers 'rejected' without ever
29
+ // showing a dialog.
30
+ const approval = ctx.get('approval');
31
+ if (approval === undefined) {
32
+ return {
33
+ kind: 'deny',
34
+ reason: 'email_send 需要确认,但当前环境没有审批通道(如 headless)。如确定安全,可在配置中设置 sendApproval: false 后直接发送。',
35
+ };
36
+ }
37
+ const outcome = await approval.request({
38
+ agent: exec.agent,
39
+ toolName: exec.name,
40
+ callId: exec.callId,
41
+ reason,
42
+ signal: exec.signal,
43
+ });
44
+ if (outcome === 'allowed-once')
45
+ return next();
46
+ if (outcome === 'cancelled')
47
+ return { kind: 'deny', reason: '发信确认被取消,邮件未发送。' };
48
+ if (outcome === 'unavailable')
49
+ return { kind: 'deny', reason: '发信确认不可用(没有可用的审批界面),邮件未发送。' };
50
+ return {
51
+ kind: 'deny',
52
+ reason: '发信未获批准:要么你拒绝了,要么当前会话处于 Full Access(审批策略 never,不会弹框)。若在 Full Access:切到 Read Only / Write 再发,或关闭 sendApproval(自行承担风险)。',
53
+ };
54
+ }, { prepend: true });
55
+ }
package/lib/index.d.ts CHANGED
@@ -1,16 +1,12 @@
1
- import { type EmailConfig } from './config.js';
1
+ import type { EmailConfig } from './config.js';
2
2
  export declare const name = "tool-email";
3
3
  export declare const inject: string[];
4
4
  export type Config = EmailConfig;
5
- /**
6
- * 解析天级日期参数为 Date。接受 YYYY-MM-DD 或完整 ISO 时间。
7
- * endInclusive=true 时返回“该日结束”(次日零点),用于 until 语义(IMAP BEFORE 是不含当天的)。
8
- */
9
- export declare function parseEmailDay(input: string, label: string, endInclusive?: boolean): Date;
5
+ /** Compose settings/pool lifecycle, tools, browser routes and the outgoing-mail gate. */
10
6
  export declare function apply(ctx: any, config?: Config): void;
11
- export { PROVIDER_NAMES, EMAIL_PASSWORD_ENV } from './config.js';
12
- export { resolveEmailConfig, resolveEmailSettings, parseAccountsYaml, clampInt, defaultDownloadDir } from './config.js';
13
- export { stripHtml, truncateText, flattenAddresses, sanitizeFilename, parseRawMessage } from './parse.js';
14
- export { EmailPool, MailError, messageOf, validateAttachmentPaths, selectAttachmentPart, messageMatchesQuery, buildReplyMessage, extractMessageIds } from './mail-client.js';
15
- export { SETTINGS_NAMESPACE, EmailSettingsSchema, toSettingsBase, toEmailConfig, validateSettingsValue } from './settings.js';
16
- export { SETTINGS_ROUTE, EmailSettingsBackend, installEmailSettingsWeb } from './web.js';
7
+ export { clampInt, defaultDownloadDir, EMAIL_PASSWORD_ENV, parseAccountsYaml, PROVIDER_NAMES, resolveEmailConfig, resolveEmailSettings } from './config.js';
8
+ export { buildReplyMessage, EmailPool, extractMessageIds, MailError, messageMatchesQuery, messageOf, selectAttachmentPart, validateAttachmentPaths } from './mail-client.js';
9
+ export { flattenAddresses, parseRawMessage, sanitizeFilename, stripHtml, truncateText } from './parse.js';
10
+ export { EmailSettingsSchema, SETTINGS_NAMESPACE, toEmailConfig, toSettingsBase, validateSettingsValue } from './settings.js';
11
+ export { parseEmailDay } from './tool-contract.js';
12
+ export { EmailSettingsBackend, installEmailSettingsWeb, SETTINGS_ROUTE } from './web.js';