@eduwork/dsh-mail 0.1.1 → 0.2.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.
@@ -1,53 +1,11 @@
1
- # 公开发布检查表
1
+ # 包发布检查表
2
2
 
3
3
  **简体中文** | [English](release-checklist.en.md)
4
4
 
5
- 发布 Pull Request 或发布记录应记维护者决策、日期、命令输出链接、独立技术复核证据和例外。项目初期由单一维护者负责发布;不得虚构尚不存在的第二位人工批准人。任何适用且未完成的安全或发布检查都会阻止稳定发布;未来增加替补和多人审查作为治理目标记录。
5
+ 源码与发布入口统一在 EduWork;遵循[公共包发布流程](https://github.com/ecnu/EduWork/blob/main/docs/PACKAGES.md),本包保留独立的 npm 名称和 SemVer。
6
6
 
7
- ## 授权与元数据
8
-
9
- - [ ] 当前维护者确认公开发布和 MIT 许可证,并记录技术复核证据与剩余风险。
10
- - [ ] GitHub 仓库、npm 包名、主页、Issue 和安全报告地址准确。
11
- - [ ] 版本、Changelog、兼容性和安装命令一致。
12
- - [ ] 默认文档为中文,每份 Markdown 文档都有英文镜像和双向入口。
13
- - [ ] Git 历史和发布内容不含真实邮箱、邮件、授权码、服务器、个人路径或未授权品牌。
14
-
15
- ## 邮件与权限边界
16
-
17
- - [ ] IMAP mailbox lock 仍固定为只读,代码没有 `STORE`、`COPY`、`MOVE`、`EXPUNGE` 或 flags 写入。
18
- - [ ] 读信和发信能力默认关闭,并且彼此独立。
19
- - [ ] `mail_send` 在读取凭据、附件和访问 SMTP 前进入 DSH 权限通路。
20
- - [ ] 普通权限逐次审批;Full Access 只免除逐次弹窗,不能自动开启发信能力。
21
- - [ ] 无 Agent 会话、拒绝、取消和不可用审批通路均失败关闭。
22
- - [ ] 邮件数据持续标记为不可信,不能成为发信授权、密钥请求或系统指令。
23
- - [ ] SMTP DATA 阶段的不确定结果返回 `unknown`,不会自动重试。
24
-
25
- ## 凭据、传输和附件
26
-
27
- - [ ] 授权码只进入 DSH Credential Provider,浏览器只能看到配置状态。
28
- - [ ] 每次工具调用重新解析凭据,不跨操作缓存。
29
- - [ ] 只允许隐式 TLS 或 STARTTLS,证书校验不可关闭,协议日志关闭。
30
- - [ ] 地址拒绝 CR/LF 注入;正文、消息和附件大小上限有效。
31
- - [ ] 发送附件限定为当前会话工作区普通文件。
32
- - [ ] 下载附件使用随机名、独占写和真实路径包含性复核;符号链接/目录联接逃逸测试通过。
33
- - [ ] Source map 和错误归一化不泄露凭据、邮件内容、服务器响应或个人路径。
34
-
35
- ## 功能验收
36
-
37
- - [ ] 使用 manifest 声明的当前 DSH 精确版本运行 `npm run accept:dsh-015-rc1`,并确认 Runtime 没有混入旧版 DSH 包。
38
- - [ ] `mail_list_folders` 返回真实目录且不修改状态。
39
- - [ ] `mail_find` 的正序/倒序、多页、日期、目录、UIDVALIDITY 失效和 `hasMore=false` 均测试。
40
- - [ ] `mail_read` 不改变已读状态,正文截断和 HTML 降级符合文档。
41
- - [ ] `mail_get_attachment` 在本地 FS 成功,在远程/不共享 Host 路径时明确失败。
42
- - [ ] 普通权限下发信批准/拒绝、Full Access 发信、工作区外附件拒绝均通过真实测试邮箱验收。
43
- - [ ] 不支持的 OAuth2、多账号、Sent 副本和复杂 MIME 行为仍清楚记录。
44
-
45
- ## 供应链与发布
46
-
47
- - [ ] Windows 与 Linux 使用 Node 22/24 完成干净 `npm ci` 和 `npm run check`。
48
- - [ ] `npm audit`、CodeQL、直接/间接许可证和安装脚本已人工审查。
49
- - [ ] GitHub Actions 固定到不可变 commit,Dependabot 与分支保护有效。
50
- - [ ] `npm pack --dry-run` 和解压后的 tarball 只包含预期文件;Host bundle 没有外部 IMAP/SMTP 裸导入。
51
- - [ ] tag、发布说明、tarball checksum、弃用和回滚方案已准备。
52
- - [ ] 已现场验证 npm Trusted Publisher 绑定本仓库、`.github/workflows/release.yml` 与 Environment `npm` 后,才启用 OIDC/provenance 发布路径。
53
- - [ ] Trusted Publisher 未验证时,从经复核的冻结包使用 npm CLI 和浏览器 2FA 发布,不声明 provenance,也不保存长期 Token。
7
+ - 核对变更行为、版本、变更记录、兼容范围和依赖锁。
8
+ - 在当前包目录执行 npm ci、npm run check;有行为变化时补对应 Host 或浏览器实测,使用合成测试数据。
9
+ - 检查实际 tarball、公开文档、许可证和导出入口,不携带凭据或用户数据。
10
+ - 在 packages-release.yml 中选择本包;publish=false 仅产出候选。明确发布时从匹配包版本的 tag 运行,使用 npm environment 与本仓库的 Trusted Publisher。
11
+ - 核验 registry 中的实际字节后再更新产品锁。发布 npm 不会发布桌面 Release 或替换客户端。
@@ -23,6 +23,10 @@ The canonical body field is `untrustedText`. Rendering adds a fixed warning and
23
23
 
24
24
  Warnings are defense in depth. The actual mutation boundary is enforced by the separate `sendEnabled` capability, argument validation, workspace fencing, and DSH's native `tools/pre-execute` permission path. Even if the model mishandles text, the mail body cannot bypass those mechanisms.
25
25
 
26
+ ## Attachment downloads
27
+
28
+ `mail_get_attachment` checks the session's effective write policy before accessing credentials or downloading. Workspace-write permits saving attachments; read-only or unknown modes require approval for this call. Rejection, cancellation or unavailable approval prevents the download. Path and size limits remain enforced; download permission does not grant permission to send mail.
29
+
26
30
  ## Send approval
27
31
 
28
32
  `mail_send` enters DSH permission handling before credentials, attachments, or SMTP access:
@@ -23,6 +23,10 @@
23
23
 
24
24
  这些提示不是唯一防线。真正的写操作依赖独立机制:`mail_send` 的能力开关、参数验证、工作区附件 fence,以及 DSH 原生 `tools/pre-execute` 权限通路。即使模型忽略提示,邮件正文仍不能绕过它们。
25
25
 
26
+ ## 附件下载
27
+
28
+ `mail_get_attachment` 在访问凭据和下载附件之前检查会话当前生效的写入权限。工作区写入权限允许保存附件;只读或未知权限仍请求本次审批。拒绝、取消或审批不可用时不执行下载。路径边界和文件大小限制继续生效;下载权限不会授予发信权限。
29
+
26
30
  ## 发信审批
27
31
 
28
32
  `mail_send` 在接触凭据、附件或 SMTP 之前进入 DSH 原生权限 waterfall:
package/lib/client.js CHANGED
@@ -29,126 +29,133 @@ window.__ModuleLoader__.load({
29
29
  let react = require("react");
30
30
  react = __toESM(react, 1);
31
31
  let react_jsx_runtime = require("react/jsx-runtime");
32
+ //#region src/client/locale.ts
33
+ const LOCALE_NS = "settings.mailAssistant";
34
+ const dictionaries = {
35
+ "zh": {
36
+ "nav": "邮件助手",
37
+ "title": "邮件助手",
38
+ "description": "让 Agent 通过标准 IMAP 只读检索邮件,并按当前权限模式通过 SMTP 发信。它不是邮箱客户端,不会移动、删除、归档或修改邮件状态。",
39
+ "loading": "正在读取配置…",
40
+ "save": "保存",
41
+ "saving": "正在保存…",
42
+ "saved": "配置已保存,新调用立即生效。",
43
+ "readPermission": "允许 Agent 读信",
44
+ "readHint": "可列出目录、分页查找、读取邮件和下载附件;IMAP 始终只读。",
45
+ "sendPermission": "允许 Agent 发信",
46
+ "sendHint": "可发送纯文本邮件;普通权限逐次确认,Full Access 不再弹窗。",
47
+ "identity": "邮箱账号",
48
+ "identityHint": "先填写日常使用的邮箱信息。多数邮箱要求使用客户端授权码,而不是网页登录密码。",
49
+ "email": "邮箱地址",
50
+ "emailHint": "用于收信和发信。",
51
+ "username": "登录用户名",
52
+ "usernameHint": "仅当服务商要求时填写;留空则使用邮箱地址。",
53
+ "fromName": "发件人显示名称",
54
+ "fromNameHint": "收件人看到的名称;可留空。",
55
+ "inbox": "收件箱目录",
56
+ "inboxHint": "通常保持 INBOX。",
57
+ "password": "密码 / 客户端授权码",
58
+ "passwordHint": "安全写入本机凭据存储,不会写进 settings.yaml,也不会回显。",
59
+ "configured": "已保存",
60
+ "missing": "未填写",
61
+ "credentialPlaceholder": "输入授权码;已保存时留空不会改变",
62
+ "clearPassword": "清除授权码",
63
+ "servers": "邮箱服务器",
64
+ "serversHint": "选择常见服务商可自动填写;单位邮箱或自建邮箱请选择“手动配置”。",
65
+ "preset": "邮箱服务商",
66
+ "custom": "手动配置",
67
+ "imap": "收信(IMAP)",
68
+ "smtp": "发信(SMTP)",
69
+ "host": "服务器地址",
70
+ "port": "端口",
71
+ "tls": "加密",
72
+ "implicit": "隐式 TLS",
73
+ "starttls": "STARTTLS",
74
+ "advanced": "高级设置",
75
+ "advancedHint": "登录用户名、收件箱目录和安全上限通常不需要修改。",
76
+ "collapse": "收起",
77
+ "agentAccess": "开放给 Agent",
78
+ "agentAccessHint": "账号和服务器保存好后,再按需要分别开放读信或发信。",
79
+ "permissionNeedsSetup": "请先填写邮箱地址、授权码和相应的服务器。",
80
+ "limits": "安全上限",
81
+ "bodyLimit": "正文字符",
82
+ "messageLimit": "整封邮件字节",
83
+ "attachmentLimit": "附件总字节",
84
+ "usage": "保存后,在对话里让 Agent“查一下最近邮件”即可验证。插件不会在启动或保存时主动连接邮箱。",
85
+ "error": "保存失败",
86
+ "reload": "重新加载",
87
+ "unavailable": "当前连接未提供此设置命名空间。",
88
+ "readOnly": "当前连接为只读模式,无法保存设置。",
89
+ "credentialPartial": "授权码已经保存,但其他配置保存失败;请修正后再次保存。"
90
+ },
91
+ "en": {
92
+ "nav": "Mail assistant",
93
+ "title": "Mail assistant",
94
+ "description": "Let the agent search mail through read-only IMAP and send through SMTP under the current permission preset. This is not a mail client: it never moves, deletes, archives, or changes message state.",
95
+ "loading": "Loading configuration…",
96
+ "save": "Save",
97
+ "saving": "Saving…",
98
+ "saved": "Saved. New calls use the configuration immediately.",
99
+ "readPermission": "Allow the agent to read mail",
100
+ "readHint": "List folders, page through searches, read messages, and download attachments. IMAP always stays read-only.",
101
+ "sendPermission": "Allow the agent to send mail",
102
+ "sendHint": "Send plain-text messages. Ordinary permissions ask each time; Full Access does not prompt.",
103
+ "identity": "Mailbox account",
104
+ "identityHint": "Start with the mailbox information you normally use. Most providers require an app password instead of the web-login password.",
105
+ "email": "Email address",
106
+ "emailHint": "Used for both incoming and outgoing mail.",
107
+ "username": "Login username",
108
+ "usernameHint": "Only set this when required by your provider; otherwise the email address is used.",
109
+ "fromName": "Sender display name",
110
+ "fromNameHint": "The name recipients see; optional.",
111
+ "inbox": "Inbox folder",
112
+ "inboxHint": "Usually keep INBOX.",
113
+ "password": "Password / app password",
114
+ "passwordHint": "Stored securely in the local credential store. It is never written to settings.yaml or returned to this page.",
115
+ "configured": "Saved",
116
+ "missing": "Missing",
117
+ "credentialPlaceholder": "Enter an app password; leave blank to keep the saved value",
118
+ "clearPassword": "Clear app password",
119
+ "servers": "Mail servers",
120
+ "serversHint": "Choose a common provider to fill these automatically, or use Manual configuration for institutional and self-hosted mail.",
121
+ "preset": "Mail provider",
122
+ "custom": "Manual configuration",
123
+ "imap": "Incoming (IMAP)",
124
+ "smtp": "Outgoing (SMTP)",
125
+ "host": "Server address",
126
+ "port": "Port",
127
+ "tls": "Encryption",
128
+ "implicit": "Implicit TLS",
129
+ "starttls": "STARTTLS",
130
+ "advanced": "Advanced settings",
131
+ "advancedHint": "Login username, inbox folder, and safety limits usually need no changes.",
132
+ "collapse": "Collapse",
133
+ "agentAccess": "Agent access",
134
+ "agentAccessHint": "After saving the account and servers, enable reading and sending independently as needed.",
135
+ "permissionNeedsSetup": "Enter the email address, app password, and corresponding server first.",
136
+ "limits": "Safety limits",
137
+ "bodyLimit": "Body characters",
138
+ "messageLimit": "Whole-message bytes",
139
+ "attachmentLimit": "Total attachment bytes",
140
+ "usage": "After saving, ask the agent to find recent mail. The plugin never connects to a mailbox during startup or save.",
141
+ "error": "Save failed",
142
+ "reload": "Reload",
143
+ "unavailable": "This connection does not expose the mail settings namespace.",
144
+ "readOnly": "This connection exposes settings in read-only mode.",
145
+ "credentialPartial": "The app password was stored, but the remaining settings failed to save. Fix the form and save again."
146
+ }
147
+ };
148
+ //#endregion
32
149
  //#region src/client/index.tsx
33
150
  const inject = [
34
151
  "slots",
152
+ "locale",
35
153
  "remote",
36
154
  "remote.credentials",
37
- "settingsScope"
155
+ "configForms"
38
156
  ];
39
157
  const NS = "dsh-mail-assistant";
40
158
  const PASSWORD_REF = "DSH_MAIL_ASSISTANT_PASSWORD";
41
- const copy = typeof navigator !== "undefined" && navigator.language.toLowerCase().startsWith("zh") ? {
42
- nav: "邮件助手",
43
- title: "邮件助手",
44
- description: "让 Agent 通过标准 IMAP 只读检索邮件,并按当前 DSH 权限模式通过 SMTP 发信。它不是邮箱客户端,不会移动、删除、归档或修改邮件状态。",
45
- loading: "正在读取配置…",
46
- save: "保存",
47
- saving: "正在保存…",
48
- saved: "配置已保存,新调用立即生效。",
49
- readPermission: "允许 Agent 读信",
50
- readHint: "可列出目录、分页查找、读取邮件和下载附件;IMAP 始终只读。",
51
- sendPermission: "允许 Agent 发信",
52
- sendHint: "可发送纯文本邮件;普通权限逐次确认,Full Access 不再弹窗。",
53
- identity: "邮箱账号",
54
- identityHint: "先填写日常使用的邮箱信息。多数邮箱要求使用客户端授权码,而不是网页登录密码。",
55
- email: "邮箱地址",
56
- emailHint: "用于收信和发信。",
57
- username: "登录用户名",
58
- usernameHint: "仅当服务商要求时填写;留空则使用邮箱地址。",
59
- fromName: "发件人显示名称",
60
- fromNameHint: "收件人看到的名称;可留空。",
61
- inbox: "收件箱目录",
62
- inboxHint: "通常保持 INBOX。",
63
- password: "密码 / 客户端授权码",
64
- passwordHint: "安全写入 DSH 凭据存储,不会写进 settings.yaml,也不会回显。",
65
- configured: "已保存",
66
- missing: "未填写",
67
- credentialPlaceholder: "输入授权码;已保存时留空不会改变",
68
- clearPassword: "清除授权码",
69
- servers: "邮箱服务器",
70
- serversHint: "选择常见服务商可自动填写;单位邮箱或自建邮箱请选择“手动配置”。",
71
- preset: "邮箱服务商",
72
- custom: "手动配置",
73
- imap: "收信(IMAP)",
74
- smtp: "发信(SMTP)",
75
- host: "服务器地址",
76
- port: "端口",
77
- tls: "加密",
78
- implicit: "隐式 TLS",
79
- starttls: "STARTTLS",
80
- advanced: "高级设置",
81
- advancedHint: "登录用户名、收件箱目录和安全上限通常不需要修改。",
82
- collapse: "收起",
83
- agentAccess: "开放给 Agent",
84
- agentAccessHint: "账号和服务器保存好后,再按需要分别开放读信或发信。",
85
- permissionNeedsSetup: "请先填写邮箱地址、授权码和相应的服务器。",
86
- limits: "安全上限",
87
- bodyLimit: "正文字符",
88
- messageLimit: "整封邮件字节",
89
- attachmentLimit: "附件总字节",
90
- usage: "保存后,在对话里让 Agent“查一下最近邮件”即可验证。插件不会在启动或保存时主动连接邮箱。",
91
- error: "保存失败",
92
- reload: "重新加载",
93
- unavailable: "当前 DSH 连接未提供此设置命名空间。",
94
- readOnly: "当前连接为只读模式,无法保存设置。",
95
- credentialPartial: "授权码已经保存,但其他配置保存失败;请修正后再次保存。"
96
- } : {
97
- nav: "Mail assistant",
98
- title: "Mail assistant",
99
- description: "Let the agent search mail through read-only IMAP and send through SMTP under the current DSH permission preset. This is not a mail client: it never moves, deletes, archives, or changes message state.",
100
- loading: "Loading configuration…",
101
- save: "Save",
102
- saving: "Saving…",
103
- saved: "Saved. New calls use the configuration immediately.",
104
- readPermission: "Allow the agent to read mail",
105
- readHint: "List folders, page through searches, read messages, and download attachments. IMAP always stays read-only.",
106
- sendPermission: "Allow the agent to send mail",
107
- sendHint: "Send plain-text messages. Ordinary permissions ask each time; Full Access does not prompt.",
108
- identity: "Mailbox account",
109
- identityHint: "Start with the mailbox information you normally use. Most providers require an app password instead of the web-login password.",
110
- email: "Email address",
111
- emailHint: "Used for both incoming and outgoing mail.",
112
- username: "Login username",
113
- usernameHint: "Only set this when required by your provider; otherwise the email address is used.",
114
- fromName: "Sender display name",
115
- fromNameHint: "The name recipients see; optional.",
116
- inbox: "Inbox folder",
117
- inboxHint: "Usually keep INBOX.",
118
- password: "Password / app password",
119
- passwordHint: "Stored securely in DSH Credentials. It is never written to settings.yaml or returned to this page.",
120
- configured: "Saved",
121
- missing: "Missing",
122
- credentialPlaceholder: "Enter an app password; leave blank to keep the saved value",
123
- clearPassword: "Clear app password",
124
- servers: "Mail servers",
125
- serversHint: "Choose a common provider to fill these automatically, or use Manual configuration for institutional and self-hosted mail.",
126
- preset: "Mail provider",
127
- custom: "Manual configuration",
128
- imap: "Incoming (IMAP)",
129
- smtp: "Outgoing (SMTP)",
130
- host: "Server address",
131
- port: "Port",
132
- tls: "Encryption",
133
- implicit: "Implicit TLS",
134
- starttls: "STARTTLS",
135
- advanced: "Advanced settings",
136
- advancedHint: "Login username, inbox folder, and safety limits usually need no changes.",
137
- collapse: "Collapse",
138
- agentAccess: "Agent access",
139
- agentAccessHint: "After saving the account and servers, enable reading and sending independently as needed.",
140
- permissionNeedsSetup: "Enter the email address, app password, and corresponding server first.",
141
- limits: "Safety limits",
142
- bodyLimit: "Body characters",
143
- messageLimit: "Whole-message bytes",
144
- attachmentLimit: "Total attachment bytes",
145
- usage: "After saving, ask the agent to find recent mail. The plugin never connects to a mailbox during startup or save.",
146
- error: "Save failed",
147
- reload: "Reload",
148
- unavailable: "This DSH connection does not expose the mail settings namespace.",
149
- readOnly: "This connection exposes settings in read-only mode.",
150
- credentialPartial: "The app password was stored, but the remaining settings failed to save. Fix the form and save again."
151
- };
152
159
  const defaults = {
153
160
  readEnabled: false,
154
161
  sendEnabled: false,
@@ -221,7 +228,7 @@ window.__ModuleLoader__.load({
221
228
  for (const [key, candidate] of Object.entries(presets)) if (Object.entries(candidate).every(([field, expected]) => value[field] === expected)) return key;
222
229
  return "custom";
223
230
  }
224
- function MailSettings({ service }) {
231
+ function MailSettings({ t, service }) {
225
232
  const [draft, setDraft] = (0, react.useState)(null);
226
233
  const [passwordConfigured, setPasswordConfigured] = (0, react.useState)(false);
227
234
  const [credentialWritable, setCredentialWritable] = (0, react.useState)(false);
@@ -282,10 +289,10 @@ window.__ModuleLoader__.load({
282
289
  path: [field],
283
290
  value
284
291
  }));
285
- await service.scope.mutate(ops, snapshot.revision);
286
- setNotice(copy.saved);
292
+ if (await service.scope.mutate(ops, snapshot.revision) === false) throw new Error(t("error"));
293
+ setNotice(t("saved"));
287
294
  } catch (cause) {
288
- setError(passwordStored ? copy.credentialPartial : cause?.message || `${copy.error}`);
295
+ setError(passwordStored ? t("credentialPartial") : cause?.message || `${t("error")}`);
289
296
  } finally {
290
297
  setBusy(false);
291
298
  }
@@ -298,7 +305,7 @@ window.__ModuleLoader__.load({
298
305
  await unwrap(service.credentials.unset(PASSWORD_REF));
299
306
  setPasswordConfigured(false);
300
307
  setPassword("");
301
- setNotice(copy.saved);
308
+ setNotice(t("saved"));
302
309
  } catch (cause) {
303
310
  setError(cause?.message || String(cause));
304
311
  } finally {
@@ -369,10 +376,10 @@ window.__ModuleLoader__.load({
369
376
  };
370
377
  if (!draft) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
371
378
  style: styles.root,
372
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: error || (snapshot.status === "unavailable" ? copy.unavailable : copy.loading) }), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
379
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: error || (snapshot.status === "unavailable" ? t("unavailable") : t("loading")) }), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
373
380
  style: styles.button,
374
381
  onClick: () => load(),
375
- children: copy.reload
382
+ children: t("reload")
376
383
  })]
377
384
  });
