dsh-email 0.14.7 → 0.15.0

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/lib/settings.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import z from 'schemastery';
2
2
  import { ENDPOINT_DEFAULTS, normalizeSettingsEndpoint, PROVIDER_NAMES } from './config.js';
3
+ import { parseRecipientPolicies } from './recipient-policy.js';
3
4
  /** Settings-document namespace this plugin owns (editable from the Web settings page). */
4
5
  export const SETTINGS_NAMESPACE = 'dsh-email';
5
6
  /**
@@ -13,6 +14,7 @@ export const EmailSettingsSchema = z.object({
13
14
  password: z.string().role('secret').default(''),
14
15
  inboxFolder: z.string().default('INBOX'),
15
16
  sendApproval: z.boolean().default(true),
17
+ trustedRecipientsYaml: z.string().default(''),
16
18
  maxBodyChars: z.number().default(20000),
17
19
  downloadDir: z.string().default(''),
18
20
  accountsYaml: z.string().role('secret').default(''),
@@ -39,6 +41,7 @@ export function toSettingsBase(config) {
39
41
  ...(config.password !== undefined && config.password !== '' ? { password: config.password } : {}),
40
42
  ...(config.inboxFolder !== undefined && config.inboxFolder !== '' ? { inboxFolder: config.inboxFolder } : {}),
41
43
  ...(config.sendApproval !== undefined ? { sendApproval: config.sendApproval } : {}),
44
+ ...(config.trustedRecipientsYaml !== undefined ? { trustedRecipientsYaml: config.trustedRecipientsYaml } : {}),
42
45
  ...(config.maxBodyChars !== undefined ? { maxBodyChars: config.maxBodyChars } : {}),
43
46
  ...(config.downloadDir !== undefined && config.downloadDir !== '' ? { downloadDir: config.downloadDir } : {}),
44
47
  ...(config.serverPresets !== undefined && config.serverPresets !== '' ? { serverPresets: config.serverPresets } : {}),
@@ -122,6 +125,8 @@ export function toEmailConfig(value, user) {
122
125
  set('inboxFolder', draft.inboxFolder);
123
126
  if (has('sendApproval'))
124
127
  set('sendApproval', draft.sendApproval);
128
+ if (has('trustedRecipientsYaml'))
129
+ set('trustedRecipientsYaml', draft.trustedRecipientsYaml);
125
130
  if (has('maxBodyChars'))
126
131
  set('maxBodyChars', draft.maxBodyChars);
127
132
  if (has('downloadDir'))
@@ -198,6 +203,11 @@ function describeValue(value) {
198
203
  */
