dsh-email 0.10.2 → 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
@@ -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
@@ -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/*` 为官方保留命名空间。
@@ -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';