dsh-email 0.3.2 → 0.3.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.md CHANGED
@@ -96,7 +96,7 @@ dsh plugin --profile web add dsh-email
96
96
  | `provider` | 无 | 预设名,自动填 imap/smtp 地址;显式写的 host/port/secure 优先 |
97
97
  | `user` | 必填 | 登录邮箱地址 |
98
98
  | `password` | 必填* | 授权码/应用专用密码;*也可用环境变量 `DSH_EMAIL_PASSWORD` |
99
- | `imap.host/port/secure` | 按预设 | 收信服务器 |
99
+ | `imap.host/port/secure` | 按预设 | 收信服务器(另有 connectionTimeoutMs/socketTimeoutMs 可调超时) |
100
100
  | `smtp.host/port/secure` | 按预设 | 发信服务器 |
101
101
  | `inboxFolder` | `INBOX` | 收发工具默认使用的文件夹 |
102
102
  | `sendApproval` | `true` | 发信前弹确认(强烈建议保留) |
@@ -118,25 +118,27 @@ dsh plugin --profile web add dsh-email
118
118
 
119
119
  ## 安全须知
120
120
 
121
- - **授权码就是你的邮箱钥匙**。它写在 profile 的 `cordis.patch.yml` 里,请勿把它提交到任何 Git 仓库;更推荐用环境变量 `DSH_EMAIL_PASSWORD`。
121
+ - **授权码就是你的邮箱钥匙**。它写在本机(profile 的 `cordis.patch.yml` `settings.yaml`),请勿提交到任何 Git 仓库;更推荐用环境变量 `DSH_EMAIL_PASSWORD`。
122
122
  - `email_send` 默认走 DSH 审批通道:每次发信都显示「发送邮件给 xx,主题「xx」」,你批准才发出。没有审批通道的环境(如无 UI 的 headless)会**直接拒绝发信**,这是安全默认。
123
123
  - 注意:会话处于 **Full Access(完全访问)** 模式时,harness 会把审批策略置为 never,`email_send` 会被**静默拒绝**(不弹框)。想发信请把访问模式切回 Read Only 或 Write。
124
124
  - 本插件不做任何联网上报,凭证只在内存中用于连接你的邮箱服务器。
125
125
 
126
- ## 已知限制(v0.2
126
+ ## 已知限制(v0.3
127
127
 
128
128
  - **连接复用**:IMAP 按账号池化(空闲自动回收),SMTP 用 nodemailer 连接池;同一账号的并发调用会排队串行(一个连接一次只服务一个操作,这是有意的)。
129
129
  - **多账号**:每个账号独立连接池;一个 `tool-email` 行可以配任意多个账号。设置页编辑的是默认账号;`accounts` 映射仍需写 cordis.patch.yml。
130
- - **附件下载**:email_attachment 按序号下载(与 email_read attachments 顺序一致);文件名会被清洗防路径穿越,已有同名文件自动加后缀,大小受 maxAttachmentBytes 限制。
130
+ - **附件下载**:email_attachment email_read 的附件列表定位(先按文件名、再按类型+大小匹配到 IMAP 部件,定位失败会报错而不是下载错文件);内嵌图片暂不支持下载;文件名会被清洗防路径穿越,已有同名文件自动加后缀,大小受 maxAttachmentBytes 限制。
131
131
  - **不支持 OAuth2**:强制 OAuth 的企业环境(部分 M365/Google Workspace)暂不可用。
132
- - 正文搜索不提供:多数服务器(如 QQ)的 IMAP `TEXT`/`HEADER` 搜索要么全量匹配要么不支持,所以只搜主题/发件人/收件人;正文搜索列入后续版本(需客户端下载解析,较慢)。
132
+ - 正文搜索不提供:多数服务器(如 QQ)的 IMAP `TEXT`/`HEADER` 搜索要么全量匹配要么不支持,所以只搜主题/发件人/收件人;正文搜索列入后续版本(需客户端下载解析,较慢)。
133
+ - **密码落盘形式**:设置页保存的授权码以明文写在本机 `settings.yaml`(schema 标记 secret 只是保证它不进日志/导出/诊断,不做磁盘加密)。请勿把 settings.yaml 交给不信任的人。
134
+ - **设置页与插件集变更**:设置页保存后工具**立即生效**(live),无需重启;但升级/增删插件(组合树变化)仍需重启 `dsh web`。
133
135
 