378
385
  const credentialReady = passwordConfigured || password !== "";
@@ -420,7 +427,7 @@ window.__ModuleLoader__.load({
420
427
  color: "#a82332",
421
428
  fontSize: 11
422
429
  },
423
- children: copy.permissionNeedsSetup
430
+ children: t("permissionNeedsSetup")
424
431
  })
425
432
  ] })]
426
433
  });
@@ -453,20 +460,20 @@ window.__ModuleLoader__.load({
453
460
  marginTop: 10
454
461
  },
455
462
  children: [
456
- field(copy.host, `${prefix}Host`),
457
- field(copy.port, `${prefix}Port`, "number"),
463
+ field(t("host"), `${prefix}Host`),
464
+ field(t("port"), `${prefix}Port`, "number"),
458
465
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
459
466
  style: styles.label,
460
- children: [copy.tls, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
467
+ children: [t("tls"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
461
468
  value: draft[`${prefix}Tls`],
462
469
  onChange: (event) => set(`${prefix}Tls`, event.currentTarget.value),
463
470
  style: styles.input,
464
471
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
465
472
  value: "implicit",
466
- children: copy.implicit
473
+ children: t("implicit")
467
474
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
468
475
  value: "starttls",
469
- children: copy.starttls
476
+ children: t("starttls")
470
477
  })]
471
478
  })]
