dsh-email 0.14.7 → 0.15.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.
@@ -0,0 +1,219 @@
1
+ # dsh-email usage guide
2
+
3
+ [Overview](../README.en.md) · [Changelog](../CHANGELOG.md) · [Validation](VALIDATION.md)
4
+
5
+ ## Tools
6
+
7
+ See the [draft workbench](DRAFTS.en.md) for editable drafts and per-account trusted recipients. The new workbench was validated with official Harness 0.2.0-rc.2.
8
+
9
+ | Tool | Purpose |
10
+ |---|---|
11
+ | `email_list` | List the newest mail in a folder (unread filter, pagination, summaries only, no body) |
12
+ | `email_read` | Read one message's full text by uid (HTML auto-converted to plain text, oversized bodies truncated) |
13
+ | `email_search` | Search subject/sender/recipient/CC by keyword (server-side subject/from/to/cc; hits are re-checked against the envelopes, so "match-everything" servers such as QQ are rejected); when nothing believable is returned, it falls back to a body scan of the most recent 30 messages by default (including to/cc) |
14
+ | `email_send` | Send mail on your behalf (attachments supported). **Prompts for confirmation before sending by default**, showing recipients, subject and attachment count; only sends after you approve |
15
+ | `email_draft` | Prepare a local draft without sending; edit, save, inspect the preview and manually confirm in Settings → Email → Drafts |
16
+ | `email_folders` | List the mailbox folders (INBOX/Sent/Junk/custom…); feed the `path` to other tools |
17
+ | `email_attachment` | Download an attachment by index (saved to the session workspace by default so the model can read it directly; size capped by `maxAttachmentBytes`) |
18
+ | `email_health` | Check account configuration and IMAP/SMTP host details offline; makes no network connection. Use Test connection in Web settings to verify IMAP connectivity |
19
+ | `email_watch` | Incremental new-mail check: the first call seeds a baseline, every later call reports only unread mail newer than the last check — ideal for scheduled new-mail notifications |
20
+ | `email_mark` | Mark messages as read/unread, add/remove stars, or move messages to another folder |
21
+ | `email_reply` | Reply, reply-all or forward with thread headers and quoted content; uses the same send-approval gate |
22
+
23
+ ### New-mail notifications (web UI)
24
+
25
+ Once an account is configured, a "whale-girl courier" popup lives in the bottom-right corner of the main UI: it checks for new mail every 30 seconds and shows a card (sender + subject) when something arrives, auto-dismissing after 12 seconds. The popup shares the cursor logic with `email_watch` but keeps its own counter, so they never consume each other's mail.
26
+
27
+ The popup artwork prefers the locally installed [dsh-deep-whale](https://github.com/Small-tailqwq/dsh-deep-whale) whale-girl skin assets (**never bundled** — read at runtime from your own installation): the artwork is a derivative of the original whale-girl character by [上善](https://www.pixiv.net/users/62155430) (skin by Small-tailqwq), published under CC BY-NC-SA 4.0 (Attribution-NonCommercial-ShareAlike); the popup shows the full attribution chain. Without the skin, a bundled community whale-girl artwork is used (copyright stays with the original author, personal non-commercial use only; removed on request via issue).
28
+
29
+ Example:
30
+
31
+ > Check the 10 newest unread messages in my QQ mailbox and list the ones that need a reply.
32
+
33
+ ## Installation
34
+
35
+ ```sh
36
+ dsh plugin --profile web add dsh-email
37
+ ```
38
+
39
+ (Or install a specific GitHub commit: `dsh plugin --profile web add github:STARDUSTLC666/dsh-email#<commit>`. The repository includes prebuilt `lib/` artifacts; no `prepare` build is required.)
40
+
41
+ After installing, restart `dsh web`. The plugin ships with an empty config and **won't crash startup**; calling any email tool before configuration returns a clear configuration hint.
42
+
43
+ **Two configuration methods (pick one):**
44
+
45
+ 1. **Web settings (recommended)**: after restart, open **Settings → Mail (dsh-email)**, fill in the email address and authorization code in an account card — edits auto-save, no button to click; each card also has its own "Test connection" button. Zero YAML, zero restart.
46
+ 2. **YAML**: hand-write the `accounts` map in the cordis.patch.yml template below. The settings page's `accountsYaml` is written by the card editor (it overrides `accounts` when non-empty); the panel itself no longer provides a raw-YAML textarea. Fields not modeled by cards (e.g. `socketTimeoutMs`, `connectionTimeoutMs`) can still be hand-written in YAML and are preserved in place when cards are saved. Authentication method (`authKind`) has a selector on the card — no need to hand-write it.
47
+
48
+ The whole settings page follows DSH's light and dark themes: every panel style references the official `--dsw-alias-*` design tokens instead of hard-coded colors, so switching themes takes effect immediately (0.10.8 referenced a border token that does not exist, which produced a glaring light-gray border in dark mode; that is fixed).
49
+
50
+ Multiple accounts can be edited visually in the settings page: account cards add, edit and delete accounts, rename them, pick the default, and run "Test connection" per account name; a half-filled account never blocks saving — it just gets an "incomplete" badge. Card edits are debounced and auto-saved; there is no longer a "write to YAML text, then click save" step. Version conflicts (settings changed elsewhere) are automatically rebased and re-saved once, rather than repeatedly failing with a stale revision. When a card is saved, an already-stored authorization code is kept by default (leave the password field empty to keep it, type into it to overwrite); comments in the YAML are preserved in place where possible — with an explicit notice when they cannot be. Rename re-keys in place, preserving auth codes and advanced keys, and refuses to overwrite an existing account name. Account-level hand-written imap/smtp endpoints are only cleaned when the **provider actually changes** — runtime resolution prefers the account's own host, so a routine save never silently re-points the connection target.
51
+
52
+ On Harness 0.1.7, the page saves directly to the current profile’s `tool-email` entry and applies changes live. Installations using the default entry automatically import the retired `dsh-email` section from `settings.yaml` or `settings.yaml.imported` once. Existing profile values win, and the original file is preserved. Older hosts retain their original settings storage. Authorization-code fields are marked secret, but saving a filled field still writes its value to the local settings file. For a single account, set `DSH_EMAIL_PASSWORD` and leave the authorization-code field empty to avoid saving it; the environment value is not copied into settings.
53
+
54
+ ## Uninstall
55
+
56
+ ```bash
57
+ dsh plugin --profile web remove dsh-email
58
+ ```
59
+
60
+ Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
61
+
62
+ ## Configuration
63
+
64
+ In your profile's `cordis.patch.yml` (under `$DSH_HOME/profiles/<name>/`), override the `tool-email` line, then restart:
65
+
66
+ ```yaml
67
+ - id: tool-email
68
+ config:
69
+ provider: qq # qq | 163 | 126 | sina | aliyun | gmail | outlook | icloud
70
+ user: you@qq.com
71
+ password: 你的授权码 # 强烈建议改用环境变量 DSH_EMAIL_PASSWORD,见下
72
+ ```
73
+
74
+ No preset needed? Hand-write any IMAP/SMTP server:
75
+
76
+ ```yaml
77
+ - id: tool-email
78
+ config:
79
+ user: you@corp.example
80
+ password: 你的授权码
81
+ imap: { host: imap.corp.example, port: 993, secure: true }
82
+ smtp: { host: smtp.corp.example, port: 465, secure: true }
83
+ inboxFolder: INBOX
84
+ ```
85
+
86
+ Multiple accounts: one `tool-email` line can hold several mailboxes; select one with the `account` argument when calling a tool:
87
+
88
+ ```yaml
89
+ - id: tool-email
90
+ config:
91
+ accounts:
92
+ work: { provider: qq, user: work@qq.com, password: 授权码1 }
93
+ home: { provider: '163', user: home@163.com, password: 授权码2 }
94
+ defaultAccount: work # 省略 account 参数时用这个
95
+ downloadDir: E:/attachments # 可选,默认 $DSH_HOME/email-downloads
96
+ ```
97
+
98
+ Top-level `provider`/`user`/`password`/`imap`/`smtp`/`inboxFolder` remain available as shared defaults for all accounts (the v0.1 single-account style stays fully compatible).
99
+
100
+ To reuse one set of connection endpoints across accounts, define your own provider presets with `serverPresets` (a YAML map: key = preset name, value has an optional `label` plus `imap`/`smtp`):
101
+
102
+ ```yaml
103
+ - id: tool-email
104
+ config:
105
+ serverPresets: |
106
+ corp:
107
+ label: 公司邮箱
108
+ imap: { host: imap.corp.example, port: 993, secure: true }
109
+ smtp: { host: smtp.corp.example, port: 465, secure: true }
110
+ ```
111
+
112
+ The settings page's "Server presets" fold-out edits these presets visually, and the account cards' provider dropdown automatically gains the preset names — picking one pre-fills its endpoints into the account. A preset holds connection endpoints only — **never an email address or authorization code**. `port`/`secure` may be omitted (defaults are 993/465 with SSL).
113
+
114
+ ## Presets
115
+
116
+ | provider | IMAP | SMTP |
117
+ |---|---|---|
118
+ | `qq` | imap.qq.com:993 (SSL) | smtp.qq.com:465 (SSL) |
119
+ | `163` | imap.163.com:993 | smtp.163.com:465 |
120
+ | `126` | imap.126.com:993 | smtp.126.com:465 |
121
+ | `sina` | imap.sina.com:993 | smtp.sina.com:465 |
122
+ | `aliyun` | imap.aliyun.com:993 | smtp.aliyun.com:465 |
123
+ | `gmail` | imap.gmail.com:993 | smtp.gmail.com:465 |
124
+ | `outlook` | outlook.office365.com:993 | smtp.office365.com:587 (STARTTLS) |
125
+ | `icloud` | imap.mail.me.com:993 | smtp.mail.me.com:587 (STARTTLS) |
126
+
127
+ ### Full configuration options
128
+
129
+ | Field | Default | Description |
130
+ |---|---|---|
131
+ | `provider` | — | Preset name; auto-fills imap/smtp addresses. Explicitly written host/port/secure take precedence |
132
+ | `user` | required | Login email address |
133
+ | `password` | required* | Authorization code / app-specific password; *can also use the env var `DSH_EMAIL_PASSWORD` |
134
+ | `senderName` | — | Display name for the From header; the address itself stays `user` |
135
+ | `authUser` | = `user` | Login account. Alias / SMTP-relay setups: `user` is the address mail is sent from, this is the account IMAP/SMTP authenticates with |
136
+ | `authPassword` | = `password` | Password for `authUser`; only needed when the login account differs from `user` and has its own password |
137
+ | `imap.host/port/secure` | per preset | Incoming server (also `connectionTimeoutMs` / `socketTimeoutMs` for timeouts) |
138
+ | `smtp.host/port/secure` | per preset | Outgoing server |
139
+ | `inboxFolder` | `INBOX` | Default folder for read/send tools |
140
+ | `sendApproval` | `true` | Confirm before sending (strongly recommended) |
141
+ | `maxBodyChars` | `20000` | Body truncation limit for `email_read` (1000–200000) |
142
+ | `accounts` | — | Named account map; account-level fields override top-level shorthand |
143
+ | `accountsYaml` | — | YAML text of the account map, written by the settings-page card editor; overrides `accounts` when non-empty |
144
+ | `clientId` | built-in community app (below) | Application (client) ID for OAuth2 accounts: empty uses the plugin's built-in public client, a value overrides it (account-level also overrides the top-level shorthand) |
145
+ | `authKind` | derived from provider | Authentication method override: `oauth2` / `password`. When omitted, derived from provider and IMAP host; hybrid or on-premises tenants that still accept app passwords for Exchange Online can pin `password`. The card's "Authentication method" selector writes this key |
146
+ | `serverPresets` | — | YAML text of custom provider presets (key = preset name, value has optional `label` + `imap`/`smtp`); endpoints only, no credentials. The settings-page dropdown lists preset names and pre-fills endpoints into account cards; editing a preset does not reconnect established connections |
147
+ | `defaultAccount` | auto (single account) | Account used when the `account` argument is omitted (required for multi-account) |
148
+ | `downloadDir` | `.dsh-email-downloads` under session workspace (fallback `$DSH_HOME/email-downloads`) | Download directory for `email_attachment`; pinned once explicitly set |
149
+ | `maxAttachmentBytes` | 20 MiB | Per-attachment and total size cap (1024–512 MiB) |
150
+ | `idleTimeoutMs` | `60000` | IMAP idle connection recycle time (connection reuse; consecutive ops are faster) |
151
+ | `bodySearchFallback` | `true` | Fall back to client-side body scan of recent messages when server search returns nothing |
152
+ | `bodySearchLimit` | `30` | Number of messages for the body fallback scan (5–200) |
153
+
154
+ ## Getting an authorization code
155
+
156
+ Every provider requires an authorization code / app-specific password instead of your login password:
157
+
158
+ - **QQ Mail**: Settings → Account → enable IMAP/SMTP → generate authorization code
159
+ - **163/126**: Settings → POP3/SMTP/IMAP → enable → add authorization code
160
+ - **Gmail**: enable 2-Step Verification → Security → App passwords
161
+ - **Outlook**: Microsoft account security → App password (some accounts need 2-step verification first)
162
+
163
+ ## Security
164
+
165
+ - **The authorization code is the key to your mailbox.** It lives on this machine (`cordis.patch.yml` or `settings.yaml` in the profile); never commit it to any Git repo; prefer the `DSH_EMAIL_PASSWORD` env var.
166
+ - `email_send` uses the DSH approval channel by default and refuses sends when no approval UI is available. Explicitly enabled, saved [trusted rules](DRAFTS.en.md) skip plugin approval only when every To/Cc address matches; host permissions still apply.
167
+ - **Full Access** uses the host's never approval policy. Legacy sends requiring individual approval remain blocked; change the access mode or use the new draft page to review and confirm manually. `sendApproval: false` globally disables plugin send approval.
168
+ - This plugin performs no outbound telemetry; credentials are used in memory only to connect to your mail servers.
169
+
170
+ ## Outlook OAuth2 (device-code login)
171
+
172
+ Microsoft has disabled username+password basic auth for Exchange Online: personal outlook.com accounts and most tenants now require OAuth2. This plugin supports the device-code flow — IMAP and SMTP share a single token with automatic refresh.
173
+
174
+ **Where the bundled application ID comes from**: the device-code flow needs an app registration, and making every user register one is a wall nobody should have to climb — so the plugin ships one: `15dcd5aa-00dd-487f-82d7-1d2b2c299e14`, registered by contributor [gurio-wine](https://github.com/gurio-wine) in [PR #13](https://github.com/STARDUSTLC666/dsh-email/pull/13) and used here with their permission — thank you. The cost is stated plainly: the Microsoft consent screen names **their** application (enterprise security teams may refuse it), and the sign-in logs and telemetry land in **their** tenant, including your UPN. If they ever delete the app, every account that did not bring its own id stops signing in at once, with no better error than "clientId may be wrong". **Registering your own (free, ~10 minutes) keeps you independent — paste it into the card to override the built-in value; leaving the field empty keeps using the bundled one.**
175
+
176
+ **Using your own application instead (optional, free, ~10 minutes)**:
177
+
178
+ 1. Open the [Entra admin center](https://entra.microsoft.com/) → **App registrations** → **New registration**.
179
+ 2. Under **Supported account types**, select "Accounts in any organizational directory and personal Microsoft accounts" — this determines whether personal outlook.com accounts can sign in. Choosing incorrectly yields `AADSTS700016` or `AADSTS50020`.
180
+ 3. Leave the **Redirect URI** blank (the device-code flow does not need one). Click Register.
181
+ 4. On the Overview page, copy the **Application (client) ID** — this is the `clientId` you will supply.
182
+ 5. In the left sidebar, go to **Authentication** → scroll to the bottom → set **Allow public client flows** to **Yes** and save. Without this, login fails with an `AADSTS700028`-style "device-code flow not enabled" error.
183
+ 6. In the left sidebar, go to **API permissions** → Add a permission → Microsoft Graph → **Delegated permissions** → check `IMAP.AccessAsUser.All`, `SMTP.Send`, and `offline_access` (the last one is essential for obtaining a refresh token — without it, every expiry forces a fresh login). Personal tenants generally need no admin consent; enterprise tenants may require an admin to click "Grant admin consent" once.
184
+
185
+ **Filling it into the plugin (overrides the built-in value)**: Settings → Mail (dsh-email) → the account card's "Application (client) ID" field (the card shows which application is in effect; empty keeps the bundled community app); or in YAML (account-level `clientId`, which can also be set at the top level as a default for all accounts).
186
+
187
+ **Login flow**: click "Sign in to Microsoft account" on the card → the panel shows a `microsoft.com/devicelogin` link and a code → open the link in a browser, enter the code, and complete authorization → the panel polls until it shows "Signed in: your@email". Both receiving and sending then use this token.
188
+
189
+ **Caveats**:
190
+
191
+ - Enterprise tenants may additionally require an admin to enable **IMAP** and **SMTP AUTH** for the mailbox in the Exchange admin center. The typical symptom of SMTP AUTH being off: receiving works fine, sending is rejected.
192
+ - The token is bound to the application ID that issued it: changing `clientId` is treated as "switched apps" and requires re-login (this is intentional — it prevents using the old app's credentials against the new one). The bundled app is shared by every account that names none: a future release that replaces it with the project's own registration will ask those accounts to sign in once more.
193
+ - If your tenant is hybrid or on-premises and SMTP AUTH is still enabled, app passwords work: select "Password / authorization code" in the card's "Authentication method" selector — no OAuth2 needed.
194
+
195
+ ## Known limitations
196
+
197
+ - **OAuth2 covers Outlook / Exchange Online only, and works out of the box (your own app ID optional)**: device-code login supports both IMAP and SMTP and defaults to the bundled community application (see "Outlook OAuth2" above), so no registration is needed to sign in; if your organisation refuses third-party apps, paste your own `clientId` into the card to override it. Other environments that mandate OAuth (e.g. Google Workspace) remain unusable; use the provider's app-specific password / authorization code instead.
198
+ - **Search match counts**: server hits are re-checked against the envelopes (see `email_search` above); when they hold up, "N matches" is the count the server reported while every listed row really carries the keyword. The local body-scan fallback only looked at the newest `bodySearchLimit` messages, so it renders "N rows on this page (only the newest N scanned)" instead of "N matches".
199
+ - **Body search**: the server side only searches subject / from / to / cc. Most servers (e.g. QQ) have unreliable IMAP `TEXT` / `HEADER` search, so with no results it falls back to a body scan of the most recent `bodySearchLimit` messages (slower; disable with `bodySearchFallback`).
200
+ - **Attachments**: inline images aren't downloadable separately yet; a failed attachment match errors instead of downloading the wrong file (safe default).
201
+ - **Password storage**: the authorization code saved in the settings page is written in plaintext to the local `settings.yaml` (the secret mark only keeps it out of logs / exports / diagnostics; no disk encryption). Don't hand `settings.yaml` to untrusted people.
202
+ - **OAuth2 token storage**: access / refresh tokens are stored as plaintext JSON at `$DSH_HOME/data/dsh-email/oauth2-tokens.json` (deliberately kept out of `settings.yaml`, so they are not included in settings exports). The file is written with `mode: 0o600`, but that permission bit only takes effect **at file creation** and is **effectively a no-op on Windows** — do not hand this file to untrusted people. Tokens are bound to the issuing application ID; changing `clientId` requires re-login. Deleting an account cleans up its tokens.
203
+ - **Local edits are undone by `pnpm install`**: if you deploy by editing files inside `node_modules/dsh-email/`, any `pnpm install` restores the registry version (0.10.7, for example). To keep changes long-term, install from a local path or a Git commit instead.
204
+
205
+ ## Development
206
+
207
+ ```sh
208
+ pnpm install
209
+ pnpm run build # tsc → lib/
210
+ pnpm test # build + offline tests; no real mailbox required
211
+ ```
212
+
213
+ `src/index.ts` composes the plugin. `runtime.ts` owns live settings, account pools, and separate tool/web watch cursors. `tools.ts` wires the ten tool implementations. `tool-contract.ts` defines parameters, output schemas, and text rendering. `approval.ts` owns the outgoing-mail gate. IMAP/SMTP transport remains in `mail-client.ts`, and browser routes remain in `web.ts` — which also hosts the parsing, serialization (comment-preserving, keeping stored authorization codes) and custom-preset snapshot the account-card editor depends on.
214
+
215
+ Tests cover pool replacement after live settings changes, unload cleanup, cancellation and workspace propagation, independent tool/web cursors, account-card serialization and preset parsing, and rejected approval preventing send execution. In-memory clients replace mailbox connections.
216
+
217
+ ## License
218
+
219
+ MIT. This is a community plugin, not affiliated with DeepSeek; `@deepseek-ai/*` is an officially reserved namespace.
package/docs/USAGE.md ADDED
@@ -0,0 +1,225 @@
1
+ # dsh-email 使用说明
2
+
3
+ [返回简介](../README.md) · [更新记录](../CHANGELOG.md) · [验证记录](VALIDATION.md)
4
+
5
+ ## 工具一览
6
+
7
+ 可编辑草稿和按账号的可信收件人规则见[草稿工作台](DRAFTS.md)。新工作台已用官方 Harness 0.2.0-rc.2 验收。
8
+
9
+ | 工具 | 作用 |
10
+ |---|---|
11
+ | `email_list` | 列出文件夹里最新的邮件(未读过滤、分页、只看摘要不带正文) |
12
+ | `email_read` | 按 uid 读取一封邮件的全文(HTML 邮件自动转纯文本,超长截断) |
13
+ | `email_search` | 按关键词搜索主题/发件人/收件人/抄送(服务器端 subject/from/to/cc;命中会先用信封复核,QQ 这种"什么都匹配"的响应会被判无效);复核或服务器都没给出可信结果时,默认回退到最近 30 封的正文扫描(含 to/cc) |
14
+ | `email_send` | 代发邮件(支持带附件)。**默认发信前会弹确认**,显示收件人、主题和附件数,由你批准后才发出 |
15
+ | `email_draft` | 准备本地草稿,不发送;用户在「设置 → 邮件 → 草稿」编辑、保存、检查预览后手动确认发送 |
16
+ | `email_folders` | 列出邮箱的文件夹(INBOX/已发送/垃圾邮件/自定义…),拿 path 喂给其他工具 |
17
+ | `email_attachment` | 按序号下载邮件附件(默认存到会话工作区,模型可直接读取;大小受 maxAttachmentBytes 限制) |
18
+ | `email_health` | 离线检查账号配置及 IMAP/SMTP 主机信息;不建立网络连接,实际 IMAP 连通性使用设置页的“测试连接” |
19
+ | `email_watch` | 增量检查新邮件:首次调用建立基线,之后每次只报告比上次多出来的未读邮件,适合定时任务做新邮件提醒 |
20
+ | `email_mark` | 修改邮件状态:标记已读/未读、加/取消星标,或移动到别的文件夹(归档、丢回收站),收发闭环的「收完之后」那一半 |
21
+ | `email_reply` | 回复/回复全部/转发已有邮件:自动带上 In-Reply-To/References 线程头与原文引文,收件人自动排除自己,主题不重复叠 Re:/Fwd:;同样走发信审批门 |
22
+
23
+ ### 新邮件提醒(Web 端)
24
+
25
+ 配置好账号后,主界面右下角会出现「鲸鱼娘递信」小弹窗:每 30 秒检查一次新邮件,有新邮件时弹出卡片(发件人 + 主题),12 秒自动消失。弹窗与 `email_watch` 工具共用同一套游标逻辑但各自独立计数,互不抢占。
26
+
27
+ 弹窗形象优先使用本地安装的 [dsh-deep-whale](https://github.com/Small-tailqwq/dsh-deep-whale) 鲸鱼娘皮肤素材(**不打包分发**,运行时从你自己的安装目录读取):该素材为一创 [上善](https://www.pixiv.net/users/62155430) 鲸鱼娘形象的衍生创作(二创 Small-tailqwq),以 CC BY-NC-SA 4.0(署名-非商业性使用-相同方式共享)发布,弹窗内附完整署名链。未安装皮肤时使用内置的社区鲸鱼娘形象(版权归原作者,仅供个人非商业使用;如有异议请提 Issue,会立即移除)。
28
+
29
+ 示例对话:
30
+
31
+ > 帮我看下 QQ 邮箱最新的 10 封未读,把要回复的列出来。
32
+
33
+ ## 安装
34
+
35
+ ```sh
36
+ dsh plugin --profile web add dsh-email
37
+ ```
38
+
39
+ (或从 GitHub 安装指定提交:`dsh plugin --profile web add github:STARDUSTLC666/dsh-email#<commit>`。仓库已包含 `lib/` 构建产物,无需 `prepare` 构建。)
40
+
41
+ 装好后重启 `dsh web`。插件自带空配置,**不会弄崩启动**;配置前调用任何 email 工具都会返回明确的配置提示。
42
+
43
+ **配置方式有两种(任选其一):**
44
+
45
+ 1. **网页设置(推荐)**:重启后打开 **设置 → 邮件 (dsh-email)**,在账号卡片里填邮箱地址和授权码——改动即自动保存,无需再点按钮;每张卡片还能单独「测试连接」。零 YAML、零重启。
46
+ 2. **YAML**:按下面的 cordis.patch.yml 模板手写 `accounts` 映射。设置页的 `accountsYaml` 由卡片编辑器写入(非空时覆盖 `accounts`),面板本身不再提供 YAML 原文文本框;卡片不建模的字段(如 `socketTimeoutMs`、`connectionTimeoutMs`)仍可在 YAML 里手写,保存卡片时会原地保留。认证方式(`authKind`)已在卡片上提供选择器,无需手写。
47
+
48
+ 设置页整体跟随 DSH 的深浅主题:面板样式全部引用官方 `--dsw-alias-*` 设计变量、不写死颜色,切换浅色/深色即时生效(0.10.8 曾引用一个并不存在的边框变量,暗色下会出现刺眼的浅灰边框,已修掉)。
49
+
50
+ 多账号可以在设置页可视化编辑:账号卡片支持增删改账号、改名、设默认、按账号名单独「测试连接」;没填完的账号不阻断保存,只标一个「未完成」。卡片改动即时防抖落盘,不再有"先写 YAML 文本、再点一次保存"这一步;版本冲突(别处也改了设置)会自动重基后重存一次,而不是拿旧版本号反复失败。保存卡片时,已存的授权码默认保持(密码栏留空 = 不变,填内容 = 覆盖);YAML 里的注释尽量原地保留,实在保不住时会明确提示。改名走的是原地改键,授权码与高级键一并保留,且不允许改成已有账号名(那会顶掉另一个账号)。账号自己手写的 imap/smtp 端点只在**服务商真的换了**时才清洗——运行时以账号自己的 host 优先,普通保存不会悄悄改动连接目标。
51
+
52
+ Harness 0.1.7 中,设置页直接保存到当前 profile 的 `tool-email` 配置行,修改立即生效。使用默认插件行的旧安装会自动导入 `settings.yaml` 或 `settings.yaml.imported` 中的 `dsh-email` 设置;已有 profile 配置优先,原文件保留且只迁移一次,无需重新输入账号。旧版宿主仍使用原 settings 存储。授权码字段标记为 secret,但填写后保存仍会写入本机配置文件。单账号如需避免保存授权码,可设置 `DSH_EMAIL_PASSWORD` 并将授权码栏留空;环境变量不会被复制进设置文件。
53
+
54
+ ## 卸载
55
+
56
+ ```bash
57
+ dsh plugin --profile web remove dsh-email
58
+ ```
59
+
60
+ 卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
61
+
62
+ ## 配置
63
+
64
+ 在你 profile 的 `cordis.patch.yml` 里覆盖 `tool-email` 行(在 `$DSH_HOME/profiles/<name>/` 下),然后重启:
65
+
66
+ ```yaml
67
+ - id: tool-email
68
+ config:
69
+ provider: qq # qq | 163 | 126 | sina | aliyun | gmail | outlook | icloud
70
+ user: you@qq.com
71
+ password: 你的授权码 # 强烈建议改用环境变量 DSH_EMAIL_PASSWORD,见下
72
+ ```
73
+
74
+ 不需要预设?手填任意 IMAP/SMTP 服务器即可:
75
+
76
+ ```yaml
77
+ - id: tool-email
78
+ config:
79
+ user: you@corp.example
80
+ password: 你的授权码
81
+ imap: { host: imap.corp.example, port: 993, secure: true }
82
+ smtp: { host: smtp.corp.example, port: 465, secure: true }
83
+ inboxFolder: INBOX
84
+ ```
85
+
86
+ 多账号:一个 `tool-email` 行可以配多个邮箱,工具调用时用 `account` 参数选择:
87
+
88
+ ```yaml
89
+ - id: tool-email
90
+ config:
91
+ accounts:
92
+ work: { provider: qq, user: work@qq.com, password: 授权码1 }
93
+ home: { provider: '163', user: home@163.com, password: 授权码2 }
94
+ defaultAccount: work # 省略 account 参数时用这个
95
+ downloadDir: E:/attachments # 可选,默认 $DSH_HOME/email-downloads
96
+ ```
97
+
98
+ 顶层的 `provider`/`user`/`password`/`imap`/`smtp`/`inboxFolder` 仍兼容旧单账号写法。具名账号声明自己的 `provider` 时,服务器取该预设,账号自己的 `imap`/`smtp` 显式覆盖优先;未声明服务商的账号继续继承顶层服务器。共享连接超时仍可生效。
99
+
100
+ 想在多个账号之间复用同一套连接端点,可以用 `serverPresets` 自定义服务商预设(YAML 映射,键 = 预设名,值含可选的 `label` 与 `imap`/`smtp`):
101
+
102
+ ```yaml
103
+ - id: tool-email
104
+ config:
105
+ serverPresets: |
106
+ corp:
107
+ label: 公司邮箱
108
+ imap: { host: imap.corp.example, port: 993, secure: true }
109
+ smtp: { host: smtp.corp.example, port: 465, secure: true }
110
+ ```
111
+
112
+ 设置页的「服务器预设」折叠区能可视化增删改这些预设,账号卡片的服务商下拉里会自动多出预设名,选中即把端点预填进账号。预设只记连接参数,**不含邮箱地址和授权码**;`port`/`secure` 可省略(默认 993/465 与 SSL)。
113
+
114
+ ### 常用邮箱预设
115
+
116
+ | provider | IMAP | SMTP |
117
+ |---|---|---|
118
+ | `qq` | imap.qq.com:993 (SSL) | smtp.qq.com:465 (SSL) |
119
+ | `163` | imap.163.com:993 | smtp.163.com:465 |
120
+ | `126` | imap.126.com:993 | smtp.126.com:465 |
121
+ | `sina` | imap.sina.com:993 | smtp.sina.com:465 |
122
+ | `aliyun` | imap.aliyun.com:993 | smtp.aliyun.com:465 |
123
+ | `gmail` | imap.gmail.com:993 | smtp.gmail.com:465 |
124
+ | `outlook` | outlook.office365.com:993 | smtp.office365.com:587 (STARTTLS) |
125
+ | `icloud` | imap.mail.me.com:993 | smtp.mail.me.com:587 (STARTTLS) |
126
+
127
+ ### 完整配置项
128
+
129
+ | 字段 | 默认 | 说明 |
130
+ |---|---|---|
131
+ | `provider` | 无 | 预设名,自动填 imap/smtp 地址;显式写的 host/port/secure 优先 |
132
+ | `user` | 必填 | 登录邮箱地址 |
133
+ | `password` | 必填* | 授权码/应用专用密码;*也可用环境变量 `DSH_EMAIL_PASSWORD` |
134
+ | `senderName` | 无 | 发件显示名:只改收件人看到的名称,发件地址仍是 `user` |
135
+ | `authUser` | = `user` | 登录账号。别名 / SMTP 中继场景:`user` 是发件地址,这里填真正用于 IMAP/SMTP 认证的账号 |
136
+ | `authPassword` | = `password` | `authUser` 对应的密码;只有登录账号与 `user` 不同、且密码也不一样时才需要 |
137
+ | `imap.host/port/secure` | 按预设 | 收信服务器(另有 connectionTimeoutMs/socketTimeoutMs 可调超时) |
138
+ | `smtp.host/port/secure` | 按预设 | 发信服务器 |
139
+ | `inboxFolder` | `INBOX` | 收发工具默认使用的文件夹 |
140
+ | `sendApproval` | `true` | 发信前弹确认(强烈建议保留) |
141
+ | `maxBodyChars` | `20000` | email_read 正文截断上限(1000–200000) |
142
+ | `accounts` | 无 | 具名账号表;账号级字段覆盖顶层简写 |
143
+ | `accountsYaml` | 无 | 账号映射的 YAML 文本,由设置页的卡片编辑器写入;非空时覆盖 accounts |
144
+ | `clientId` | 内置社区应用(见下) | OAuth2 账号的应用(客户端)ID:留空即用插件内置的公共客户端,填了则覆盖内置值(账号级也可覆盖顶层简写) |
145
+ | `authKind` | 按 provider 派生 | 认证方式覆盖,取值 `oauth2` / `password`。缺省时按 provider 与 IMAP 主机派生;仍能用应用密码连 Exchange Online 的混合或本地租户可钉 `password`。设置页卡片的「认证方式」选择器即写此键 |
146
+ | `serverPresets` | 无 | 自定义服务商预设的 YAML 文本(键=预设名,值含 `label?`/`imap`/`smtp`);只存端点、不含凭证,设置页下拉会列出预设名并把端点预填进账号卡片,改预设不会重连已建立的连接 |
147
+ | `defaultAccount` | 单账号时自动 | 工具省略 account 参数时使用的账号(多账号必填) |
148
+ | `downloadDir` | 会话工作区下 .dsh-email-downloads(回退 $DSH_HOME/email-downloads) | email_attachment 的落盘目录;显式设置后固定 |
149
+ | `maxAttachmentBytes` | 20 MiB | 单个附件与附件总大小上限(1024–512 MiB) |
150
+ | `idleTimeoutMs` | `60000` | IMAP 空闲连接回收时间(连接复用,连续操作更快) |
151
+ | `bodySearchFallback` | `true` | 服务器搜索无结果时,回退到客户端扫描最近邮件的正文 |
152
+ | `bodySearchLimit` | `30` | 正文回退扫描的邮件数量(5-200) |
153
+
154
+ ## 第一步:拿到授权码
155
+
156
+ 各邮箱都要求用「授权码/应用专用密码」而不是登录密码:
157
+
158
+ - **QQ 邮箱**:设置 → 账户 → 开启 IMAP/SMTP 服务 → 生成授权码
159
+ - **163/126**:设置 → POP3/SMTP/IMAP → 开启 → 新增授权码
160
+ - **Gmail**:开启两步验证 → 安全 → 应用专用密码
161
+ - **Outlook**:Microsoft 账户安全 → 应用密码(部分账号需先开两步验证)
162
+
163
+ ## 安全须知
164
+
165
+ - **授权码就是你的邮箱钥匙**。它写在本机(profile 的 `cordis.patch.yml` 或 `settings.yaml`),请勿提交到任何 Git 仓库;更推荐用环境变量 `DSH_EMAIL_PASSWORD`。
166
+ - `email_send` 默认走 DSH 审批通道,没有审批界面时拒绝发信。主动启用且保存的可信规则仅在全部 To/Cc 匹配时跳过插件确认,详见[收件人规则](DRAFTS.md);宿主的其他权限仍生效。
167
+ - **Full Access** 模式的宿主审批策略为 never。需要逐次审批的旧发信路径仍会被拦截;可切换访问模式,或使用新草稿页手动核对并确认。`sendApproval: false` 是关闭所有插件发信确认的全局选项。
168
+ - 本插件不做任何联网上报,凭证只在内存中用于连接你的邮箱服务器。
169
+
170
+ ## Outlook OAuth2(设备码登录)
171
+
172
+ 微软已经对 Exchange Online 关闭了用户名+密码的 basic auth:个人 outlook.com 与绝大多数租户现在只能用 OAuth2。本插件支持设备码(device code)流程,IMAP 与 SMTP 双端共用同一份 token,过期自动刷新。
173
+
174
+ **内置的应用 ID 是哪来的**:设备码登录必须先有一个"应用注册",而让每个用户自己注册一次实在太麻烦——所以插件内置了一份:`15dcd5aa-00dd-487f-82d7-1d2b2c299e14`,由贡献者 [gurio-wine](https://github.com/gurio-wine) 在 [PR #13](https://github.com/STARDUSTLC666/dsh-email/pull/13) 注册,并授权本项目内置使用,在此致谢。代价也要说清楚:微软同意屏上显示的是**他的应用名**(企业安全团队可能因此拒绝授权),登录日志与 telemetry 会归到**他的租户**(含你的 UPN);哪天他删掉这个应用,所有没填自己 ID 的账号会同时登不上,报错还只是一句"clientId 可能填错了"。**想完全自主就注册一个自己的(免费,约 10 分钟)填进卡片覆盖内置值;留空则一直用内置的。**
175
+
176
+ **换成自己的应用(可选,免费,约 10 分钟)**:
177
+
178
+ 1. 打开 [Entra 管理中心](https://entra.microsoft.com/) → **应用注册(App registrations)** → **新注册**。
179
+ 2. **受支持的账户类型**选「任何组织目录中的账户 **以及** 个人 Microsoft 账户」——这一项决定了个人 outlook.com 能不能登录,选错会报 `AADSTS700016` 或 `AADSTS50020`。
180
+ 3. **重定向 URI** 留空(设备码流程不需要)。点注册。
181
+ 4. 在概览页复制 **应用程序(客户端) ID**,这就是要填的 `clientId`。
182
+ 5. 左侧 **身份验证** → 页面最下方 **允许公共客户端流** 设为 **是** 并保存。不开这一项,登录会报 `AADSTS700028` 之类的"未开启设备码流"错误。
183
+ 6. 左侧 **API 权限** → 添加权限 → Microsoft Graph → **委托的权限**,勾上 `IMAP.AccessAsUser.All`、`SMTP.Send`、`offline_access`(最后这个是拿到 refresh token 的关键,少了它每次过期都要重新登录)。个人租户一般无需管理员同意;企业租户可能需要管理员点一次「授予同意」。
184
+
185
+ **填进插件(覆盖内置值)**:设置页 → 邮件 (dsh-email) → 该账号卡片的「应用(客户端)ID」栏(卡片会显示当前生效的是哪个应用;留空即继续用内置的社区应用);或者写在 YAML 里(账号级 `clientId`,也可写在顶层作为所有账号的默认)。
186
+
187
+ **登录**:卡片上点「登录 Microsoft 账号」→ 面板给出一个 `microsoft.com/devicelogin` 链接和一段代码 → 在浏览器打开链接、输入代码、完成授权 → 面板轮询到成功后即显示「已登录:你的地址」。之后收信与发信都用这份 token。
188
+
189
+ **注意事项**:
190
+
191
+ - 企业租户可能还需要管理员在 Exchange 管理中心开启该邮箱的 **IMAP** 与 **SMTP AUTH**。没开 SMTP AUTH 时的典型症状是:收信一切正常,发信被拒。
192
+ - token 与签发它的应用 ID 绑定:换了 `clientId` 会被判为"换了应用",需要重新登录(这是有意的,避免拿旧应用的凭据去撞新应用)。内置应用是所有没填 `clientId` 的账号共用的一份:将来某个版本把它换成项目自己的注册时,这些账号也会需要重新登录一次。
193
+ - 如果你的租户是混合或本地部署、SMTP AUTH 仍然开着,用应用密码也能连:在卡片的「认证方式」里选「密码 / 授权码」即可,不必走 OAuth2。
194
+
195
+ ## 已知限制
196
+
197
+ - **OAuth2 仅覆盖 Outlook / Exchange Online(开箱即用,也可自带应用 ID)**:设备码登录已支持 IMAP 与 SMTP 双端,默认使用插件内置的社区应用(见上文「Outlook OAuth2」),无需自己注册即可登录;企业策略不接受第三方应用时,在卡片里填自己的 `clientId` 覆盖。Google Workspace 等其它强制 OAuth 的环境仍不可用,只能用服务商的应用专用密码 / 授权码。
198
+ - **搜索的匹配数**:服务器命中会先用信封复核(见上文 `email_search`);复核通过时「共 N 条匹配」沿用服务器给出的条数,而列出的每一行都保证真的带关键词。正文回退扫描只看了最近 `bodySearchLimit` 封,不知道全文件夹匹配数,因此渲染为「本页 N 条(仅扫描最近 N 封)」而不是「共 N 条」。
199
+ - **正文搜索**:服务器端只搜 subject / from / to / cc;多数服务器(如 QQ)的 IMAP `TEXT` / `HEADER` 搜索不可靠,无结果时回退到最近 `bodySearchLimit` 封的正文扫描(较慢,可用 `bodySearchFallback` 关闭)。
200
+ - **附件**:内嵌图片暂不支持单独下载;附件定位失败会直接报错而不是下载错误文件(安全默认)。
201
+ - **密码落盘**:设置页保存的授权码以明文写在本机 `settings.yaml`(secret 标记只保证它不进日志 / 导出 / 诊断,不做磁盘加密)。请勿把 `settings.yaml` 交给不信任的人。
202
+ - **OAuth2 token 落盘**:access / refresh token 以明文 JSON 存在 `$DSH_HOME/data/dsh-email/oauth2-tokens.json`(刻意不放进 `settings.yaml`,因此不会随设置导出)。写入时带了 `mode: 0o600`,但这个权限位只在**文件创建那一刻**生效,且**在 Windows 上等于无效**——请勿把该文件交给不信任的人。token 与签发它的应用 ID 绑定,换了 `clientId` 需要重新登录;删除账号会清理它的 token。
203
+ - **本地改动会被 `pnpm install` 还原**:如果你是直接改 `node_modules/dsh-email/` 里的文件做本地部署,任何一次 `pnpm install` 都会把它还原成 registry 上的版本(例如 0.10.7);要长期保留请改成从本地路径或 Git 提交安装。
204
+
205
+ ## 开发
206
+
207
+ ```sh
208
+ pnpm install
209
+ pnpm run build # tsc → lib/
210
+ pnpm test # 构建 + 离线测试,无需真实邮箱
211
+ ```
212
+
213
+ `src/index.ts` 只负责组合插件。`runtime.ts` 管理动态设置、账号连接池和网页/工具各自的监视游标;`tools.ts` 接线十个工具的执行逻辑;`tool-contract.ts` 集中维护参数、输出 schema 和中文渲染;`approval.ts` 管理发信审批。IMAP/SMTP 传输仍由 `mail-client.ts` 负责,网页路由由 `web.ts` 负责——设置页的账号卡片编辑器要的解析、序列化(保留注释、保住已存授权码)和自定义预设快照也在这里。
214
+
215
+ 测试覆盖动态配置换池、卸载释放、取消信号与工作区透传、工具/网页游标隔离、账号卡片序列化与预设解析,以及审批拒绝时不会进入发送执行。测试用内存客户端替代邮箱连接。
216
+
217
+ ## 协议
218
+
219
+ MIT。这是一个社区插件,与 DeepSeek 官方无关;`@deepseek-ai/*` 为官方保留命名空间。
220
+
221
+ ## 相关插件
222
+
223
+ - [dsh-slack](https://github.com/STARDUSTLC666/dsh-slack) — Slack 通知/收件箱
224
+ - [dsh-dingtalk](https://github.com/STARDUSTLC666/dsh-dingtalk) — 钉钉群通知(零依赖)
225
+ - [dsh-email](https://github.com/STARDUSTLC666/dsh-email) — 邮件八件套 + Web 设置页 + 新邮件弹窗
@@ -0,0 +1,25 @@
1
+ # dsh-email 验证记录
2
+
3
+ 本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
4
+
5
+ ## 0.15.0 · 2026-10-03
6
+
7
+ 328 项 Windows 自动测试通过;官方 RC2 Web 工作台验收覆盖编辑/上传/下载/明确发送、草稿冲突、按账号规则、SMTP 回执不确定与部分拒绝、390px 布局及英文文案。生产依赖审计无发现。详细范围见 [0.15.0 验收](validation/0.15.0.md),真实邮箱与新工作台原生桌面操作尚未验证。
8
+
9
+ ## 原中文记录
10
+
11
+ 当前验证基线为官方发布标签源码构建的 Harness **0.2.0-rc.2**(2026-09-30)。18 个插件共同加载,99 个工具与 35 个技能的注册及输出检查通过;邮件新增多服务商隔离、卡片保存、端点错误提示、发送别名与编辑提示回归。桌面操作范围与尚未完成的真实服务验证见上文。
12
+
13
+ **0.11.0 的同载验证(2026-09-18,本地构建的 Harness `0.1.5-rc.2`,`web` profile)**:插件挂载无报错;设置路由 GET 返回 200 且响应中已无 `raw` 字段;用 `text/plain` 发 POST 被 **415** 拒绝(同源守卫在真实宿主下生效);`application/json` 的 POST 下卡片投影正确,账号钉住 `authKind: password` 后 `authKindDeclared` 与 `authKind` 均为 `password`;设置面板实际渲染出账号卡片、8 个服务商预设的中文下拉、「认证方式」三态选择器、「应用(客户端)ID」输入格与提示、未填 ID 时的警示条(说明 `--dsw-alias-state-warn-primary` 在真实宿主下确有定义)与「登录 Microsoft 账号」按钮;浏览器控制台无报错;save 全链路可用,验证结束后已把 `accountsYaml` 还原为空、原有账号恢复。离线测试 231 项全绿。**仍未做**:真实 Outlook 租户的 OAuth2 端到端(设备码流程要真人在浏览器完成授权)与真实发信未测,`clientId` 相关路径目前只有假 authority 的用例覆盖。采用 `cordis.patch.yml` + `dsh.bundle.patch` 组合包模型。Node 要求为 22.19 及以上的 22.x,或 24 及以上。外部服务的实际业务操作需按各组件配置单独验证。
14
+
15
+ 遵循官方[插件打包与安装要求](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md):ESM 入口、预构建 `lib/`、`dsh.bundle.patch` 和 `cordis.patch.yml` 配置层;显式注入所需服务,提供 JSON Schema 参数、规范化输出和渲染函数,运行时不导入宿主内部服务;配置声明使用官方 `@deepseek-ai/schemastery` 公共包。使用 Node 22.19 及以上的 22.x 或 Node 24 及以上版本;Harness 仍在快速迭代,上述版本是实测基线。
16
+
17
+ ## Original English record
18
+
19
+ The current baseline is Harness **0.2.0-rc.2** built from its official release tag on 2026-09-30. All 18 plugins mount together with 99 tools and 35 skills; registration and output checks pass. New email regressions cover provider isolation, card saves, endpoint diagnostics, sender aliases and editor hints. The native UI scope and remaining live-service checks are described above.
20
+
21
+ **0.11.0 co-load verification (2026-09-18, locally built Harness `0.1.5-rc.2`, `web` profile)**: the plugin mounted without errors; the settings route answered GET with 200 and no `raw` field in the response; a `text/plain` POST was refused with **415**, proving the same-origin guard holds in the real host; under a `application/json` POST the card projection was correct, and an account pinning `authKind: password` reported both `authKindDeclared` and `authKind` as `password`; the panel actually rendered account cards, the eight provider presets with localized labels, the three-way authentication selector, the application (client) ID field with its hint, the missing-ID warning banner (so `--dsw-alias-state-warn-primary` is genuinely defined in the real host) and the Sign in with Microsoft button; the browser console was clean; save worked end to end, and afterwards `accountsYaml` was restored to empty with the original account intact. Offline tests: 231 green. **Still not done**: an end-to-end OAuth2 run against a real Outlook tenant (the device-code flow needs a human to authorize in a browser) and real sending; the `clientId` paths are covered only against a fake authority. Uses the `cordis.patch.yml` + `dsh.bundle.patch` bundle model. Node requirements are 22.19 or later within 22.x, or 24 or later. Live external-service workflows require separate configuration and validation.
22
+
23
+ On 2026-09-10, npm `dsh-email@0.10.6` passed real QQ mailbox folder/list/read/search calls, the settings page's connection test and Save & Apply, and separate SMTP authentication. An empty authorization-code field correctly used `DSH_EMAIL_PASSWORD`. This recheck did not connect to a real mailbox or send, modify or delete mail.
24
+
25
+ Follows the official [plugin packaging and installation requirements](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md): an ESM entry point, prebuilt `lib/`, `dsh.bundle.patch` and a `cordis.patch.yml` layer. The plugin explicitly injects its required services and supplies JSON Schema parameters, canonical output and rendering, without importing host-internal services; configuration uses the public `@deepseek-ai/schemastery` package. Use Node 22.19 or later within 22.x, or Node 24 or later. Harness is evolving rapidly; the version above is the tested baseline.
@@ -0,0 +1,29 @@
1
+ # 0.15.0 验收范围
2
+
3
+ 日期:2026-10-03。基线:0.14.7;官方 Harness 0.2.0-rc.2 源码构建 639ed01;Windows、Node 24.16.0。
4
+
5
+ ## 自动回归
6
+
7
+ 328 项 Windows 测试通过,无失败或跳过;类型构建与客户端语法检查通过,官方 npm 生产依赖审计无发现。
8
+
9
+ 覆盖原有邮件行为及新增草稿、规则测试:显示名/地址组/Cc、IDN、地址大小写、域名边界、账号隔离、规则编辑不重连、账号保存不覆盖规则、原子存储、跨进程锁、编辑冲突、附件快照、预览失效、并发提交、取消、SMTP 结果不明、启动恢复、上传/复制/删除和来源校验。
10
+
11
+ ## 官方 Web 可见操作
12
+
13
+ 使用独立 DSH_HOME 和只监听本机的合成 SMTP 服务。未使用真实模型或邮箱凭据,未向外部邮箱发送邮件。
14
+
15
+ - 新建、编辑 To/Cc/主题/正文、保存、重开和切换发件账号。
16
+ - 原生文件选择器上传,预览下载字节一致;取消预览不触发 SMTP。
17
+ - 关闭设置后恢复未保存编辑;冲突保留输入,显式选择已保存版本。
18
+ - 规则按账号保存,未保存切换提示、陌生 Cc 阻止跳过、全部匹配预览及账号隔离;修改规则后重新检查发送预览。
19
+ - 明确确认后提交一封完整邮件:发件显示名、全部 To/Cc、二次编辑正文和附件与实际 MIME 一致。
20
+ - 模拟 SMTP 收到 DATA 后断开:结果未确定,刷新不重发。另一次拒绝 Cc:回执明确列出接受与拒绝地址,不自动重发。
21
+ - 删除原草稿后,独立副本附件仍可下载;只操作合成草稿的上传副本。
22
+ - 390px 视口:工作区宽度与滚动宽度均为 330px,预览均为 328px;导航、表单、下载和确认可操作。检查英文工作台文案;浏览器错误日志为空。
23
+ - 无登录 cookie 访问新增真实 carrier 路由返回 401。
24
+
25
+ 合成服务共记录 3 次 SMTP DATA:完整接受 1 次、回执丢失 1 次、Cc 拒绝 1 次。其余新建、编辑、规则检查、取消、冲突恢复和下载没有增加投递次数。
26
+
27
+ ## 范围边界
28
+
29
+ 本轮没有验证真实邮箱 SMTP 投递、OAuth2 发信、最终收件箱送达,也没有在原生桌面窗口操作新草稿工作台。旧配置路径有回归;新工作台可见操作来自上述官方 RC2 Web 环境。
package/lib/approval.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { matchRecipients, parseRecipientPolicies } from './recipient-policy.js';
1
2
  export function installSendApproval(ctx, runtime) {
2
3
  ctx.on('tools/pre-execute', async (exec, next) => {
3
4
  if (exec?.name !== 'email_send' && exec?.name !== 'email_reply')
@@ -11,11 +12,23 @@ export function installSendApproval(ctx, runtime) {
11
12
  catch {
12
13
  return next(); // unconfigured: let the tool report the actionable hint
13
14
  }
15
+ if (exec.name === 'email_send') {
16
+ const args = (exec.arguments ?? {});
17
+ try {
18
+ const settings = runtime.getEffectiveSettings(), account = typeof args.account === 'string' && args.account.trim() ? args.account.trim() : settings.defaultAccount;
19
+ if (settings.accounts.has(account) && typeof args.to === 'string' && (args.cc === undefined || typeof args.cc === 'string')) {
20
+ const policy = parseRecipientPolicies(value.trustedRecipientsYaml).get(account);
21
+ if (policy?.skipApproval && (await matchRecipients(args.to, args.cc ?? '', policy)).skipsApproval)
22
+ return next();
23
+ }
24
+ }
25
+ catch { /* Invalid rules or addresses keep the usual approval; never grant trust on a parse failure. */ }
26
+ }
14
27
  let reason;
15
28
  if (exec.name === 'email_send') {
16
29
  const args = (exec.arguments ?? {});
17
30
  const attachCount = Array.isArray(args.attachments) ? args.attachments.length : 0;
18
- reason = '发送邮件给 ' + args.to + ',主题「' + args.subject + '」' + (attachCount > 0 ? ',附件 ' + attachCount + ' 个' : '');
31
+ reason = '发送邮件给 ' + args.to + (args.cc ? ',抄送 ' + args.cc : '') + ',主题「' + args.subject + '」' + (attachCount > 0 ? ',附件 ' + attachCount + ' 个' : '');
19
32
  }
20
33
  else {
21
34
  const args = (exec.arguments ?? {});