134
136
  ## 开发
135
137
 
136
138
  ```sh
137
139
  pnpm install
138
140
  pnpm run build # tsc → lib/
139
- pnpm test # 构建 + node --test(配置/解析/注册与审批门,31 个用例,无需真实邮箱)
141
+ pnpm test # 构建 + node --test(配置/解析/注册与审批门,34 个用例,无需真实邮箱)
140
142
  ```
141
143
 
142
144
  ## 协议
package/cordis.patch.yml CHANGED
@@ -1,5 +1,8 @@
1
1
  # dsh-email: IMAP/SMTP email tools for DeepSeek Harness.
2
2
  #
3
+ # 配置推荐走 Web 设置页:设置 → 邮件 (dsh-email),表单填写即生效;
4
+ # 下面的 YAML 方式用于 headless 或多账号(accounts 映射)场景。
5
+ #
3
6
  # The row ships with an EMPTY config so installing the bundle never breaks
4
7
  # boot; each email_* tool reports a configuration hint until you fill the
5
8
  # account. Configure it in your profile's cordis.patch.yml by overriding this
@@ -23,4 +26,4 @@
23
26
  - insert:
24
27
  - id: tool-email
25
28
  name: dsh-email
26
- config: {}
29
+ config: {}
package/lib/client.js CHANGED
@@ -113,6 +113,24 @@ function EmailSettingsSection() {
113
113
  useEffect(() => { load(); }, [load]);
114
114
 
115
115
  const update = (patch) => setDraft((cur) => Object.assign({}, cur, patch));
116
+
117
+ const PORT_PRESETS = {
118
+ qq: [993, 465], "163": [993, 465], "126": [993, 465], sina: [993, 465],
119
+ aliyun: [993, 465], gmail: [993, 465], outlook: [993, 587], icloud: [993, 587],
120
+ };
121
+
122
+ const onProvider = (value) => {
123
+ const preset = PORT_PRESETS[value];
124
+ if (preset) {
125
+ setDraft((cur) => Object.assign({}, cur, {
126
+ provider: value,
127
+ imap: Object.assign({}, cur.imap, { port: preset[0], secure: true }),
128
+ smtp: Object.assign({}, cur.smtp, { port: preset[1], secure: preset[1] === 465 }),
129
+ }));
130
+ } else {
131
+ update({ provider: value });
132
+ }
133
+ };
116
134
  const updateImap = (patch) => setDraft((cur) => Object.assign({}, cur, { imap: Object.assign({}, cur.imap, patch) }));
117
135
  const updateSmtp = (patch) => setDraft((cur) => Object.assign({}, cur, { smtp: Object.assign({}, cur.smtp, patch) }));
118
136
 
@@ -174,7 +192,7 @@ function EmailSettingsSection() {
174
192
  h("div", { className: "dshe-grid" }, [
175
193
  h("div", { className: "dshe-field" }, [
176
194
  h("label", null, "邮箱服务商"),
177
- h("select", { value: draft.provider, onChange: (e) => update({ provider: e.target.value }) },
195
+ h("select", { value: draft.provider, onChange: (e) => onProvider(e.target.value) },
178
196
  PROVIDERS.map(([value, label]) => h("option", { key: value, value }, label))),
179
197
  ]),
180
198
  h("div", { className: "dshe-field" }, [
package/lib/index.d.ts CHANGED
@@ -6,4 +6,6 @@ export declare function apply(ctx: any, config?: Config): void;
6
6
  export { PROVIDER_NAMES, EMAIL_PASSWORD_ENV } from './config.js';
7
7
  export { resolveEmailConfig, resolveEmailSettings, clampInt, defaultDownloadDir } from './config.js';
8
8
  export { stripHtml, truncateText, flattenAddresses, sanitizeFilename, parseRawMessage } from './parse.js';
9
- export { EmailPool, MailError, messageOf, validateAttachmentPaths } from './mail-client.js';
9
+ export { EmailPool, MailError, messageOf, validateAttachmentPaths, selectAttachmentPart } from './mail-client.js';
10
+ export { SETTINGS_NAMESPACE, EmailSettingsSchema, toSettingsBase, toEmailConfig, validateSettingsValue } from './settings.js';
11
+ export { SETTINGS_ROUTE, EmailSettingsBackend, installEmailSettingsWeb } from './web.js';
package/lib/index.js CHANGED
@@ -187,8 +187,12 @@ export function apply(ctx, config = {}) {
187
187
  let pool = null;
188
188
  let poolFingerprint = '';
189
189
  const getPool = () => {
190
+ // Only fields the user actually set in the settings page override the row
191
+ // config; untouched fields fall back to the row and the provider presets.
192
+ const descriptor = (ctx.settings.describe?.() ?? []).find((row) => row.ns === SETTINGS_NAMESPACE);
193
+ const userSection = descriptor?.user;
190
194
  const value = settingsScope.get();
191
- const effective = resolveEmailSettings({ ...config, ...toEmailConfig(value) });
195
+ const effective = resolveEmailSettings({ ...config, ...toEmailConfig(value, userSection) });
192
196
  const fp = fingerprintSettings(effective);
193
197
  if (pool === null || fp !== poolFingerprint) {
194
198
  pool?.dispose();
@@ -340,11 +344,13 @@ export function apply(ctx, config = {}) {
340
344
  ctx.on('tools/pre-execute', async (exec, next) => {
341
345
  if (exec?.name !== 'email_send')
342
346
  return next();
347
+ const descriptor = (ctx.settings.describe?.() ?? []).find((row) => row.ns === SETTINGS_NAMESPACE);
348
+ const userSection = descriptor?.user;
343
349
  const value = settingsScope.get();
344
350
  if (value.sendApproval === false)
345
351
  return next();
346
352
  try {
347
- resolveEmailSettings({ ...config, ...toEmailConfig(value) });
353
+ resolveEmailSettings({ ...config, ...toEmailConfig(value, userSection) });
348
354
  }
349
355
  catch {
350
356
  return next(); // unconfigured: let the tool report the actionable hint
@@ -358,4 +364,6 @@ export function apply(ctx, config = {}) {
358
364
  export { PROVIDER_NAMES, EMAIL_PASSWORD_ENV } from './config.js';
359
365
  export { resolveEmailConfig, resolveEmailSettings, clampInt, defaultDownloadDir } from './config.js';
360
366
  export { stripHtml, truncateText, flattenAddresses, sanitizeFilename, parseRawMessage } from './parse.js';
361
- export { EmailPool, MailError, messageOf, validateAttachmentPaths } from './mail-client.js';
367
+ export { EmailPool, MailError, messageOf, validateAttachmentPaths, selectAttachmentPart } from './mail-client.js';
368
+ export { SETTINGS_NAMESPACE, EmailSettingsSchema, toSettingsBase, toEmailConfig, validateSettingsValue } from './settings.js';
369
+ export { SETTINGS_ROUTE, EmailSettingsBackend, installEmailSettingsWeb } from './web.js';
@@ -1,10 +1,23 @@
1
1
  import { ImapFlow } from 'imapflow';
2
2
  import type { ResolvedEmailConfig, ResolvedEmailSettings } from './config.js';
3
- import type { EmailAttachmentResult, EmailFoldersResult, EmailListResult, EmailReadResult, EmailSearchResult, EmailSendResult } from './types.js';
3
+ import type { EmailAttachmentMeta, EmailAttachmentResult, EmailFoldersResult, EmailListResult, EmailReadResult, EmailSearchResult, EmailSendResult } from './types.js';
4
4
  export declare class MailError extends Error {
5
5
  constructor(message: string);
6
6
  }
7
7
  export declare function messageOf(error: unknown, fallback: string): string;
8
+ interface AttachmentPart {
9
+ part: string;
10
+ filename: string;
11
+ contentType: string;
12
+ size: number;
13
+ }
14
+ /**
15
+ * Map the index in the mailparser attachment list (what email_read showed the
16
+ * model) onto a bodyStructure part. Name first, then type + tolerant size;
17
+ * an inline image that our walk excludes simply fails instead of downloading
18
+ * the wrong part.
19
+ */
20
+ export declare function selectAttachmentPart(readAttachments: EmailAttachmentMeta[], parts: AttachmentPart[], index: number): AttachmentPart | undefined;
8
21
  /**
9
22
  * One mailbox pool for the whole plugin: pooled IMAP connections per
10
23
  * account plus pooled SMTP transporters, with idle sweep and error eviction.
@@ -41,3 +54,4 @@ export declare class EmailPool {
41
54
  export declare function validateAttachmentPaths(paths: string[], maxBytes: number): Promise<Array<{
42
55
  path: string;
43
56
  }>>;
57
+ export {};
@@ -40,6 +40,23 @@ function collectAttachmentParts(node, out = []) {
40
40
  collectAttachmentParts(child, out);
41
41
  return out;
42
42
  }
43
+ /**
44
+ * Map the index in the mailparser attachment list (what email_read showed the
45
+ * model) onto a bodyStructure part. Name first, then type + tolerant size;
46
+ * an inline image that our walk excludes simply fails instead of downloading
47
+ * the wrong part.
48
+ */
49
+ export function selectAttachmentPart(readAttachments, parts, index) {
50
+ const meta = readAttachments[index];
51
+ if (meta === undefined)
52
+ return undefined;
53
+ const byName = parts.find(part => part.filename === meta.filename || sanitizeFilename(part.filename) === meta.filename);
54
+ if (byName !== undefined)
55
+ return byName;
56
+ const tolerance = Math.max(64, Math.ceil(meta.size * 0.5));
57
+ const byTypeAndSize = parts.find(part => part.contentType === meta.contentType && Math.abs(part.size - meta.size) <= tolerance);
58
+ return byTypeAndSize;
59
+ }
43
60
  function toIso(date) {
44
61
  return date instanceof Date ? date.toISOString() : '';
45
62
  }
@@ -280,23 +297,29 @@ export class EmailPool {
280
297
  const cfg = this.account(name);
281
298
  const folderName = folder || cfg.inboxFolder;
282
299
  return this.withImap(name, folderName, async (client) => {
283
- const message = await client.fetchOne(uid, { uid: true, bodyStructure: true }, { uid: true });
284
- if (message === false) {
300
+ const message = await client.fetchOne(uid, { uid: true, bodyStructure: true, source: true }, { uid: true });
301
+ if (message === false || message.source === undefined) {
285
302
  throw new MailError('找不到 uid=' + uid + ' 的邮件(可能已被删除,或不在文件夹 "' + folderName + '")');
286
303
  }
287
- const attachments = collectAttachmentParts(message.bodyStructure);
288
- if (attachments.length === 0)
304
+ // The mailparser list is authoritative for the index email_read showed;
305
+ // the bodyStructure walk supplies the IMAP part to download.
306
+ const body = await parseRawMessage(message.source, this.settings.maxBodyChars);
307
+ const parts = collectAttachmentParts(message.bodyStructure);
308
+ if (body.attachments.length === 0)
289
309
  throw new MailError('该邮件没有附件');
290
- const att = attachments[index];
310
+ if (body.attachments[index] === undefined) {
311
+ throw new MailError('附件序号 ' + index + ' 越界:共 ' + body.attachments.length + ' 个附件(序号从 0 开始,与 email_read 返回的 attachments 顺序一致)');
312
+ }
313
+ const att = selectAttachmentPart(body.attachments, parts, index);
291
314
  if (att === undefined) {
292
- throw new MailError('附件序号 ' + index + ' 越界:共 ' + attachments.length + ' 个附件(序号从 0 开始,与 email_read 返回的 attachments 顺序一致)');
315
+ throw new MailError('附件 #' + index + '' + body.attachments[index].filename + ')无法在邮件结构中定位(可能是内嵌图片,暂不支持下载)');
293
316
  }
294
317
  if (att.size > this.settings.maxAttachmentBytes) {
295
318
  throw new MailError('附件 "' + att.filename + '" 大小 ' + att.size + ' 字节,超过上限 maxAttachmentBytes=' + this.settings.maxAttachmentBytes);
296
319
  }
297
320
  const dl = await client.download(uid, att.part, { uid: true, maxBytes: this.settings.maxAttachmentBytes });
298
321
  const buf = await collectStream(dl.content, this.settings.maxAttachmentBytes);
299
- const safeName = sanitizeFilename(dl.meta.filename ?? att.filename);
322
+ const safeName = sanitizeFilename(dl.meta.filename ?? att.filename ?? body.attachments[index].filename);
300
323
  const dir = this.settings.downloadDir;
301
324
  await mkdir(dir, { recursive: true });
302
325
  const dest = await uniquePath(join(dir, safeName));
package/lib/parse.js CHANGED
@@ -47,7 +47,6 @@ export function truncateText(text, maxChars) {
47
47
  */
48
48
  export function sanitizeFilename(raw, fallback = 'attachment.bin') {
49
49
  let name = String(raw ?? '').replace(/\\/g, '/').split('/').pop() ?? '';
50
- // eslint-disable-next-line no-control-regex
51
50
  name = name.replace(/[\u0000-\u001f\u007f]/g, '').trim();
52
51
  if (name === '' || name === '.' || name === '..')
53
52
  name = fallback;
package/lib/settings.d.ts CHANGED
@@ -81,8 +81,15 @@ export interface EmailSettingsValue {
81
81
  }
82
82
  /** Project the row config (cordis.patch.yml) into the settings-schema base shape. */
83
83
  export declare function toSettingsBase(config: EmailConfig): Partial<EmailSettingsValue>;
84
- /** Project a settings value back into EmailConfig shape ('' becomes unset). */
85
- export declare function toEmailConfig(value: EmailSettingsValue): EmailConfig;
84
+ /**
85
+ * Project a settings value back into EmailConfig shape.
86
+ *
87
+ * `user` is the raw stored user section: only fields the user actually set
88
+ * are projected, so schema defaults never shadow the row config or the
89
+ * provider presets (choosing outlook must NOT force smtp port 465 over the
90
+ * preset's 587). Pass `null` to project every field (draft paths).
91
+ */
92
+ export declare function toEmailConfig(value: EmailSettingsValue, user?: Partial<EmailSettingsValue> | null): EmailConfig;
86
93
  /**
87
94
  * Gentle write-path validation: structural mistakes fail loudly, but an
88
95
  * incomplete account is allowed (tools report the actionable hint at call
package/lib/settings.js CHANGED
@@ -52,27 +52,54 @@ export function toSettingsBase(config) {
52
52
  } : {}),
53
53
  };
54
54
  }
55
- /** Project a settings value back into EmailConfig shape ('' becomes unset). */
56
- export function toEmailConfig(value) {
57
- return {
58
- ...(value.provider !== '' ? { provider: value.provider } : {}),
59
- ...(value.user !== '' ? { user: value.user } : {}),
60
- ...(value.password !== '' ? { password: value.password } : {}),
61
- ...(value.inboxFolder !== '' && value.inboxFolder !== 'INBOX' ? { inboxFolder: value.inboxFolder } : {}),
62
- sendApproval: value.sendApproval,
63
- maxBodyChars: value.maxBodyChars,
64
- ...(value.downloadDir !== '' ? { downloadDir: value.downloadDir } : {}),
65
- imap: {
66
- ...(value.imap.host !== '' ? { host: value.imap.host } : {}),
67
- port: value.imap.port,
68
- secure: value.imap.secure,
69
- },
70
- smtp: {
71
- ...(value.smtp.host !== '' ? { host: value.smtp.host } : {}),
72
- port: value.smtp.port,
73
- secure: value.smtp.secure,
74
- },
75
- };
55
+ /**
56
+ * Project a settings value back into EmailConfig shape.
57
+ *
58
+ * `user` is the raw stored user section: only fields the user actually set
59
+ * are projected, so schema defaults never shadow the row config or the
60
+ * provider presets (choosing outlook must NOT force smtp port 465 over the
61
+ * preset's 587). Pass `null` to project every field (draft paths).
62
+ */
63
+ export function toEmailConfig(value, user) {
64
+ const has = (key) => user === null || user?.[key] !== undefined;
65
+ const out = {};
66
+ if (has('provider'))
67
+ out.provider = value.provider === '' ? undefined : value.provider;
68
+ if (has('user'))
69
+ out.user = value.user;
70
+ if (has('password'))
71
+ out.password = value.password;
72
+ if (has('inboxFolder'))
73
+ out.inboxFolder = value.inboxFolder;
74
+ if (has('sendApproval'))
75
+ out.sendApproval = value.sendApproval;
76
+ if (has('maxBodyChars'))
77
+ out.maxBodyChars = value.maxBodyChars;
78
+ if (has('downloadDir'))
79
+ out.downloadDir = value.downloadDir;
80
+ if (user === null || user?.imap !== undefined) {
81
+ const fields = user === null ? value.imap : (user.imap ?? {});
82
+ const imap = {};
83
+ if (fields.host !== undefined)
84
+ imap.host = value.imap.host;
85
+ if (fields.port !== undefined)
86
+ imap.port = value.imap.port;
87
+ if (fields.secure !== undefined)
88
+ imap.secure = value.imap.secure;
89
+ out.imap = imap;
90
+ }
91
+ if (user === null || user?.smtp !== undefined) {
92
+ const fields = user === null ? value.smtp : (user.smtp ?? {});
93
+ const smtp = {};
94
+ if (fields.host !== undefined)
95
+ smtp.host = value.smtp.host;
96
+ if (fields.port !== undefined)
97
+ smtp.port = value.smtp.port;
98
+ if (fields.secure !== undefined)
99
+ smtp.secure = value.smtp.secure;
100
+ out.smtp = smtp;
101
+ }
102
+ return out;
76
103
  }
77
104
  /**
78
105
  * Gentle write-path validation: structural mistakes fail loudly, but an
package/lib/web.d.ts CHANGED
@@ -11,7 +11,9 @@ export declare class EmailSettingsBackend {
11
11
  private readonly scope;
12
12
  private readonly rowConfig;
13
13
  constructor(ctx: any, scope: any, rowConfig: EmailConfig);
14
- private effective;
14
+ private userSection;
15
+ /** Effective config for the stored value (row + user-set fields only). */
16
+ private effectiveStored;
15
17
  snapshot(): Promise<{
16
18
  settings: {
17
19
  value: EmailSettingsValue;
package/lib/web.js CHANGED
@@ -1,11 +1,8 @@
1
1
  import { SETTINGS_NAMESPACE, toEmailConfig, validateSettingsValue } from './settings.js';
2
2
  import { resolveEmailSettings } from './config.js';
3
- import { EmailPool } from './mail-client.js';
3
+ import { EmailPool, messageOf } from './mail-client.js';
4
4
  /** Same-origin route the browser settings section talks to. */
5
5
  export const SETTINGS_ROUTE = '/_dsh/dsh-email/settings';
6
- function messageOf2(error) {
7
- return error instanceof Error ? error.message : String(error);
8
- }
9
6
  /**
10
7
  * Browser-facing backend: snapshot the settings namespace, save it with
11
8
  * optimistic concurrency, and test a draft account over a live IMAP login.
@@ -19,8 +16,13 @@ export class EmailSettingsBackend {
19
16
  this.scope = scope;
20
17
  this.rowConfig = rowConfig;
21
18
  }
22
- effective(value) {
23
- return { ...this.rowConfig, ...toEmailConfig(value) };
19
+ userSection() {
20
+ const descriptor = (this.ctx.settings.describe?.() ?? []).find((row) => row.ns === SETTINGS_NAMESPACE);
21
+ return descriptor?.user;
22
+ }
23
+ /** Effective config for the stored value (row + user-set fields only). */
24
+ effectiveStored() {
25
+ return { ...this.rowConfig, ...toEmailConfig(this.scope.get(), this.userSection()) };
24
26
  }
25
27
  async snapshot() {
26
28
  const descriptor = (this.ctx.settings.describe?.() ?? []).find((row) => row.ns === SETTINGS_NAMESPACE);
@@ -37,7 +39,7 @@ export class EmailSettingsBackend {
37
39
  }
38
40
  effectiveAccounts() {
39
41
  try {
40
- return resolveEmailSettings(this.effective(this.scope.get())).accounts;
42
+ return resolveEmailSettings(this.effectiveStored()).accounts;
41
43
  }
42
44
  catch {
43
45
  return new Map();
@@ -52,7 +54,8 @@ export class EmailSettingsBackend {
52
54
  }
53
55
  async test(value) {
54
56
  validateSettingsValue(value);
55
- const settings = resolveEmailSettings(this.effective(value));
57
+ // null projects the complete draft: test the form as the user typed it.
58
+ const settings = resolveEmailSettings({ ...this.rowConfig, ...toEmailConfig(value, null) });
56
59
  const pool = new EmailPool(settings);
57
60
  try {
58
61
  const started = Date.now();
@@ -77,7 +80,7 @@ export class EmailSettingsBackend {
77
80
  this.responseJson(res, 200, { ok: true, value: await this.snapshot() });
78
81
  }
79
82
  catch (error) {
80
- this.responseJson(res, 503, { ok: false, error: { code: 'unavailable', message: messageOf2(error) } });
83
+ this.responseJson(res, 503, { ok: false, error: { code: 'unavailable', message: messageOf(error, 'unknown error') } });
81
84
  }
82
85
  return;
83
86
  }
@@ -98,7 +101,7 @@ export class EmailSettingsBackend {
98
101
  body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
99
102
  }
100
103
  catch (error) {
101
- this.responseJson(res, 400, { ok: false, error: { code: 'invalid-request', message: messageOf2(error) } });
104
+ this.responseJson(res, 400, { ok: false, error: { code: 'invalid-request', message: messageOf(error, 'unknown error') } });
102
105
  return;
103
106
  }
104
107
  try {
@@ -118,7 +121,7 @@ export class EmailSettingsBackend {
118
121
  const conflict = error?.code === 'SETTINGS_CONFLICT';
119
122
  this.responseJson(res, conflict ? 409 : 400, {
120
123
  ok: false,
121
- error: { code: conflict ? 'settings-conflict' : 'rejected', message: messageOf2(error) },
124
+ error: { code: conflict ? 'settings-conflict' : 'rejected', message: messageOf(error, 'unknown error') },
122
125
  });
123
126
  }
124
127
  }
package/package.json CHANGED
@@ -1,73 +1,73 @@
1
- {
2
- "name": "dsh-email",
3
- "version": "0.3.2",
4
- "description": "IMAP/SMTP email tools for DeepSeek Harness: list, read, search and send mail, with QQ/163/126/Sina/Aliyun/Gmail/Outlook/iCloud presets.",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/index.d.ts",
11
- "default": "./lib/index.js"
12
- },
13
- "./client": "./lib/client.js",
14
- "./cordis.patch.yml": "./cordis.patch.yml",
15
- "./package.json": "./package.json"
16
- },
17
- "files": [
18
- "lib",
19
- "cordis.patch.yml",
20
- "README.md"
21
- ],
22
- "scripts": {
23
- "build": "tsc",
24
- "prepare": "tsc",
25
- "prepublishOnly": "pnpm run build",
26
- "test": "pnpm run build && node --test \"test/*.test.mjs\""
27
- },
28
- "keywords": [
29
- "dsh-plugin",
30
- "deepseek-harness",
31
- "dsh",
32
- "email",
33
- "imap",
34
- "smtp"
35
- ],
36
- "author": "stardustlc",
37
- "repository": {
38
- "type": "git",
39
- "url": "https://github.com/STARDUSTLC666/dsh-email"
40
- },
41
- "bugs": {
42
- "url": "https://github.com/STARDUSTLC666/dsh-email/issues"
43
- },
44
- "homepage": "https://github.com/STARDUSTLC666/dsh-email#readme",
45
- "license": "MIT",
46
- "packageManager": "pnpm@11.7.0",
47
- "engines": {
48
- "node": ">=20"
49
- },
50
- "dsh": {
51
- "bundle": {
52
- "patch": "./cordis.patch.yml"
53
- },
54
- "client": {
55
- "inject": [
56
- "@deepseek-ai/dsh-client-runtime",
57
- "@deepseek-ai/dsh-client-ui-settings"
58
- ],
59
- "platform": "web"
60
- }
61
- },
62
- "dependencies": {
63
- "schemastery": "^3.18.0",
64
- "imapflow": "^1.7.0",
65
- "mailparser": "^3.9.15",
66
- "nodemailer": "^9.0.5"
67
- },
68
- "devDependencies": {
69
- "@types/node": "^26.2.0",
70
- "@types/nodemailer": "^8.0.1",
71
- "typescript": "^5.7.3"
72
- }
73
- }
1
+ {
2
+ "name": "dsh-email",
3
+ "version": "0.3.3",
4
+ "description": "IMAP/SMTP email tools for DeepSeek Harness: list, read, search and send mail, with QQ/163/126/Sina/Aliyun/Gmail/Outlook/iCloud presets.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./client": "./lib/client.js",
14
+ "./cordis.patch.yml": "./cordis.patch.yml",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "lib",
19
+ "cordis.patch.yml",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "prepare": "tsc",
25
+ "prepublishOnly": "pnpm run build",
26
+ "test": "pnpm run build && node --test \"test/*.test.mjs\""
27
+ },
28
+ "keywords": [
29
+ "dsh-plugin",
30
+ "deepseek-harness",
31
+ "dsh",
32
+ "email",
33
+ "imap",
34
+ "smtp"
35
+ ],
36
+ "author": "stardustlc",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/STARDUSTLC666/dsh-email"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/STARDUSTLC666/dsh-email/issues"
43
+ },
44
+ "homepage": "https://github.com/STARDUSTLC666/dsh-email#readme",
45
+ "license": "MIT",
46
+ "packageManager": "pnpm@11.7.0",
47
+ "engines": {
48
+ "node": ">=20"
49
+ },
50
+ "dsh": {
51
+ "bundle": {
52
+ "patch": "./cordis.patch.yml"
53
+ },
54
+ "client": {
55
+ "inject": [
56
+ "@deepseek-ai/dsh-client-runtime",
57
+ "@deepseek-ai/dsh-client-ui-settings"
58
+ ],
59
+ "platform": "web"
60
+ }
61
+ },
62
+ "dependencies": {
63
+ "schemastery": "^3.18.0",
64
+ "imapflow": "^1.7.0",
65
+ "mailparser": "^3.9.15",
66
+ "nodemailer": "^9.0.5"
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "^26.2.0",
70
+ "@types/nodemailer": "^8.0.1",
71
+ "typescript": "^5.7.3"
72
+ }
73
+ }