199
204
  export function validateSettingsValue(value, extraProviders = []) {
200
205
  const draft = (value ?? {});
206
+ if (draft.trustedRecipientsYaml !== undefined) {
207
+ if (typeof draft.trustedRecipientsYaml !== 'string')
208
+ throw new Error('可信收件人规则必须为文本');
209
+ parseRecipientPolicies(draft.trustedRecipientsYaml);
210
+ }
201
211
  const provider = draft.provider;
202
212
  if (isSet(provider) && provider !== '' && !PROVIDER_NAMES.includes(provider) && !extraProviders.includes(provider)) {
203
213
  const names = [...PROVIDER_NAMES, ...extraProviders];
package/lib/tools.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { EmailRuntime } from './runtime.js';
2
+ import type { EmailDraftBackend } from './draft-web.js';
2
3
  import { type TextBlock } from './tool-contract.js';
3
4
  export interface EmailToolDefinition {
4
5
  name: string;
@@ -11,4 +12,4 @@ export interface EmailToolDefinition {
11
12
  execute(args: unknown, exec?: unknown): Promise<unknown>;
12
13
  timeoutMs?: number;
13
14
  }
14
- export declare function buildEmailTools(runtime: Pick<EmailRuntime, 'getPool' | 'getEffectiveSettings' | 'watch'>): EmailToolDefinition[];
15
+ export declare function buildEmailTools(runtime: Pick<EmailRuntime, 'getPool' | 'getEffectiveSettings' | 'watch'>, drafts?: Pick<EmailDraftBackend, 'create'>): EmailToolDefinition[];
package/lib/tools.js CHANGED
@@ -5,9 +5,17 @@ import { descriptions, parameters, MAX_LIMIT, MARK_ACTIONS, REPLY_MODES, executi
5
5
  /** 单次工具调用的 deadline:普通查询 60s,正文扫描/watch 这类整批操作 120s。 */
6
6
  const QUERY_TIMEOUT_MS = 60000;
7
7
  const BATCH_TIMEOUT_MS = 120000;
8
- export function buildEmailTools(runtime) {
8
+ export function buildEmailTools(runtime, drafts) {
9
9
  const { getPool, getEffectiveSettings, watch: watchCore } = runtime;
10
10
  return [
11
+ ...(drafts ? [{
12
+ name: 'email_draft',
13
+ description: '准备可编辑的本地邮件草稿,不投递邮件。用户在设置 → 邮件 → 草稿中编辑、检查附件预览并明确确认后才发送。可在邮箱尚未配置时准备文字草稿;不用于回复原邮件线程。',
14
+ parameters: { type: 'object', properties: { account: { type: 'string', description: '发件账号名,省略时使用默认账号' }, to: { type: 'string', description: 'To 地址,支持显示名和地址组' }, cc: { type: 'string' }, subject: { type: 'string' }, text: { type: 'string', description: '纯文本正文' }, attachments: { type: 'array', items: { type: 'string' }, maxItems: 10, description: '本地附件路径,用户在预览中检查后才提交' } }, required: ['to', 'subject', 'text'], additionalProperties: false },
15
+ output: { schema: { type: 'object', properties: { id: { type: 'string' }, state: { type: 'string' }, subject: { type: 'string' } }, required: ['id', 'state', 'subject'] }, render: (_args, value) => [{ type: 'text', text: '本地草稿已保存,邮件未发送。请在设置 → 邮件 → 草稿中打开并检查。\n草稿编号:' + value.id + '\n主题:' + value.subject }] },
16
+ timeoutMs: QUERY_TIMEOUT_MS,
17
+ async execute(args, exec) { const draft = await drafts.create(args, executionSignal(exec)); return { id: draft.id, state: draft.state, subject: draft.subject }; },
18
+ }] : []),
11
19
  {
12
20
  name: 'email_list',
13
21
  description: descriptions.email_list,
package/lib/web.d.ts CHANGED
@@ -232,6 +232,30 @@ export declare class EmailSettingsBackend {
232
232
  credit: string;
233
233
  };
234
234
  }>;
235
+ saveRecipientRules(text: string, expectedRevision: number): Promise<{
236
+ settings: {
237
+ value: EmailSettingsValue;
238
+ revision: any;
239
+ applies: any;
240
+ };
241
+ writable: boolean;
242
+ accounts: string[];
243
+ accountsDetail: {
244
+ error?: string | undefined;
245
+ list: AccountCardData[];
246
+ defaultAccount?: string | undefined;
247
+ };
248
+ presets: {
249
+ custom: Record<string, ServerPreset>;
250
+ error?: string;
251
+ builtin: Record<string, ProviderPreset>;
252
+ };
253
+ whale: {
254
+ url: string;
255
+ skin: boolean;
256
+ credit: string;
257
+ };
258
+ }>;
235
259
  /**
236
260
  * Test one account (by name, defaulting to the draft's default account) over
237
261
  * a live IMAP login. Returns the endpoint it dialled so the panel can show
package/lib/web.js CHANGED
@@ -810,6 +810,9 @@ export class EmailSettingsBackend {
810
810
  }
811
811
  }
812
812
  async save(value, expectedRevision) {
813
+ // Rules have an explicit, separate save action. A stale account autosave
814
+ // must never restore an older trust policy after another tab changed it.
815
+ value = { ...value, trustedRecipientsYaml: this.scope.get().trustedRecipientsYaml ?? '' };
813
816
  if (this.ctx.settings.writable === false)
814
817
  throw new Error('settings provider is read-only');
815
818
  // The provider dropdown offers the custom preset names, so a value naming
@@ -847,6 +850,18 @@ export class EmailSettingsBackend {
847
850
  }
848
851
  return this.snapshot();
849
852
  }
853
+ async saveRecipientRules(text, expectedRevision) {
854
+ if (this.ctx.settings.writable === false)
855
+ throw new Error('settings provider is read-only');
856
+ if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
857
+ throw new Error('请重新加载规则后保存');
858
+ validateSettingsValue({ trustedRecipientsYaml: text });
859
+ if (this.scope.config)
860
+ await this.ctx.settings.update(this.namespace, { trustedRecipientsYaml: text }, expectedRevision);
861
+ else
862
+ await this.ctx.settings.replace(this.namespace, { ...this.scope.get(), trustedRecipientsYaml: text }, expectedRevision);
863
+ return this.snapshot();
864
+ }
850
865
  /**
851
866
  * Test one account (by name, defaulting to the draft's default account) over
852
867
  * a live IMAP login. Returns the endpoint it dialled so the panel can show
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-email",
3
- "version": "0.14.7",
4
- "description": "DSH 邮件插件:IMAP/SMTP 收发搜索、回复转发、附件与整理,多账号卡片设置页、Outlook OAuth2 登录与发信审批。",
3
+ "version": "0.15.0",
4
+ "description": "在 DSH 中收发、搜索和整理邮件,支持多个邮箱账号。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -20,7 +20,8 @@
20
20
  "cordis.patch.yml",
21
21
  "README.md",
22
22
  "README.en.md",
23
- "CHANGELOG.md"
23
+ "CHANGELOG.md",
24
+ "docs"
24
25
  ],
25
26
  "scripts": {
26
27
  "build": "tsc",
@@ -61,15 +62,17 @@
61
62
  }
62
63
  },
63
64
  "dependencies": {
65
+ "@deepseek-ai/schemastery": "^3.18.3",
64
66
  "imapflow": "^2.2.1",
65
67
  "mailparser": "^3.9.33",
66
68
  "nodemailer": "^10.0.13",
69
+ "proper-lockfile": "4.1.2",
67
70
  "schemastery": "^3.18.0",
68
- "yaml": "^2.9.0",
69
- "@deepseek-ai/schemastery": "^3.18.3"
71
+ "yaml": "^2.9.0"
70
72
  },
71
73
  "devDependencies": {
72
74
  "@types/node": "^26.2.0",
75
+ "@types/proper-lockfile": "4.1.4",
73
76
  "typescript": "^5.7.3"
74
77
  }
75
78
  }