@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.
- package/CHANGELOG.en.md +6 -0
- package/CHANGELOG.md +6 -0
- package/CODE_OF_CONDUCT.en.md +1 -1
- package/CODE_OF_CONDUCT.md +1 -1
- package/MAINTAINERS.en.md +1 -1
- package/MAINTAINERS.md +1 -1
- package/README.md +11 -13
- package/{README.en.md → README_EN.md} +11 -13
- package/SECURITY.en.md +1 -1
- package/SECURITY.md +1 -1
- package/SUPPORT.en.md +1 -1
- package/SUPPORT.md +1 -1
- package/THIRD_PARTY_NOTICES.en.md +2 -0
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/docs/EDUWORK-MIGRATION.en.md +2 -6
- package/docs/EDUWORK-MIGRATION.md +2 -6
- package/docs/architecture.en.md +3 -3
- package/docs/architecture.md +3 -3
- package/docs/compatibility.en.md +5 -6
- package/docs/compatibility.md +5 -6
- package/docs/getting-started.en.md +8 -8
- package/docs/getting-started.md +8 -8
- package/docs/release-checklist.en.md +7 -49
- package/docs/release-checklist.md +7 -49
- package/docs/security-model.en.md +4 -0
- package/docs/security-model.md +4 -0
- package/lib/client.js +167 -157
- package/lib/index.js +171 -36
- package/package.json +26 -17
|
@@ -1,53 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 包发布检查表
|
|
2
2
|
|
|
3
3
|
**简体中文** | [English](release-checklist.en.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
源码与发布入口统一在 EduWork;遵循[公共包发布流程](https://github.com/ecnu/EduWork/blob/main/docs/PACKAGES.md),本包保留独立的 npm 名称和 SemVer。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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:
|
package/docs/security-model.md
CHANGED
|
@@ -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
|
-
"
|
|
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(
|
|
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 ?
|
|
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(
|
|
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" ?
|
|
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:
|
|
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:
|
|
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(
|
|
457
|
-
field(
|
|
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: [
|
|
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:
|
|
473
|
+
children: t("implicit")
|
|
467
474
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
468
475
|
value: "starttls",
|
|
469
|
-
children:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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: [
|
|
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 ?
|
|
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:
|
|
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:
|
|
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:
|
|
564
|
+
children: t("clearPassword")
|
|
558
565
|
})
|
|
559
566
|
]
|
|
560
567
|
}),
|
|
561
|
-
field(
|
|
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:
|
|
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:
|
|
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: [
|
|
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:
|
|
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",
|
|
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:
|
|
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:
|
|
663
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: advancedOpen ?
|
|
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(
|
|
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:
|
|
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(
|
|
682
|
-
field(
|
|
683
|
-
field(
|
|
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:
|
|
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:
|
|
713
|
+
children: t("agentAccessHint")
|
|
707
714
|
}),
|
|
708
715
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
709
716
|
style: styles.grid,
|
|
710
|
-
children: [permission("readEnabled",
|
|
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:
|
|
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:
|
|
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 ?
|
|
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.
|
|
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: () =>
|
|
775
|
+
label: () => t("nav"),
|
|
766
776
|
inject: () => ({ service })
|
|
767
777
|
}, MailSettings));
|
|
768
778
|
}
|