dsh-email 0.14.0 → 0.14.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/CHANGELOG.md +14 -0
- package/README.en.md +7 -1
- package/README.md +7 -1
- package/lib/client.js +36 -15
- package/lib/config.d.ts +15 -1
- package/lib/config.js +46 -13
- package/lib/settings.js +13 -11
- package/lib/web.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.3(2026-09-28)
|
|
4
|
+
|
|
5
|
+
- 兼容验证更新到 Harness 0.2.0-rc.1:282 项邮件测试与 18 插件共同加载检查通过。
|
|
6
|
+
- 运行时代码未变;同步中英文兼容性声明。
|
|
7
|
+
|
|
8
|
+
## 0.14.2(2026-09-28)
|
|
9
|
+
|
|
10
|
+
- 修复 [#17](https://github.com/STARDUSTLC666/dsh-email/issues/17):空主机配合未修改的表单默认端口/TLS 不再覆盖服务商预设;Outlook 恢复 587/STARTTLS。旧设置转换和 Harness 0.1.7 已迁移配置共用归一化规则,保留自定义主机、非默认组合和不含占位 host 的显式端口覆盖。
|
|
11
|
+
- 感谢 [SenkjM 的 PR #18](https://github.com/STARDUSTLC666/dsh-email/pull/18) 提供复现与修复方向。本次补齐新版运行时路径并避免误删明确的端口设置;282 项邮件测试通过。配置和界面验证不代表真实 SMTP 投递已验证。
|
|
12
|
+
|
|
13
|
+
## 0.14.1(2026-09-27)
|
|
14
|
+
|
|
15
|
+
- 未补全的账号和服务器预设会显示未保存提示;避免草稿丢失和错误的“已保存”状态。测试连接只校验所选账号,错误直接指向设置页中的对应字段。
|
|
16
|
+
|
|
3
17
|
> 完整历史(含详细改动说明)。README 只保留最近几个版本的一句话摘要。
|
|
4
18
|
|
|
5
19
|
## 中文版
|
package/README.en.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# dsh-email
|
|
2
2
|
|
|
3
|
+
## 0.14.2 update (2026-09-28)
|
|
4
|
+
|
|
5
|
+
Fixes saved blank-host defaults overriding Outlook's 587/STARTTLS preset, including legacy settings migrated to Harness 0.1.7. Explicit custom hosts, ports and TLS choices remain effective. Thanks to [SenkjM for the reproduction and proposed fix in #17 / #18](https://github.com/STARDUSTLC666/dsh-email/pull/18).
|
|
6
|
+
|
|
7
|
+
Validation host: Harness `0.2.0-rc.1` built from official sources (commit `407e65c8`) with Node `24.16.0` on 2026-09-28. All 282 plugin tests pass in an isolated environment; all 18 plugins mount together in one host registering 10 tools, with tool schemas and health-check contracts passing. No live ports or external services were exercised in this round.
|
|
8
|
+
|
|
3
9
|
   
|
|
4
10
|
|
|
5
11
|
[](https://awesome-dsh-plugin.com)
|
|
@@ -47,7 +53,7 @@ Example:
|
|
|
47
53
|
- **0.10.8 and earlier**: see [CHANGELOG.md](CHANGELOG.md).
|
|
48
54
|
## Compatibility
|
|
49
55
|
|
|
50
|
-
The current baseline is official-source Harness **0.
|
|
56
|
+
The current baseline is official-source Harness **0.2.0-rc.1** (2026-09-28, with a local `Symbol.for` tool-scheduler fix). All 18 plugins load together. Settings coverage includes legacy migration, real Web UI editing and automatic saving, refresh, revision conflicts and restart persistence with isolated fixture accounts. No live mailbox connection or sending was exercised in this round.
|
|
51
57
|
|
|
52
58
|
2026-09-21: the current release package was installed through the official CLI in an isolated profile and co-loaded with the other two most-downloaded plugins on source-built Harness `0.1.6-alpha.2`. All 18 plugin tools registered; calendar/email configuration checks, PPT theme listing and 17-row table generation passed. The host is based on the official alpha.2 release plus the tool-scheduler `Symbol.for` fix (`93badd88`). This run did not connect to live mail or calendar services.
|
|
53
59
|
|
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# dsh-email
|
|
4
4
|
|
|
5
|
+
## 0.14.2 更新(2026-09-28)
|
|
6
|
+
|
|
7
|
+
修复保存过的空服务器默认值覆盖 Outlook 预设的问题:恢复 `587 / STARTTLS`,并覆盖 Harness 0.1.7 的旧配置迁移路径。明确设置的自定义主机、端口和 TLS 仍然生效。感谢 [SenkjM 在 #17 / #18 提供复现和修复方向](https://github.com/STARDUSTLC666/dsh-email/pull/18)。
|
|
8
|
+
|
|
9
|
+
验证宿主:官方源码构建的 Harness `0.2.0-rc.1`(commit `407e65c8`)+ Node `24.16.0`(2026-09-28)。282 项插件测试在隔离环境全部通过;同一个宿主里 18 个插件共同加载,注册 10 个工具,工具 schema 与健康检查契约通过。本轮未启用真实端口与外部服务。
|
|
10
|
+
|
|
5
11
|
> **让 agent 协助处理邮件**:收发、搜索、回复转发、附件、邮件整理与新邮件提醒,支持八种常见邮箱服务预设。
|
|
6
12
|
|
|
7
13
|
   
|
|
@@ -55,7 +61,7 @@ IMAP/SMTP email tools for DeepSeek Harness, with replies, forwarding, mailbox or
|
|
|
55
61
|
- **0.10.8 及更早**:见 [CHANGELOG.md](CHANGELOG.md)。
|
|
56
62
|
## 兼容性
|
|
57
63
|
|
|
58
|
-
当前验证基线为官方源码构建的 Harness **0.
|
|
64
|
+
当前验证基线为官方源码构建的 Harness **0.2.0-rc.1**(2026-09-28,含本地工具调度器 `Symbol.for` 修复)。18 个插件共同加载;邮件设置覆盖旧配置迁移、真实网页编辑与自动保存、刷新恢复、修订冲突和重启持久化检查。本轮使用隔离测试账号,未连接真实邮箱或发送邮件。
|
|
59
65
|
|
|
60
66
|
2026-09-21:当前发布包经官方 CLI 安装到隔离 profile,在源码构建的 Harness `0.1.6-alpha.2` 上与另外两个下载量前三插件共同加载,18 个插件工具注册正常;日历/邮件配置自检、PPT 主题查询和 17 行表格生成通过。测试本体基于官方 alpha.2 发布提交,另含工具调度器 `Symbol.for` 修复(`93badd88`)。本轮未连接真实邮箱或日历服务。
|
|
61
67
|
|
package/lib/client.js
CHANGED
|
@@ -208,6 +208,7 @@ const CSS = [
|
|
|
208
208
|
".dshe-auto-status{font-size:12px;font-weight:600;align-self:center;color:var(--dsw-alias-label-tertiary,#77736d)}",
|
|
209
209
|
".dshe-auto-status.is-saved{color:var(--dsw-alias-state-success-primary,#267d52)}",
|
|
210
210
|
".dshe-auto-status.is-error{color:var(--dsw-alias-state-error-primary,#aa3939)}",
|
|
211
|
+
".dshe-auto-status.is-draft{color:var(--dsw-alias-state-warning-primary,#986000)}",
|
|
211
212
|
".dshe-acc-badge.is-oauth{border-color:transparent;background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#267d52) 12%,transparent);color:var(--dsw-alias-state-success-primary,#267d52)}",
|
|
212
213
|
".dshe-oauth{display:grid;gap:8px;padding:10px 11px;border:1px dashed var(--dsw-alias-border-l2,#dedbd5);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#fff)}",
|
|
213
214
|
".dshe-oauth.is-logged-in{border-style:solid;border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#267d52) 34%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#267d52) 8%,transparent)}",
|
|
@@ -260,7 +261,7 @@ var UI = {
|
|
|
260
261
|
"provider.outlook": "Outlook",
|
|
261
262
|
"provider.icloud": "iCloud",
|
|
262
263
|
"provider.custom": "自定义",
|
|
263
|
-
"provider.noneInYaml": "
|
|
264
|
+
"provider.noneInYaml": "请选择邮箱服务商",
|
|
264
265
|
"provider.undefinedPresetSuffix": "(未定义预设)",
|
|
265
266
|
|
|
266
267
|
// 全局设置
|
|
@@ -272,6 +273,7 @@ var UI = {
|
|
|
272
273
|
// 自动保存状态条
|
|
273
274
|
"status.saved": "已保存",
|
|
274
275
|
"status.saving": "保存中…",
|
|
276
|
+
"status.draft": "还有未保存的内容,请补全下方标记的字段。",
|
|
275
277
|
"status.saveFailed": "保存失败:{message}(点击重试)",
|
|
276
278
|
|
|
277
279
|
// 账号卡片
|
|
@@ -314,7 +316,7 @@ var UI = {
|
|
|
314
316
|
"card.passwordLabel": "授权码 / 应用专用密码",
|
|
315
317
|
"card.passwordPlaceholder": "留空保持不变",
|
|
316
318
|
"card.passwordSaved": "已存有授权码:留空保持不变,清空后填内容即覆盖。",
|
|
317
|
-
"card.passwordHint": "
|
|
319
|
+
"card.passwordHint": "请填写邮箱服务商提供的授权码或应用专用密码,然后点击「测试连接」。",
|
|
318
320
|
"card.senderNameLabel": "发件显示名(可选)",
|
|
319
321
|
"card.senderNamePlaceholder": "只影响收件人看到的名称",
|
|
320
322
|
"card.senderNameHint": "只改收件人看到的显示名,发件地址仍是上面的邮箱地址。",
|
|
@@ -439,7 +441,7 @@ var UI = {
|
|
|
439
441
|
"provider.outlook": "Outlook",
|
|
440
442
|
"provider.icloud": "iCloud",
|
|
441
443
|
"provider.custom": "Custom",
|
|
442
|
-
"provider.noneInYaml": "
|
|
444
|
+
"provider.noneInYaml": "Choose your email provider",
|
|
443
445
|
"provider.undefinedPresetSuffix": "(undefined preset)",
|
|
444
446
|
|
|
445
447
|
// 全局设置
|
|
@@ -451,6 +453,7 @@ var UI = {
|
|
|
451
453
|
// 自动保存状态条
|
|
452
454
|
"status.saved": "Saved",
|
|
453
455
|
"status.saving": "Saving…",
|
|
456
|
+
"status.draft": "Some edits are not saved yet. Complete the highlighted fields below.",
|
|
454
457
|
"status.saveFailed": "Save failed: {message} (click to retry)",
|
|
455
458
|
|
|
456
459
|
// 账号卡片
|
|
@@ -493,7 +496,7 @@ var UI = {
|
|
|
493
496
|
"card.passwordLabel": "Authorization code / app password",
|
|
494
497
|
"card.passwordPlaceholder": "Leave empty to keep it",
|
|
495
498
|
"card.passwordSaved": "A password is already stored: leave empty to keep it, or clear the field and type to overwrite.",
|
|
496
|
-
"card.passwordHint": "
|
|
499
|
+
"card.passwordHint": "Enter the authorization code or app password from your email provider, then click “Test connection”.",
|
|
497
500
|
"card.senderNameLabel": "Sender display name (optional)",
|
|
498
501
|
"card.senderNamePlaceholder": "Only the name recipients see",
|
|
499
502
|
"card.senderNameHint": "Changes only the name recipients see; the address stays the one above.",
|
|
@@ -1350,33 +1353,38 @@ function AccountCardsEditor(props) {
|
|
|
1350
1353
|
*/
|
|
1351
1354
|
const autoSave = (draftsNow, orderNow, defaultNow, immediate) => {
|
|
1352
1355
|
if (typeof props.onAutoSave !== "function") return null;
|
|
1356
|
+
const incomplete = (text) => {
|
|
1357
|
+
setNotice({ kind: "error", text });
|
|
1358
|
+
if (typeof props.onDraftStatus === "function") props.onDraftStatus(true);
|
|
1359
|
+
};
|
|
1353
1360
|
const names = orderNow.map((row) => row.key).filter((name) => Object.prototype.hasOwnProperty.call(draftsNow, name));
|
|
1354
1361
|
let built;
|
|
1355
1362
|
try {
|
|
1356
1363
|
built = names.map((name) => draftToInput(draftsNow[name]));
|
|
1357
1364
|
} catch (e) {
|
|
1358
|
-
|
|
1365
|
+
incomplete(e && e.message ? e.message : String(e));
|
|
1359
1366
|
return null;
|
|
1360
1367
|
}
|
|
1361
1368
|
if (built.some((card) => String(card.name || "").trim() === "")) {
|
|
1362
|
-
|
|
1369
|
+
incomplete(t("card.errNoName"));
|
|
1363
1370
|
return null;
|
|
1364
1371
|
}
|
|
1365
1372
|
if (names.some((name) => !draftsNow[name].provider && !(draftsNow[name].imap?.host && draftsNow[name].smtp?.host))) {
|
|
1366
1373
|
// 服务商就是账号的连接身份:没有它,后端写回时会把 provider 键删掉,这个账号
|
|
1367
1374
|
// 永远连不上。名字和它都属于「这还是一张半填的卡」,一起拦在这里。
|
|
1368
|
-
|
|
1375
|
+
incomplete(t("card.errNoProvider"));
|
|
1369
1376
|
return null;
|
|
1370
1377
|
}
|
|
1371
1378
|
if (built.some((card) => card.name === "defaultAccount")) {
|
|
1372
|
-
|
|
1379
|
+
incomplete(t("card.errReservedName"));
|
|
1373
1380
|
return null;
|
|
1374
1381
|
}
|
|
1375
1382
|
if (built.length > 1 && String(defaultNow || "") === "") {
|
|
1376
|
-
|
|
1383
|
+
incomplete(t("card.errNoDefault"));
|
|
1377
1384
|
return null;
|
|
1378
1385
|
}
|
|
1379
1386
|
setNotice(null);
|
|
1387
|
+
if (typeof props.onDraftStatus === "function") props.onDraftStatus(false);
|
|
1380
1388
|
// 返回值只给 OAuth 登录用:它必须等这份配置真的落盘(后端按账号名去读 provider
|
|
1381
1389
|
// 和端点)。其余调用方照旧忽略返回值。
|
|
1382
1390
|
return props.onAutoSave(built, String(defaultNow || ""), immediate === true);
|
|
@@ -1889,9 +1897,11 @@ function ServerPresetsEditor(props) {
|
|
|
1889
1897
|
const result = serializePresetDrafts(nextOrder.map((row) => row.key), nextDrafts);
|
|
1890
1898
|
if (result.error !== undefined) {
|
|
1891
1899
|
setNotice({ kind: "error", text: result.error });
|
|
1900
|
+
if (typeof props.onDraftStatus === "function") props.onDraftStatus(true);
|
|
1892
1901
|
return;
|
|
1893
1902
|
}
|
|
1894
1903
|
setNotice(null);
|
|
1904
|
+
if (typeof props.onDraftStatus === "function") props.onDraftStatus(false);
|
|
1895
1905
|
props.onAutoSave(result.text);
|
|
1896
1906
|
};
|
|
1897
1907
|
|
|
@@ -2109,6 +2119,8 @@ function EmailSettingsSection() {
|
|
|
2109
2119
|
const [error, setError] = useState("");
|
|
2110
2120
|
/** 自动保存的可见状态:null 视同「已保存」(刚加载完、什么都没改)。 */
|
|
2111
2121
|
const [saveState, setSaveState] = useState(null);
|
|
2122
|
+
const [incompleteDrafts, setIncompleteDrafts] = useState({ accounts: false, presets: false });
|
|
2123
|
+
const hasIncompleteDraft = incompleteDrafts.accounts || incompleteDrafts.presets;
|
|
2112
2124
|
/** 卡片级提示(注释丢失、序列化失败…):走卡片自己的 notice 位,不打断顶部状态。 */
|
|
2113
2125
|
const [cardsNotice, setCardsNotice] = useState(null);
|
|
2114
2126
|
/**
|
|
@@ -2163,6 +2175,7 @@ function EmailSettingsSection() {
|
|
|
2163
2175
|
accountsYamlRef.current = next.accountsYaml || "";
|
|
2164
2176
|
savedSignatureRef.current = signatureOf(next, next.accountsYaml || "");
|
|
2165
2177
|
setSaveState(null);
|
|
2178
|
+
setIncompleteDrafts({ accounts: false, presets: false });
|
|
2166
2179
|
setCardsNotice(null);
|
|
2167
2180
|
setCustomNames(presetNamesOf(next.serverPresets));
|
|
2168
2181
|
setCardsDetail(snap.accountsDetail);
|
|
@@ -2184,6 +2197,7 @@ function EmailSettingsSection() {
|
|
|
2184
2197
|
* 防抖那条路交不出 promise(定时器还没到点),返回 null。
|
|
2185
2198
|
*/
|
|
2186
2199
|
const scheduleSave = (immediate) => {
|
|
2200
|
+
setSaveState({ kind: "busy" });
|
|
2187
2201
|
// 用户又改了:还在飞的那次保存结果就作废,别拿旧结果盖住新状态。
|
|
2188
2202
|
seqRef.current += 1;
|
|
2189
2203
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
@@ -2270,14 +2284,14 @@ function EmailSettingsSection() {
|
|
|
2270
2284
|
// 有没落盘的内容(保存中或保存失败)才拦离开:这正是「编辑即保存」剩下的窗口。
|
|
2271
2285
|
useEffect(() => {
|
|
2272
2286
|
const guard = (e) => {
|
|
2273
|
-
if (saveState === null || saveState.kind === "saved") return undefined;
|
|
2287
|
+
if (!hasIncompleteDraft && (saveState === null || saveState.kind === "saved")) return undefined;
|
|
2274
2288
|
e.preventDefault();
|
|
2275
2289
|
e.returnValue = "";
|
|
2276
2290
|
return "";
|
|
2277
2291
|
};
|
|
2278
2292
|
window.addEventListener("beforeunload", guard);
|
|
2279
2293
|
return () => window.removeEventListener("beforeunload", guard);
|
|
2280
|
-
}, [saveState]);
|
|
2294
|
+
}, [saveState, hasIncompleteDraft]);
|
|
2281
2295
|
|
|
2282
2296
|
/** 卡片序列化的往返序号:打字快时多个 serializeAccounts 会并发,只认最后一个。 */
|
|
2283
2297
|
const cardsSeqRef = useRef(0);
|
|
@@ -2312,6 +2326,7 @@ function EmailSettingsSection() {
|
|
|
2312
2326
|
*/
|
|
2313
2327
|
const onCardsAutoSave = async (cards, defaultAccount, immediate) => {
|
|
2314
2328
|
const seq = ++cardsSeqRef.current;
|
|
2329
|
+
setSaveState({ kind: "busy" });
|
|
2315
2330
|
try {
|
|
2316
2331
|
const result = await serializeCards(cards, defaultAccount);
|
|
2317
2332
|
if (seq !== cardsSeqRef.current) return { ok: false, reason: "superseded" }; // 更晚的一次已经在路上,别用旧结果盖新状态
|
|
@@ -2333,8 +2348,10 @@ function EmailSettingsSection() {
|
|
|
2333
2348
|
if (flushed && typeof flushed.then === "function") await flushed;
|
|
2334
2349
|
return { ok: true };
|
|
2335
2350
|
} catch (e) {
|
|
2351
|
+
if (seq !== cardsSeqRef.current) return { ok: false, reason: "superseded" };
|
|
2336
2352
|
const message = e && e.message ? e.message : String(e);
|
|
2337
2353
|
setCardsNotice({ kind: "error", text: t("notice.cardsNotWritten", { message }) });
|
|
2354
|
+
setSaveState({ kind: "error", text: message });
|
|
2338
2355
|
return { ok: false, reason: message };
|
|
2339
2356
|
}
|
|
2340
2357
|
};
|
|
@@ -2360,7 +2377,7 @@ function EmailSettingsSection() {
|
|
|
2360
2377
|
|
|
2361
2378
|
const accounts = snapshot && snapshot.accounts ? snapshot.accounts : [];
|
|
2362
2379
|
const presetsMerged = presetsWithNames((snapshot && snapshot.presets) || undefined, customNames);
|
|
2363
|
-
const statusText = saveState === null
|
|
2380
|
+
const statusText = hasIncompleteDraft ? t("status.draft") : saveState === null
|
|
2364
2381
|
? t("status.saved")
|
|
2365
2382
|
: saveState.kind === "busy"
|
|
2366
2383
|
? t("status.saving")
|
|
@@ -2368,9 +2385,11 @@ function EmailSettingsSection() {
|
|
|
2368
2385
|
? t("status.saved")
|
|
2369
2386
|
: t("status.saveFailed", { message: saveState.text });
|
|
2370
2387
|
const status = h("span", {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2388
|
+
role: "status",
|
|
2389
|
+
"aria-live": "polite",
|
|
2390
|
+
className: "dshe-auto-status" + (hasIncompleteDraft ? " is-draft" : saveState === null ? "" : " is-" + saveState.kind),
|
|
2391
|
+
onClick: !hasIncompleteDraft && saveState !== null && saveState.kind === "error" ? () => flushSave() : undefined,
|
|
2392
|
+
style: !hasIncompleteDraft && saveState !== null && saveState.kind === "error" ? { cursor: "pointer" } : undefined,
|
|
2374
2393
|
}, statusText);
|
|
2375
2394
|
|
|
2376
2395
|
return h("div", { className: "dshe-settings" }, [
|
|
@@ -2403,6 +2422,7 @@ function EmailSettingsSection() {
|
|
|
2403
2422
|
presets: presetsMerged,
|
|
2404
2423
|
extraNotice: cardsNotice,
|
|
2405
2424
|
onAutoSave: onCardsAutoSave,
|
|
2425
|
+
onDraftStatus: (blocked) => setIncompleteDrafts((current) => ({ ...current, accounts: blocked })),
|
|
2406
2426
|
onTest: onCardTest,
|
|
2407
2427
|
// OAuth2 登录成功后重拉整份快照:卡片的 authKind/oauthState/oauthUser 是
|
|
2408
2428
|
// 服务端算出来的,只有新快照才拿得到(和加载走同一条路径)。
|
|
@@ -2413,6 +2433,7 @@ function EmailSettingsSection() {
|
|
|
2413
2433
|
h(ServerPresetsEditor, {
|
|
2414
2434
|
presets: presetsAtLoad,
|
|
2415
2435
|
onAutoSave: (text) => update({ serverPresets: typeof text === "string" ? text : "" }),
|
|
2436
|
+
onDraftStatus: (blocked) => setIncompleteDrafts((current) => ({ ...current, presets: blocked })),
|
|
2416
2437
|
}),
|
|
2417
2438
|
]),
|
|
2418
2439
|
snapshot && snapshot.writable === false
|
package/lib/config.d.ts
CHANGED
|
@@ -56,6 +56,18 @@ export interface SmtpConfig {
|
|
|
56
56
|
port?: number;
|
|
57
57
|
secure?: boolean;
|
|
58
58
|
}
|
|
59
|
+
/** Legacy form placeholders; an untouched blank-host endpoint delegates to its provider. */
|
|
60
|
+
export declare const ENDPOINT_DEFAULTS: {
|
|
61
|
+
readonly imap: {
|
|
62
|
+
readonly port: 993;
|
|
63
|
+
readonly secure: true;
|
|
64
|
+
};
|
|
65
|
+
readonly smtp: {
|
|
66
|
+
readonly port: 465;
|
|
67
|
+
readonly secure: true;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export declare function normalizeSettingsEndpoint<T extends SmtpConfig>(value: T, kind: keyof typeof ENDPOINT_DEFAULTS): Omit<T, 'host' | 'port' | 'secure'> & SmtpConfig;
|
|
59
71
|
/** One mailbox account. Top-level shorthand fields act as shared defaults. */
|
|
60
72
|
export interface AccountConfig {
|
|
61
73
|
/** Built-in provider name, or a custom serverPresets name. */
|
|
@@ -256,8 +268,10 @@ export declare function serializeAccountsYaml(raw: unknown, defaultAccount?: str
|
|
|
256
268
|
* Resolve and validate the raw row config. Throws with an actionable message
|
|
257
269
|
* (in Chinese, since it is what the user and the model both read) when the
|
|
258
270
|
* account is not fully specified.
|
|
271
|
+
* An explicit account resolves only that card for connection tests, without
|
|
272
|
+
* making named accounts eligible for the single-account environment password.
|
|
259
273
|
*/
|
|
260
|
-
export declare function resolveEmailSettings(config: EmailConfig | undefined): ResolvedEmailSettings;
|
|
274
|
+
export declare function resolveEmailSettings(config: EmailConfig | undefined, onlyAccount?: string): ResolvedEmailSettings;
|
|
261
275
|
/** Every name a `provider:` may legally use, built-ins first. */
|
|
262
276
|
export declare function providerNames(custom?: Record<string, ServerPreset>): string[];
|
|
263
277
|
/**
|
package/lib/config.js
CHANGED
|
@@ -34,6 +34,24 @@ export const OUTLOOK_IMAP_HOST = 'outlook.office365.com';
|
|
|
34
34
|
* them, so such a release asks every built-in account to log in one more time.
|
|
35
35
|
*/
|
|
36
36
|
export const OUTLOOK_OAUTH2_CLIENT_ID = '15dcd5aa-00dd-487f-82d7-1d2b2c299e14';
|
|
37
|
+
/** Legacy form placeholders; an untouched blank-host endpoint delegates to its provider. */
|
|
38
|
+
export const ENDPOINT_DEFAULTS = {
|
|
39
|
+
imap: { port: 993, secure: true }, smtp: { port: 465, secure: true },
|
|
40
|
+
};
|
|
41
|
+
export function normalizeSettingsEndpoint(value, kind) {
|
|
42
|
+
if (typeof value.host !== 'string' || value.host.trim() !== '')
|
|
43
|
+
return value;
|
|
44
|
+
const normalized = { ...value };
|
|
45
|
+
delete normalized.host;
|
|
46
|
+
const defaults = ENDPOINT_DEFAULTS[kind];
|
|
47
|
+
// A changed port or TLS choice is intentional; preserve that whole combination.
|
|
48
|
+
if ((value.port === undefined || value.port === defaults.port)
|
|
49
|
+
&& (value.secure === undefined || value.secure === defaults.secure)) {
|
|
50
|
+
delete normalized.port;
|
|
51
|
+
delete normalized.secure;
|
|
52
|
+
}
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
37
55
|
export const PROVIDER_PRESETS = {
|
|
38
56
|
qq: { imap: { host: 'imap.qq.com', port: 993, secure: true }, smtp: { host: 'smtp.qq.com', port: 465, secure: true } },
|
|
39
57
|
'163': { imap: { host: 'imap.163.com', port: 993, secure: true }, smtp: { host: 'smtp.163.com', port: 465, secure: true } },
|
|
@@ -201,8 +219,10 @@ function normalizeAccountForYaml(value) {
|
|
|
201
219
|
* Resolve and validate the raw row config. Throws with an actionable message
|
|
202
220
|
* (in Chinese, since it is what the user and the model both read) when the
|
|
203
221
|
* account is not fully specified.
|
|
222
|
+
* An explicit account resolves only that card for connection tests, without
|
|
223
|
+
* making named accounts eligible for the single-account environment password.
|
|
204
224
|
*/
|
|
205
|
-
export function resolveEmailSettings(config) {
|
|
225
|
+
export function resolveEmailSettings(config, onlyAccount) {
|
|
206
226
|
const raw = config ?? {};
|
|
207
227
|
const common = {
|
|
208
228
|
provider: raw.provider,
|
|
@@ -218,6 +238,10 @@ export function resolveEmailSettings(config) {
|
|
|
218
238
|
const entries = parsedYaml !== undefined
|
|
219
239
|
? parsedYaml.map
|
|
220
240
|
: (raw.accounts === undefined || Object.keys(raw.accounts).length === 0 ? undefined : raw.accounts);
|
|
241
|
+
const available = entries === undefined ? ['default'] : Object.keys(entries);
|
|
242
|
+
if (onlyAccount !== undefined && !available.includes(onlyAccount)) {
|
|
243
|
+
throw new Error(`未知账号 "${onlyAccount}",可用:${available.join('、')}`);
|
|
244
|
+
}
|
|
221
245
|
// The custom preset table is read once, here, and never stored on the result:
|
|
222
246
|
// it is a lookup source for provider names, not part of the resolved config.
|
|
223
247
|
// A broken preset text degrades to "no custom presets" — the account-level
|
|
@@ -237,11 +261,16 @@ export function resolveEmailSettings(config) {
|
|
|
237
261
|
}
|
|
238
262
|
else {
|
|
239
263
|
for (const [name, acc] of Object.entries(entries)) {
|
|
264
|
+
if (onlyAccount !== undefined && name !== onlyAccount)
|
|
265
|
+
continue;
|
|
240
266
|
accounts.set(name, resolveAccount(name, common, acc ?? {}, false, providers, known));
|
|
241
267
|
}
|
|
242
268
|
}
|
|
243
269
|
let defaultName;
|
|
244
|
-
if (
|
|
270
|
+
if (onlyAccount !== undefined) {
|
|
271
|
+
defaultName = onlyAccount;
|
|
272
|
+
}
|
|
273
|
+
else if (raw.defaultAccount !== undefined && raw.defaultAccount !== '') {
|
|
245
274
|
if (!accounts.has(raw.defaultAccount)) {
|
|
246
275
|
throw new Error(`dsh-email:defaultAccount "${raw.defaultAccount}" 不存在,可用账号:${[...accounts.keys()].join('、')}`);
|
|
247
276
|
}
|
|
@@ -329,21 +358,25 @@ function resolveAccount(name, common, acc, allowEnvPassword, providers, known) {
|
|
|
329
358
|
const authUser = (acc.authUser ?? common.authUser ?? '').trim() || user;
|
|
330
359
|
const authPassword = (acc.authPassword ?? common.authPassword) || password;
|
|
331
360
|
const senderName = (acc.senderName ?? common.senderName ?? '').trim();
|
|
361
|
+
const accountImap = acc.imap && normalizeSettingsEndpoint(acc.imap, 'imap');
|
|
362
|
+
const sharedImap = common.imap && normalizeSettingsEndpoint(common.imap, 'imap');
|
|
363
|
+
const accountSmtp = acc.smtp && normalizeSettingsEndpoint(acc.smtp, 'smtp');
|
|
364
|
+
const sharedSmtp = common.smtp && normalizeSettingsEndpoint(common.smtp, 'smtp');
|
|
332
365
|
const imap = {
|
|
333
|
-
host:
|
|
334
|
-
port:
|
|
335
|
-
secure:
|
|
336
|
-
connectionTimeoutMs:
|
|
337
|
-
socketTimeoutMs:
|
|
366
|
+
host: accountImap?.host ?? sharedImap?.host ?? preset?.imap.host,
|
|
367
|
+
port: accountImap?.port ?? sharedImap?.port ?? preset?.imap.port,
|
|
368
|
+
secure: accountImap?.secure ?? sharedImap?.secure ?? preset?.imap.secure,
|
|
369
|
+
connectionTimeoutMs: accountImap?.connectionTimeoutMs ?? sharedImap?.connectionTimeoutMs,
|
|
370
|
+
socketTimeoutMs: accountImap?.socketTimeoutMs ?? sharedImap?.socketTimeoutMs,
|
|
338
371
|
};
|
|
339
372
|
const smtp = {
|
|
340
|
-
host:
|
|
341
|
-
port:
|
|
342
|
-
secure:
|
|
373
|
+
host: accountSmtp?.host ?? sharedSmtp?.host ?? preset?.smtp.host,
|
|
374
|
+
port: accountSmtp?.port ?? sharedSmtp?.port ?? preset?.smtp.port,
|
|
375
|
+
secure: accountSmtp?.secure ?? sharedSmtp?.secure ?? preset?.smtp.secure,
|
|
343
376
|
};
|
|
344
377
|
const problems = [];
|
|
345
378
|
if (user === '')
|
|
346
|
-
problems.push(`账号 "${name}"
|
|
379
|
+
problems.push(`账号 "${name}" 的邮箱地址未填写`);
|
|
347
380
|
// An explicit `authKind` outranks the derivation: a tenant that still accepts
|
|
348
381
|
// an app password for Exchange Online, or a mailbox that worked before the
|
|
349
382
|
// derivation existed, keeps working instead of being told「尚未登录」.
|
|
@@ -358,14 +391,14 @@ function resolveAccount(name, common, acc, allowEnvPassword, providers, known) {
|
|
|
358
391
|
// in the OAuth2 store, and requiring a password would demand a secret
|
|
359
392
|
// Microsoft no longer accepts for Exchange Online.
|
|
360
393
|
if (!oauth2 && authPassword === '') {
|
|
361
|
-
problems.push(`账号 "${name}"
|
|
394
|
+
problems.push(`账号 "${name}" 的${authUser === user ? '授权码 / 应用专用密码' : '登录账号的密码'}未填写`);
|
|
362
395
|
}
|
|
363
396
|
if (imap.host === undefined || imap.host === '')
|
|
364
397
|
problems.push(`账号 "${name}" 的 imap.host 未填写(可填 provider 预设:${known.join('/')})`);
|
|
365
398
|
if (smtp.host === undefined || smtp.host === '')
|
|
366
399
|
problems.push(`账号 "${name}" 的 smtp.host 未填写(同上)`);
|
|
367
400
|
if (problems.length > 0) {
|
|
368
|
-
throw new Error(
|
|
401
|
+
throw new Error(`邮件账号配置不完整:${problems.join(';')}。请在「设置 → 邮件」补全对应账号,再点击「测试连接」。修改会自动保存并立即生效。`);
|
|
369
402
|
}
|
|
370
403
|
const clientId = (acc.clientId ?? common.clientId ?? '').trim();
|
|
371
404
|
return {
|
package/lib/settings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from 'schemastery';
|
|
2
|
-
import { PROVIDER_NAMES } from './config.js';
|
|
2
|
+
import { ENDPOINT_DEFAULTS, normalizeSettingsEndpoint, PROVIDER_NAMES } from './config.js';
|
|
3
3
|
/** Settings-document namespace this plugin owns (editable from the Web settings page). */
|
|
4
4
|
export const SETTINGS_NAMESPACE = 'dsh-email';
|
|
5
5
|
/**
|
|
@@ -22,13 +22,13 @@ export const EmailSettingsSchema = z.object({
|
|
|
22
22
|
serverPresets: z.string().default(''),
|
|
23
23
|
imap: z.object({
|
|
24
24
|
host: z.string().default(''),
|
|
25
|
-
port: z.number().default(
|
|
26
|
-
secure: z.boolean().default(
|
|
25
|
+
port: z.number().default(ENDPOINT_DEFAULTS.imap.port),
|
|
26
|
+
secure: z.boolean().default(ENDPOINT_DEFAULTS.imap.secure),
|
|
27
27
|
}),
|
|
28
28
|
smtp: z.object({
|
|
29
29
|
host: z.string().default(''),
|
|
30
|
-
port: z.number().default(
|
|
31
|
-
secure: z.boolean().default(
|
|
30
|
+
port: z.number().default(ENDPOINT_DEFAULTS.smtp.port),
|
|
31
|
+
secure: z.boolean().default(ENDPOINT_DEFAULTS.smtp.secure),
|
|
32
32
|
}),
|
|
33
33
|
});
|
|
34
34
|
/** Project the row config (cordis.patch.yml) into the settings-schema base shape. */
|
|
@@ -45,15 +45,15 @@ export function toSettingsBase(config) {
|
|
|
45
45
|
...(config.imap !== undefined ? {
|
|
46
46
|
imap: {
|
|
47
47
|
host: config.imap.host ?? '',
|
|
48
|
-
port: config.imap.port ??
|
|
49
|
-
secure: config.imap.secure ??
|
|
48
|
+
port: config.imap.port ?? ENDPOINT_DEFAULTS.imap.port,
|
|
49
|
+
secure: config.imap.secure ?? ENDPOINT_DEFAULTS.imap.secure,
|
|
50
50
|
},
|
|
51
51
|
} : {}),
|
|
52
52
|
...(config.smtp !== undefined ? {
|
|
53
53
|
smtp: {
|
|
54
54
|
host: config.smtp.host ?? '',
|
|
55
|
-
port: config.smtp.port ??
|
|
56
|
-
secure: config.smtp.secure ??
|
|
55
|
+
port: config.smtp.port ?? ENDPOINT_DEFAULTS.smtp.port,
|
|
56
|
+
secure: config.smtp.secure ?? ENDPOINT_DEFAULTS.smtp.secure,
|
|
57
57
|
},
|
|
58
58
|
} : {}),
|
|
59
59
|
};
|
|
@@ -131,7 +131,9 @@ export function toEmailConfig(value, user) {
|
|
|
131
131
|
// serverPresets is intentionally NOT projected: it is UI-side metadata that
|
|
132
132
|
// resolution never reads, and projecting it would put it into the resolved
|
|
133
133
|
// fingerprint, so saving a preset would dispose every live IMAP connection.
|
|
134
|
-
|
|
134
|
+
// Only a persisted blank host signals a placeholder. A schema-added blank
|
|
135
|
+
// host must not erase an explicit port-only override from the raw document.
|
|
136
|
+
const imapValue = user?.imap?.host !== undefined && draft.imap ? normalizeSettingsEndpoint(draft.imap, 'imap') : draft.imap;
|
|
135
137
|
const imapKeys = user === null ? imapValue : user?.imap;
|
|
136
138
|
if (isSet(imapKeys) && isSet(imapValue)) {
|
|
137
139
|
const imap = {};
|
|
@@ -145,7 +147,7 @@ export function toEmailConfig(value, user) {
|
|
|
145
147
|
imap.secure = imapValue.secure;
|
|
146
148
|
out.imap = imap;
|
|
147
149
|
}
|
|
148
|
-
const smtpValue = draft.smtp;
|
|
150
|
+
const smtpValue = user?.smtp?.host !== undefined && draft.smtp ? normalizeSettingsEndpoint(draft.smtp, 'smtp') : draft.smtp;
|
|
149
151
|
const smtpKeys = user === null ? smtpValue : user?.smtp;
|
|
150
152
|
if (isSet(smtpKeys) && isSet(smtpValue)) {
|
|
151
153
|
const smtp = {};
|
package/lib/web.js
CHANGED
|
@@ -855,12 +855,12 @@ export class EmailSettingsBackend {
|
|
|
855
855
|
// does for the stored settings (it never enters the resolved fingerprint).
|
|
856
856
|
const draft = toEmailConfig(value, null);
|
|
857
857
|
const presets = value?.serverPresets ?? this.scope.get()?.serverPresets;
|
|
858
|
+
const requested = typeof accountName === 'string' && accountName.trim() !== '' ? accountName.trim() : '';
|
|
858
859
|
const settings = resolveEmailSettings({
|
|
859
860
|
...this.currentConfig,
|
|
860
861
|
...draft,
|
|
861
862
|
...(typeof presets === 'string' ? { serverPresets: presets } : {}),
|
|
862
|
-
});
|
|
863
|
-
const requested = typeof accountName === 'string' && accountName.trim() !== '' ? accountName.trim() : '';
|
|
863
|
+
}, requested || undefined);
|
|
864
864
|
const available = [...settings.accounts.keys()];
|
|
865
865
|
const name = requested !== '' ? requested : settings.defaultAccount;
|
|
866
866
|
const cfg = settings.accounts.get(name);
|