dsh-email 0.3.1 → 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 +8 -6
- package/cordis.patch.yml +4 -1
- package/lib/client.js +19 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.js +11 -3
- package/lib/mail-client.d.ts +15 -1
- package/lib/mail-client.js +30 -7
- package/lib/parse.js +0 -1
- package/lib/settings.d.ts +9 -2
- package/lib/settings.js +48 -21
- package/lib/web.d.ts +3 -1
- package/lib/web.js +14 -11
- package/package.json +73 -66
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
|
-
-
|
|
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.
|
|
126
|
+
## 已知限制(v0.3)
|
|
127
127
|
|
|
128
128
|
- **连接复用**:IMAP 按账号池化(空闲自动回收),SMTP 用 nodemailer 连接池;同一账号的并发调用会排队串行(一个连接一次只服务一个操作,这是有意的)。
|
|
129
129
|
- **多账号**:每个账号独立连接池;一个 `tool-email` 行可以配任意多个账号。设置页编辑的是默认账号;`accounts` 映射仍需写 cordis.patch.yml。
|
|
130
|
-
- **附件下载**:email_attachment
|
|
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(配置/解析/注册与审批门,
|
|
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) =>
|
|
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';
|
package/lib/mail-client.d.ts
CHANGED
|
@@ -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 {};
|
package/lib/mail-client.js
CHANGED
|
@@ -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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
/**
|
|
85
|
-
|
|
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
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
124
|
+
error: { code: conflict ? 'settings-conflict' : 'rejected', message: messageOf(error, 'unknown error') },
|
|
122
125
|
});
|
|
123
126
|
}
|
|
124
127
|
}
|
package/package.json
CHANGED
|
@@ -1,66 +1,73 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-email",
|
|
3
|
-
"version": "0.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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|