472
479
  })
@@ -481,7 +488,7 @@ window.__ModuleLoader__.load({
481
488
  margin: 0,
482
489
  fontSize: 21
483
490
  },
484
- children: copy.title
491
+ children: t("title")
485
492
  }),
486
493
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
487
494
  style: {
@@ -490,7 +497,7 @@ window.__ModuleLoader__.load({
490
497
  fontSize: 12,
491
498
  lineHeight: 1.65
492
499
  },
493
- children: copy.description
500
+ children: t("description")
494
501
  }),
495
502
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
496
503
  style: styles.card,
@@ -500,7 +507,7 @@ window.__ModuleLoader__.load({
500
507
  margin: 0,
501
508
  fontSize: 15
502
509
  },
503
- children: copy.identity
510
+ children: t("identity")
504
511
  }),
505
512
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
506
513
  style: {
@@ -509,12 +516,12 @@ window.__ModuleLoader__.load({
509
516
  fontSize: 11,
510
517
  lineHeight: 1.55
511
518
  },
512
- children: copy.identityHint
519
+ children: t("identityHint")
513
520
  }),
514
521
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
515
522
  style: styles.grid,
516
523
  children: [
517
- field(copy.email, "email", "email", copy.emailHint),
524
+ field(t("email"), "email", "email", t("emailHint")),
518
525
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
519
526
  style: styles.label,
520
527
  children: [
@@ -524,7 +531,7 @@ window.__ModuleLoader__.load({
524
531
  alignItems: "center",
525
532
  gap: 7
526
533
  },
527
- children: [copy.password, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
534
+ children: [t("password"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
528
535
  style: {
529
536
  borderRadius: 999,
530
537
  padding: "2px 7px",
@@ -532,7 +539,7 @@ window.__ModuleLoader__.load({
532
539
  color: passwordConfigured ? "#32724e" : "#9d2f3f",
533
540
  background: passwordConfigured ? "#eaf6ef" : "#f8e9ec"
534
541
  },
535
- children: passwordConfigured ? copy.configured : copy.missing
542
+ children: passwordConfigured ? t("configured") : t("missing")
536
543
  })]
537
544
  }),
538
545
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -540,11 +547,11 @@ window.__ModuleLoader__.load({
540
547
  autoComplete: "new-password",
541
548
  value: password,
542
549
  disabled: busy || !credentialWritable,
543
- placeholder: copy.credentialPlaceholder,
550
+ placeholder: t("credentialPlaceholder"),
544
551
  onChange: (event) => setPassword(event.currentTarget.value),
545
552
  style: styles.input
546
553
  }),
547
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: copy.passwordHint }),
554
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("passwordHint") }),
548
555
  passwordConfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
549
556
  type: "button",
550
557
  disabled: busy || !credentialWritable,
@@ -554,11 +561,11 @@ window.__ModuleLoader__.load({
554
561
  color: "#a82332"
555
562
  },
556
563
  onClick: clearPassword,
557
- children: copy.clearPassword
564
+ children: t("clearPassword")
558
565
  })
559
566
  ]
560
567
  }),
561
- field(copy.fromName, "fromName", "text", copy.fromNameHint)
568
+ field(t("fromName"), "fromName", "text", t("fromNameHint"))
562
569
  ]
563
570
  })
564
571
  ]
@@ -578,7 +585,7 @@ window.__ModuleLoader__.load({
578
585
  margin: 0,
579
586
  fontSize: 15
580
587
  },
581
- children: copy.servers
588
+ children: t("servers")
582
589
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
583
590
  style: {
584
591
  margin: "6px 0 0",
@@ -586,20 +593,20 @@ window.__ModuleLoader__.load({
586
593
  fontSize: 11,
587
594
  lineHeight: 1.55
588
595
  },
589
- children: copy.serversHint
596
+ children: t("serversHint")
590
597
  })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
591
598
  style: {
592
599
  ...styles.label,
593
600
  minWidth: 210
594
601
  },
595
- children: [copy.preset, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
602
+ children: [t("preset"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
596
603
  value: preset,
597
604
  onChange: (event) => applyPreset(event.currentTarget.value),
598
605
  style: styles.input,
599
606
  children: [
600
607
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
601
608
  value: "custom",
602
- children: copy.custom
609
+ children: t("custom")
603
610
  }),
604
611
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
605
612
  value: "gmail",
@@ -629,7 +636,7 @@ window.__ModuleLoader__.load({
629
636
  display: "grid",
630
637
  gap: 11
631
638
  },
632
- children: [server("imap", copy.imap), server("smtp", copy.smtp)]
639
+ children: [server("imap", t("imap")), server("smtp", t("smtp"))]
633
640
  })]
634
641
  }),
635
642
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -651,7 +658,7 @@ window.__ModuleLoader__.load({
651
658
  display: "block",
652
659
  fontSize: 13
653
660
  },
654
- children: copy.advanced
661
+ children: t("advanced")
655
662
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
656
663
  style: {
657
664
  display: "block",
@@ -659,28 +666,28 @@ window.__ModuleLoader__.load({
659
666
  color: colors.secondary,
660
667
  fontSize: 11
661
668
  },
662
- children: copy.advancedHint
663
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: advancedOpen ? copy.collapse : "›" })]
669
+ children: t("advancedHint")
670
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: advancedOpen ? t("collapse") : "›" })]
664
671
  }), advancedOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
665
672
  style: { marginTop: 14 },
666
673
  children: [
667
674
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
668
675
  style: styles.grid,
669
- children: [field(copy.username, "username", "text", copy.usernameHint), field(copy.inbox, "inboxFolder", "text", copy.inboxHint)]
676
+ children: [field(t("username"), "username", "text", t("usernameHint")), field(t("inbox"), "inboxFolder", "text", t("inboxHint"))]
670
677
  }),
671
678
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
672
679
  style: {
673
680
  margin: "17px 0 10px",
674
681
  fontSize: 13
675
682
  },
676
- children: copy.limits
683
+ children: t("limits")
677
684
  }),
678
685
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
679
686
  style: styles.grid,
680
687
  children: [
681
- field(copy.bodyLimit, "maxBodyChars", "number"),
682
- field(copy.messageLimit, "maxMessageBytes", "number"),
683
- field(copy.attachmentLimit, "maxAttachmentBytes", "number")
688
+ field(t("bodyLimit"), "maxBodyChars", "number"),
689
+ field(t("messageLimit"), "maxMessageBytes", "number"),
690
+ field(t("attachmentLimit"), "maxAttachmentBytes", "number")
684
691
  ]
685
692
  })
686
693
  ]
@@ -694,7 +701,7 @@ window.__ModuleLoader__.load({
694
701
  margin: 0,
695
702
  fontSize: 15
696
703
  },
697
- children: copy.agentAccess
704
+ children: t("agentAccess")
698
705
  }),
699
706
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
700
707
  style: {
@@ -703,11 +710,11 @@ window.__ModuleLoader__.load({
703
710
  fontSize: 11,
704
711
  lineHeight: 1.55
705
712
  },
706
- children: copy.agentAccessHint
713
+ children: t("agentAccessHint")
707
714
  }),
708
715
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
709
716
  style: styles.grid,
710
- children: [permission("readEnabled", copy.readPermission, copy.readHint, readReady), permission("sendEnabled", copy.sendPermission, copy.sendHint, sendReady)]
717
+ children: [permission("readEnabled", t("readPermission"), t("readHint"), readReady), permission("sendEnabled", t("sendPermission"), t("sendHint"), sendReady)]
711
718
  })
712
719
  ]
713
720
  }),
@@ -717,7 +724,7 @@ window.__ModuleLoader__.load({
717
724
  fontSize: 11,
718
725
  lineHeight: 1.55
719
726
  },
720
- children: copy.usage
727
+ children: t("usage")
721
728
  }),
722
729
  !snapshot.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
723
730
  role: "status",
@@ -725,7 +732,7 @@ window.__ModuleLoader__.load({
725
732
  color: colors.secondary,
726
733
  fontSize: 12
727
734
  },
728
- children: copy.readOnly
735
+ children: t("readOnly")
729
736
  }),
730
737
  error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
731
738
  role: "alert",
@@ -748,21 +755,24 @@ window.__ModuleLoader__.load({
748
755
  disabled: busy || !snapshot.writable,
749
756
  onClick: save,
750
757
  style: styles.primary,
751
- children: busy ? copy.saving : copy.save
758
+ children: busy ? t("saving") : t("save")
752
759
  })
753
760
  ]
754
761
  });
755
762
  }
756
763
  function apply(ctx) {
764
+ ctx.effect(() => ctx.locale.register(LOCALE_NS, dictionaries), "mail: dictionaries");
765
+ const t = ctx.locale.bind(LOCALE_NS);
757
766
  const service = {
758
- scope: ctx.settingsScope.bind({ namespace: NS }),
767
+ scope: ctx.configForms.get(NS),
759
768
  credentials: ctx.remote.credentials
760
769
  };
761
770
  ctx.slots.inject("settings.section", () => ctx.slots.register({
762
771
  name: "settings.section",
763
772
  id: "mail-assistant",
773
+ locale: LOCALE_NS,
764
774
  order: 25,
765
- label: () => copy.nav,
775
+ label: () => t("nav"),
766
776
  inject: () => ({ service })
767
777
  }, MailSettings));
768
778
  }