Smail/docs/代码逐行审查报告-20260906.md
smail a34ca0e959
Some checks are pending
Security Checks / security (push) Waiting to run
更新审查报告中外部邮箱重发与重试状态为等待浏览器验收
2026-09-11 07:37:56 +08:00

743 lines
76 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Smail 代码逐行审查阶段报告
## 审查范围与状态
- 请求:对全部代码逐行审查,不能用搜索命中、编译成功或测试通过代替逐行检查。
- 开始时间:2026-09-06 01:46:05,时区 Asia/Shanghai。
- 原始审查基线:`9849f8f11c9f49efd1bc877d7fedd8da265e32df`,分支 `main`。
- 审查前已建立中文提交检查点,远端 `smail/main` 已核验一致。
- 修复阶段开始时间:2026-09-06 11:03:06;修改前检查点 `addfcea1cdb59f1fd28bd1639c54502cb05990de` 已提交并推送。
- 当前结论:**已实施业务修复并通过本节所列自动化回归,仍有部分修复及待真实验收项;尚未完成全仓逐行覆盖。**
- 本文保留 R01–R51 原始发现、原位置与失败证据,它们是历史审查记录,不代表修复后的当前行为。当前状态以“修复状态”和“修复后最终回归与构建”两节为准。
- 修复范围包含原始基线之后已有提交中的修复及本次补充;测试使用隔离数据库与配置目录,未清空真实运行数据库,未将新构建部署到用户正在使用的实例。
## 需求确认与任务清单
按用户明确要求,审查范围包括业务源码、测试、构建部署脚本、配置模板及前端页面。锁文件、第三方资源、二进制资源单独分类,不伪装成已逐行审查的业务代码。
- [✓] 核验工作区及审查前 Git 检查点。
- [✓] 运行原有前后端测试基线,保留原始通过及新增失败证据。
- [~] 建立逐文件、行范围和哈希覆盖账本。
- [~] 检查认证恢复、迁移、邮件列表、IMAP、配置与前端交互。
- [✓] 用隔离 SQLite、真实本地协议和真实前端函数验证已登记问题并执行修复回归。
- [x] 修复认证授权、配置事务、邮件协议、隐私、前端交互及部署回滚问题,等待适用环境复审。
- [x] 补齐外部邮箱输入错误重试、倒计时、过期提示及重新发码恢复流程。
- [✓] 完成前端 55/55、独立前端审查 11/11、后端全量与 race、独立审查普通与 race 回归。
- [✓] 完成 Windows 八阶段内存回滚验证、Telegram 隔离测试及前后端构建。
- [ ] 补齐全仓其余文件的逐行审查。
- [ ] 浏览器 MCP 验收完整业务流程。
- [ ] MySQL、PostgreSQL 实库及 Linux/Windows 部署验收。
- [ ] 部署新产物并完成真实证书续期、邮件投递及在途请求排空验收。
- [~] 全部发现项修复后的独立复审;自动化场景已通过,但不等于完整功能验收。
## 修复状态
状态约定:本表 `[x]` 表示代码修改及列出的自动化场景已完成,仍需对应的浏览器、外部服务或部署复审;`[~]` 表示仍有明确未完成的代码流程。上方测试任务的 `[✓]` 仅指该项测试本身复核通过,不扩大为产品验收通过。
| 原始编号 | 当前状态与修复内容 | 自动化证据与仍需验收的边界 |
| --- | --- | --- |
| R01、R03、R28 | [x] 不支持的源索引明确拒绝;迁移排空响应取消;保留历史自增高水位。 | `dbmigration_findings_test.go`;SQLite 迁移回归通过,MySQL/PostgreSQL 实库待验。 |
| R02 | [x] 登录二次验证重发进入登录前安全路由,仍保留挑战、限频及 CSRF 边界。 | `TestAuditAnonymousLoginResendMustReachController`;真实验证码投递与浏览器待验。 |
| R04 | [x] 邮件计数与分页查询分离,不再把偏移带入计数。 | `TestAuditPaginationMustPreserveTotal`;普通列表页面待浏览器验收。 |
| R05 | [x] 恢复完成时复查绑定的 WebAuthn 凭据及账户安全状态,撤销后的旧票据不能改密。 | `recovery_findings_test.go`;真实安全密钥设备流程待验。 |
| R06 | [x] 直接使用响应拦截器返回的 Blob;错误 Blob 不生成下载,允许重试。 | `frontend-findings.test.mjs` 的 CSV/JSON 内容及错误响应断言;浏览器下载待验。 |
| R07、R35 | [x] 写信页接收 `fileRef`,邮件列表接收 `taskTableDataRef`,连接真实模板与组合函数。 | 真实 SFC 编译挂载的附件和列表测试;系统文件选择框、实际表格待浏览器验收。 |
| R08、R11 | [x] 区分内容身份和邮箱关系身份,为副本创建独立状态对象,保持 COPY 后移动、FETCH 的 UID 一致。 | `imap_findings_test.go` 的副本、移动与 FETCH 回归;未宣告原生 MOVE,客户端仍使用回退流程。 |
| R09 | [x] 安全服务使用加锁快照发布与读取;先停止入口,再撤下服务引用及会话清理器。 | `runtime_findings_test.go`、`lifecycle_findings_test.go` 普通与 race 通过;完整 HTTP 在途排空待验。 |
| R10、R12–R15 | [x] 保存只读选择状态,统一动态 UID 与序号解析,修复默认文件夹枚举、UIDNEXT 和有效邮件计数。 | `imap_findings_test.go`、`imap_wire_findings_test.go`;同时覆盖 Poll/IDLE、DONE 与清理。 |
| R16 | [x] 传递目标角色,并用安全版本条件更新及失效流程保护角色变更。 | `TestAuditAuthorizedRoleChangeMustSucceed`;角色变化后的真实 HTTP 会话失效待验。 |
| R17 | [x] 恢复 Cookie 的签发和清除采用覆盖新旧接口的 `/api/` 路径,保留上下文校验。 | `TestAuditRecoveryCookieMustReachVersionedAPI` 的 TLS/CookieJar 新旧接口对照;自签名 HTTPS 浏览器 Cookie/CSRF 待验。 |
| R18–R23 | [x] POP3 建立会话快照及连续编号,统一可见集合与删除视图,支持 RSET、正确单行响应和非负 TOP 参数。 | `pop3_action_findings_test.go` 及解析器测试;LIST/STAT 字节大小、dot-stuffing 和 RETR 额外空行另行专项验收。 |
| R24–R27 | [x] 收件人响应不暴露密送地址;保留标准 Reply-To、带逗号姓名和附件 Content-ID;IMAP 重建字节稳定。 | `message_findings_test.go` 与真实解析、存储、响应回归;网页 `cid:` 显示及外部客户端待验。 |
| R29 | [x] 配置重试前处理未决事务,成功重试不再被旧失败事务在重启时回滚。 | `TestAuditSuccessfulConfigRetryMustSurviveRestart`:重启后代次仍为 2,策略仍为 8 MiB。 |
| R30 | [x] 候选有唯一 ID,提供受授权的脱敏查询、恢复和取消;页面刷新恢复候选,垃圾邮件设置同步适配。 | `TestAuditPendingIdentityAndRecovery`、配置前端及审查测试;跨会话恢复保留近期身份验证,已落盘候选不能取消。 |
| R31 | [x] 同页面输错验证码保留原挑战重试,弹窗支持倒计时、过期提示与重新发码。 | 前端弹窗与代管弹窗已实现保留挑战、倒计时过期提示及重新发送链路;刷新和重发自动化测试已覆盖;浏览器真实验收待执行。 |
| R32、R33 | [x] 外部邮箱绑定执行近期认证,规范化后拒绝当前用户自己的主邮箱。 | `TestAuditExternalBindingMustRequireRecentAuthentication`、`TestAuditExternalBindingMustRejectOwnPrimaryMailbox`;真实邮件投递待验。 |
| R34 | [x] 修正正常附件下载路由解析,保留邮件归属及索引校验。 | `TestAuditAttachmentPageDownloadMustReturnStoredContent` 的真实 TLS 路由;浏览器点击下载待验。 |
| R36 | [x] Windows 升级按目标记录备份和安装状态,回滚只处理本次实际替换的文件。 | 最新八个失败阶段均保留旧主程序与旧管理程序;这是内存隔离控制流验证,不是正式服务部署。 |
| R37 | [x] 编辑规则使用独立草稿,新建和取消重置状态,新建提交 ID 为 0。 | `rule-frontend.test.mjs`;列表未被未保存条件污染,实际请求走新增接口。 |
| R38、R39、R41 | [x] 完整证书链去重,按 X.509 校验泛域名覆盖,迁移保留 DNS 挑战方式。 | `ssl_findings_test.go` 及原有 SSL 测试;不请求真实 CA。自动 DNS 只申请 `*.主域名`,不加入根域名 SAN。 |
| R40 | [x] 证书先写入独立托管版本,再通过分区配置事务提交引用;不提前替换现用目录。 | `TestAuditPartitionedRenewalMustCommitBeforeSwitch`、`TestAuditStagedSSLDoesNotReplaceCurrent`;发布失败保留已提交候选,超级管理员可受授权恢复,但自动恢复与真实 CA 续期尚未验收。 |
| R42 | [x] 旧配置迁移保留 SQLite 原路径,避免静默连接到新空库。 | `TestAuditLegacyMigrationMustPreserveSQLiteLocation`;原目录标记数据仍可读取。 |
| R43 | [x] 移除特殊域名的 DKIM 放行,只依据实际签名校验结果。 | `TestAuditInvalidDKIMMustNotPassForSpecialDomain`;普通域名与原特殊域名均拒绝无效签名。 |
| R44、R45 | [x] SEARCH 独立搜索键求交,OR 节点内部求并,删除搜索使用与 STORE/FETCH 一致的标记。 | `TestAuditSearchKeysMustIntersect`、`TestAuditSearchDeletedMustMatchStoredFlag` 的本地 TLS 协议测试。 |
| R46–R48 | [x] Telegram 附件发送遵守隐私开关,普通文本 HTML 转义,Unicode 截断保持有效字符和闭合标记。 | `telegram_findings_test.go`:关闭上传 0 个附件、开启对照 2 个;真实 Telegram API 待验。 |
| R49 | [x] 协议开关采用受控值,请求成功才提交新状态,失败恢复服务端一致状态。 | `protocol-frontend.test.mjs` 成功与失败对照通过;浏览器及真实协议连接关闭待验。 |
| R50 | [x] Telegram 设置改为主程序原生 `.vue/.js/.css` 页面,不依赖受限 iframe 存储或网络访问。 | `test/frontend/telegram-settings.test.mjs`;其他插件继续保留 `sandbox="allow-scripts"`,完整新页面部署待验。 |
| R51 | [x] 插件 RPC 用明确字符串映射传递失败信息,在适配层与 Go `error` 转换。 | `TestAuditSendAfterFailureMustReachPlugin` 的成功与失败回调对照;外部插件进程待验。 |
### 修复期间追加的边界问题
1. **大邮箱 UID 查询上限**:`session_messages.go` 每批最多读取 500 个 UID,保持会话序号;EXPUNGE 分批读取后汇总并单事务删除。`TestAuditLargeSelectedMailboxMustBatchUIDQueries` 使用 10,001 封邮件,修复前 `count=0 err=invalid UID list`,最终普通测试 4.73 秒、race 179.29 秒通过。失败证据:`test-log/run-2026-09-06T04-25-22-809Z-39264/backend.log`。
2. **SSL 提交覆盖签发期间的其他配置**:旧配置读写共用 `legacyConfigMu`,SSL 提交重读最新配置,只合并 SSL/HTTPS 字段;域名或证书策略改变时拒绝旧结果。`TestAuditLegacySSLCommitPreservesLatestUnrelatedSettings` 修复前把端口覆盖回 8080,最终断言保留 9080。失败证据:`test-log/run-2026-09-06T04-32-01-312Z-3768/backend.log`。本项不是通用旧版整份配置写入的完整 CAS,不宣称所有并发更新都已解决。
3. **停止验证运行时后残留旧服务引用**:`ClearSecurityRuntime()` 在锁内清空五项服务,再清登录挑战;有无清理运行时均撤下引用,重复停止安全。`TestAuditStopVerificationWithdrawsPublishedServices` 先失败,最终两组均通过,并验证真实控制器返回 `NeedLogin`。失败证据:`test-log/run-2026-09-06T04-33-27-630Z-42076/backend.log`。HTTP 仍使用 `Close()`,这不等于完整在途请求排空已验收。
此外,微信推送增加业务响应码及响应解析检查,不能把 HTTP 成功误判为业务成功;`TestAuditWeChatBusinessResponses` 的隔离回归通过,未向真实微信端点投递。
## 原始已确认问题(历史发现)
优先级:P1 为安全授权、数据完整性或核心功能问题;P2 为特定场景故障及一致性问题。编号固定,便于后续复现和修复。
### R01 / P1:迁移成功切库,但自定义唯一索引静默丢失
- 位置:`backend/services/dbmigration/portable_schema.go:9-28`;`portable_executor.go:177-223`;`portable_data.go:72-95,111-117`。
- 前提:源库包含版本化表之外的自定义唯一索引。
- 原因:源对象校验未识别这些索引,目标库按内置结构重建,之后只验证数据,不验证自定义约束等价性。
- 实测:完整版本化 SQLite 源库增加唯一索引后执行迁移,结果为 `switched=true custom_index_count=0 duplicate_insert_error=<nil>`。
- 影响:迁移被报告成功,但源库禁止的重复值可以写入目标库。
- 建议:无法忠实迁移的结构在预览和执行时明确拒绝,或完整迁移并比较索引、约束。
- 边界:只实测 SQLite,不将结果泛化为三种数据库实库均已验证。
- 复现:`TestAuditCustomUniqueIndexMustNotDisappear`。
### R02 / P1:登录二次验证重发接口错误要求先登录
- 位置:`backend/listen/http_server/https_server.go:168-170,194-200`;`api_v1_compat.go:19`;`http_server.go:52`;`frontend/src/views/LoginVerification/LoginVerification.js:93-103`。
- 前提:密码已验证,但尚未完成登录二次验证,用户需要重发验证码。
- 原因:登录前安全接口白名单遗漏 `/api/v1/security/login/resend`,请求未到验证码控制器就被登录拦截。
- 实测:真实路由、内存会话返回 HTTP 200,响应体业务码 403:`Login has expired.`。
- 影响:未登录用户无法在二次验证页重发验证码。
- 建议:将该接口归入登录前安全路由,保留挑战票据、限频及 CSRF 等校验。
- 边界:未发送真实验证码;不能据此断言前端必然跳回登录页。
- 复现:`TestAuditAnonymousLoginResendMustReachController`。
### R03 / P2:迁移排空请求不响应取消
- 位置:`backend/services/dbmigration/maintenance.go:62-70`;`service.go:224-232`。
- 原因:持有或升级请求租约时使用不可取消的 `RWMutex.Lock()`,传入的上下文未参与等待。
- 实测:30ms 上下文超时,200ms 后仍未退出,必须释放在途读锁才恢复。
- 影响:在途请求长期不结束时,迁移取消/超时不能及时结束排空,还可能让新 HTTP 请求排队等待。
- 建议:采用可取消的排空协调方式,确保请求租约、迁移状态和错误路径一致释放。
- 边界:证明的是超时不生效,不是已经复现永久死锁。
- 复现:`TestAuditRequestDrainMustRespectCancellation`。
### R04 / P2:邮件列表第二页起返回总数 0
- 位置:`backend/services/list/list.go:28-30,98-107`;`backend/controllers/email/list.go:120-129`。
- 原因:聚合计数查询错误附带分页偏移和排序。
- 实测:31 封邮件,每页 15 封;偏移 0/15/30 分别返回行数 15/15/1,总数却为 31/0/0。
- 影响:接口分页元数据错误,依赖每次响应总数的调用方会出现错误页数。
- 建议:计数与列表查询分离,仅共享筛选条件。
- 边界:当前网页翻页函数未逐次刷新总页数,不能声称网页翻页后分页控件必然消失。
- 复现:`TestAuditPaginationMustPreserveTotal`。
### R05 / P1:撤销安全密钥后,旧恢复票据仍允许改密
- 位置:`backend/services/recovery/service.go:315-328,390-417`;`backend/services/webauthn/service.go:323-333`。
- 前提:安全密钥验证后已签发恢复票据,随后该凭据被撤销,票据仍未过期。
- 原因:恢复完成阶段只对邮箱来源重新检查凭据;WebAuthn 来源没有同等撤销检查。
- 实测:真实撤销凭据后,旧票据完成改密返回成功,并用密码校验函数确认新密码生效。
- 影响:用户撤销凭据后,先前依赖该凭据产生的改密授权仍有效到票据过期。
- 建议:完成改密时重新校验票据所绑定的凭据归属、撤销状态及版本,或撤销时使关联票据失效。
- 边界:隔离测试从签发恢复票据阶段开始,不验证真实设备的 WebAuthn 签名链;不是未认证直接获取票据的证明。
- 复现:`TestAuditRevokedWebAuthnMustInvalidateRecoveryTicket`。
### R06 / P2:配置审计导出下载内容为 undefined
- 位置:`frontend/src/utils/axios.js:71-78`;`frontend/src/views/components/ConfigSettings/ConfigSettings.js:549-561`。
- 原因:公共响应拦截器已返回 `response.data`,导出函数却再次读取结果的 `.data`。
- 实测:执行真实响应拦截器和真实导出函数,CSV、JSON 两种格式下载内容均为字符串 `undefined`。
- 影响:导出请求可成功,但生成的文件没有审计记录。
- 建议:统一请求工具的返回类型,直接使用返回的 Blob,并覆盖两种导出格式。
- 边界:网络和下载 DOM 被隔离,未做真实浏览器文件下载验收。
- 复现:`frontend-findings.test.mjs` 中的审计导出测试。
### R07 / P2:写信页附件按钮未绑定文件输入框
- 位置:`frontend/src/views/Editer/Editer.vue:12,116-134`;`Editer.js:197-199`。
- 原因:`usePage()` 已返回 `fileRef`,但 Vue 文件的解构遗漏该变量,模板引用没有连接到组合函数的引用。
- 实测:编译真实 SFC、挂载真实组合函数后,`template_file_ref_connected=false`,调用附件按钮方法触发 `dispatchEvent` 空引用错误。
- 影响:通过附件按钮选择文件失败。
- 建议:在页面脚本中正确接收并绑定 `fileRef`,增加挂载后点击测试。
- 边界:使用 Vue 自定义渲染器,不是操作系统文件选择框的浏览器验收。
- 复现:`frontend-findings.test.mjs` 中的附件按钮测试。
### R08 / P2:IMAP COPY 后,网页移动误判邮件不属于当前用户
- 位置:`backend/services/group/group.go:393-403,741-751`;`backend/controllers/email/move.go`。
- 前提:IMAP COPY 为同一封邮件建立多个当前用户的文件夹关系。
- 原因:移动服务将关系行数和唯一邮件 ID 数量直接比较,没有正确处理合法副本。
- 实测:真实本地 TLS IMAP COPY 后,关系行数为 2;网页调用的服务返回 `one or more messages are not owned by the user`。
- 影响:用户自己的邮件因存在 IMAP 副本而不能通过网页正常移动。
- 建议:定义清晰的邮箱关系身份,区分邮件内容 ID 与文件夹中的关系 ID,所有权校验按实际目标关系执行。
- 边界:当前服务器没有声明 MOVE 扩展,客户端 `Move()` 回退 COPY/STORE/EXPUNGE 且成功;不能报告成 IMAP 客户端 MOVE 必然失败。
- 复现:`TestAuditCopyThenMoveMustKeepMailboxIdentity`。
### R09 / P2:安全运行时发布和读取存在数据竞争
- 位置:`backend/controllers/security.go:89`;`backend/controllers/webauthn.go:63`;`backend/res_init/init.go:252-259,394-402`。
- 原因:全局 WebAuthn 服务指针无同步读写,关闭流程在 HTTP 停止前清空服务,可能与在途控制器读取并发。
- 实测:`-race` 报告 `ConfigureWebAuthnRuntime` 与 `webAuthnRuntime` 的数据竞争。
- 影响:生命周期切换期间并发访问不安全,运行时可见性无保证。
- 建议:用互斥锁或原子发布一致的安全运行时快照,调整停止顺序并补并发生命周期测试。
- 边界:尚未复现真实重启崩溃;普通测试模式显示通过不能消除 race 证据。
- 复现:`TestAuditWebAuthnRuntimePublication`,必须使用 `-race`。
### R10 / P2:IMAP EXAMINE 只读选择仍持久化修改邮件标记
- 位置:`backend/listen/imap_server/session_select.go:10-44`;`session_store.go:18-20,81-91`;`server.go:50-58`。
- 原因:会话未保存 `SelectOptions.ReadOnly`,STORE 没有只读状态检查。
- 实测:真实 TLS IMAP 使用 EXAMINE 打开 INBOX 后执行 STORE,返回成功,临时数据库的 `is_read` 从 0 变为 1。
- 影响:客户端要求的只读邮箱状态没有生效。
- 建议:保存只读选中状态,对 STORE、EXPUNGE 及非 PEEK FETCH 等写入路径统一检查。
- 复现:`TestAuditExamineMustNotPersistStore`。
### R11 / P1:IMAP 同文件夹副本复用可变对象,覆盖 UID 并遗漏 FETCH 响应
- 位置:`backend/services/list/list.go:307-322`;`backend/listen/imap_server/session_fetch.go:147-157,253-300`。
- 前提:同一邮件经 IMAP COPY 在同一文件夹生成两个不同 UID 的关系,并在一次范围查询中读取。
- 原因:内容缓存按 `email_id` 保存同一个响应指针,在遍历关系时直接覆盖该指针的 UID、序号和标记;两个结果最终指向同一对象。
- 实测:数据库为 UID 17、18,均在 INBOX;真实列表函数返回 `hydrated_uids=18,18 same_pointer=true`;TLS IMAP FETCH 两个 UID 只收集到 1 封响应且没有错误。
- 影响:邮箱副本身份和状态被混淆,客户端同步可能漏信;使用相同构造函数的批量操作也需逐项复查。
- 建议:为每个邮箱关系构造独立响应对象,允许共享不可变内容但不能共享 UID、序号和状态;增加 COPY 后范围 FETCH/STORE 的回归测试。
- 边界:协议层实测为漏响应,不将其表述成客户端已收到两条重复 UID 响应。
- 复现:`TestAuditCopiedMessagesMustKeepDistinctUIDs`。
### R12 / P2:IMAP 顶层文件夹枚举遗漏默认文件夹
- 位置:`backend/listen/imap_server/session_list.go:14-16,62-66`。
- 原因:仅 `LIST "" "*"` 分支生成默认文件夹;标准顶层通配符 `%` 分支只查询自定义文件夹表,且通配规则直接复用 SQL LIKE。
- 实测:真实协议 `LIST "" "%"` 返回空列表,尽管同一账户有 INBOX 和真实邮件。
- 影响:按层级枚举邮箱的客户端无法发现默认收件箱等文件夹。
- 建议:先组合默认和自定义文件夹,再统一按 IMAP 的 reference、`%` 和 `*` 规则匹配,不能直接等同 SQL 通配符。
- 复现:`TestAuditListPercentMustIncludeInbox`。
### R13 / P2:移走最大 UID 邮件后,UIDNEXT 倒退
- 位置:`backend/services/group/group.go:573-575,631-651`。
- 原因:用当前仍在文件夹内的最大关系 ID 加一计算 UIDNEXT,没有保存已分配 UID 的高水位。
- 实测:将最新邮件从 INBOX 移到 Junk,STATUS 返回 UIDNEXT 从 20 降为 19,但 UIDVALIDITY 始终为 2000000000。
- 影响:违反同一 UIDVALIDITY 下 UIDNEXT 不应倒退的同步约定,客户端缓存与增量检测可能不一致。
- 建议:使用单调分配的邮箱 UID 状态,不能根据剩余邮件反推下一个 UID。
- 边界:实测是 UIDNEXT 倒退,不是已经证明数据库复用了旧 UID。
- 复现:`TestAuditUIDNextMustNotDecrease`。
### R14 / P2:UID FETCH * 无法读取最后一封邮件
- 位置:`backend/listen/imap_server/session_fetch.go:147-151`;`backend/services/list/list.go:250-252`。
- 原因:协议动态 UID `*` 用 0 表示,未解析为当前最大 UID 就传入查询函数,被判定为空条件返回空列表。
- 实测:非空 INBOX 中最大 UID 为 20,真实协议 `UID FETCH *` 返回 0 封且没有错误。
- 影响:依赖标准动态 UID 查询的客户端拿不到最后一封邮件。
- 建议:在协议入口统一解析动态 UID、反向范围及空邮箱边界,再交给数据库查询;同步检查 COPY/STORE。
- 复现:`TestAuditFetchWildcardUIDMustReturnLastMessage`。
### R15 / P2:登录邮箱绑定被计入收件箱邮件数量
- 位置:`backend/services/group/group.go:654-662`;`backend/models/user_email.go`。
- 原因:`user_email` 同时保存邮件关系和登录邮箱绑定,绑定记录的 `email_id=0`;邮箱计数未排除这类记录,但邮件内容查询会排除它们。
- 实测:真实本地 TLS IMAP STATUS 返回 `login_bindings=1 real_messages=15 status_messages=16`,与临时数据库中有效邮件数量不一致。
- 影响:客户端看到的收件箱数量大于实际可读取数量。
- 建议:统一有效邮件关系的筛选条件,或把账户绑定与邮件关系分表;同步检查 SELECT、STATUS、IDLE、Poll 的计数定义。
- 边界:已复现 STATUS 数量错误;其他命令的实际客户端影响仍需分别验证。
- 复现:`TestAuditInboxCountMustExcludeLoginBindings`。
### R16 / P2:符合权限条件的用户角色修改仍被拒绝
- 位置:`backend/controllers/authorization.go:13-24`;`backend/controllers/user.go:218-240`;`backend/services/access/authorize.go:89-95`。
- 前提:超级管理员近期认证有效,目标普通用户已配置恢复码,拟将其调整为管理员。
- 原因:控制器调用通用授权适配器时没有传递 `NewRole`,而 `role.manage` 策略明确要求 `NewRole.Valid()`,因此总是拒绝。
- 实测:直接构造完整授权请求可通过;同样前提下执行真实 EditUser 控制器返回业务码 405,数据库角色仍为 `user`。
- 影响:已有用户的角色提升和降低流程不可用。
- 建议:明确传递旧角色和新角色;修复后同时复查角色变化时会话版本递增逻辑,避免启用当前不可达分支时引入新问题。
- 边界:已验证控制器及临时数据库,不是浏览器用户管理界面的操作验收。
- 复现:`TestAuditAuthorizedRoleChangeMustSucceed`。
### R17 / P1:密码恢复 Cookie 路径不覆盖前端使用的版本化接口
- 位置:`backend/controllers/security.go:583-586,595-598`;`backend/listen/http_server/api_v1_compat.go:27-31`;`frontend/src/views/PasswordRecovery/PasswordRecovery.js:65,130,143`。
- 原因:恢复上下文 Cookie 的 Path 固定为 `/api/security/recovery`,前端接口已改为 `/api/v1/security/recovery/...`;服务端内部路由重写不会改变浏览器的 Cookie 路径匹配。
- 实测:真实恢复申请控制器签发 Cookie;TLS HTTP 客户端配合标准 CookieJar,旧接口后续请求携带 1 个 Cookie、上下文可读取;版本化接口携带 0 个、上下文不可读取。
- 影响:邮箱恢复在验证阶段被拒绝;离线恢复即使得到票据,也会在完成阶段因缺少上下文被拒绝。安全密钥恢复使用同一 Cookie,需纳入修复回归。
- 建议:为新旧接口制定一致且足够窄的 Cookie Path,并同步更新签发、读取、清除与历史 Cookie 兼容测试;不能删除上下文校验来绕过故障。
- 边界:已验证真实 TLS 传输、控制器和 Cookie 路径;未操作浏览器、未投递真实邮件、未完成真实安全密钥恢复。
- 复现:`TestAuditRecoveryCookieMustReachVersionedAPI`,旧路径为通过对照组。
### R18 / P2:POP3 消息序号错误使用全局邮件 ID
- 位置:`backend/listen/pop3_server/action.go:145-176,195-208,215-235,243-245`。
- 原因:没有会话级邮箱快照和从 1 开始的消息序号映射,列表和读取参数直接使用全局邮件 ID。
- 实测:临时邮箱只有两封邮件,LIST 返回编号 305、101,查询 `LIST 1` 返回 `Not authorized`。
- 影响:按 STAT 得到的数量依次读取 1..N 的标准 POP3 客户端不能正确下载邮件,连接期间新增邮件也缺乏稳定序号视图。
- 建议:认证成功后建立稳定的有效邮箱关系快照,分离消息序号和 UIDL 唯一标识,读取、删除和统计共用该映射。
- 边界:已验证真实 Action 和临时数据库;未操作外部 POP3 客户端。
- 复现:`TestAuditPOP3MessageNumbersMustStartAtOne`。
### R19 / P2:POP3 删除标记没有影响当前会话的邮箱视图
- 位置:`backend/listen/pop3_server/action.go:130-138,142-180,190-239,267-272`。
- 原因:DELE 只追加 `session.DeleteIds`,没有校验消息存在性;STAT、LIST、UIDL 没有排除已标记删除项。
- 实测:DELE 后 LIST 和 STAT 仍返回 1 封、UIDL 仍可查询;重复 DELE 和不存在消息的 DELE 都返回成功。
- 影响:客户端看到错误的删除状态,后续同步和退出提交不符合 POP3 事务约定。
- 建议:统一检查会话内有效消息及删除标记,直到 RSET 撤销或 QUIT 提交;不能直接在 DELE 时物理删除邮件。
- 边界:本项验证临时库的真实 Action,不证明跨用户越权删除。
- 复现:`TestAuditPOP3MarkedDeletionMustHideMessage`。
### R20 / P2:POP3 STAT 与 LIST 查询不同的邮箱集合
- 位置:`backend/services/list/list.go:63-64,87-89,117-123`;`backend/listen/pop3_server/action.go:133,215`。
- 原因:STAT 统计全部未删除的接收邮件,LIST 只查询收件箱状态和分组。
- 实测:收件箱 1 封、垃圾箱 1 封时,STAT 返回 2,LIST 只列出 1 封。
- 影响:客户端得到的邮箱数量与实际列表不一致;这与 R19 的会话删除标记遗漏是不同原因。
- 建议:明确 POP3 邮箱可见范围,并让统计、列表、UIDL 和读取共享同一快照。
- 复现:`TestAuditPOP3STATMustMatchVisibleMailbox`。
### R21 / P2:POP3 将标准 RSET 拼成 REST,无法撤销待提交删除
- 位置:`backend/internal/gopop/pop3.go:309-326`;`backend/listen/pop3_server/action.go:61,275-278,327-334`。
- 原因:解析器只识别 `REST`,标准命令 `RSET` 落入不支持的自定义命令分支。
- 实测:通过 `net.Pipe` 执行真实解析器,DELE 后发送 RSET 返回 `-ERR unsupported command`,随后 QUIT 仍把 `[1]` 传给删除提交操作。
- 影响:标准客户端不能用 RSET 撤销删除;若忽略错误继续 QUIT,原有删除集合仍会提交。
- 建议:支持标准 RSET 并测试事务回滚;如保留旧拼写兼容,应明确其非标准身份。
- 边界:数据提供层使用测试 Action,真实解析器和协议响应未替换;没有删除真实或临时数据库邮件。
- 复现:`TestAuditPOP3RSETMustCancelPendingDeletion`。
### R22 / P2:单封 LIST 和 UIDL 被错误编码成多行响应
- 位置:`backend/internal/gopop/pop3.go:232-258`。
- 原因:没有按命令是否携带消息序号区分单行与多行响应,两种情况都输出列表和终止点。
- 实测:真实解析器对 `LIST 1` 和 `UIDL 1` 的首行都只返回 `+OK`,其后还残留 10 和 20 字节的额外列表响应。
- 影响:客户端不能从成功首行解析消息大小或唯一标识,后续命令可能读取到前一命令残留响应。
- 建议:携带消息序号时使用单行成功响应;无参数列表才使用多行与终止点。
- 边界:通过内存连接验证真实响应编码,数据提供层为确定性夹具。
- 复现:`TestAuditPOP3SingleMessageRepliesMustBeSingleLine`。
### R23 / P1:POP3 TOP 负数行参数触发未恢复的切片越界 panic
- 位置:`backend/internal/gopop/pop3.go:77,111,260-267`;`backend/listen/pop3_server/action.go:299-311`。
- 前提:已通过 POP3 认证,拥有一封可正常读取的邮件。
- 原因:解析器只校验整数格式、不拒绝负数;Top 用该值直接计算切片上界,连接处理 goroutine 未设置 panic 恢复。
- 实测:真实解析器把 `-100000` 传入 Top;另一个隔离测试先确认正常 Top 成功,再调用真实 Action,捕获 `slice bounds out of range [:-99992]`。
- 影响:持有有效协议授权码的客户端可以触发未恢复 panic,按 Go 的运行规则会终止后端进程,不只关闭这一条连接。
- 建议:协议入口和处理层双重校验非负行数及有效消息序号,使用安全边界计算;panic 防护只能作为兜底,不能代替参数修复。
- 边界:解析器参数传递与真实 Action panic 分段验证;测试捕获 panic,未向正在运行的服务发送恶意命令,也未实际终止后端进程。
- 复现:`TestAuditPOP3ParserMustRejectNegativeTOPLines`、`TestAuditPOP3NegativeTOPMustNotPanic`。
### R24 / P1:普通收件人的邮件详情响应泄露密送收件地址
- 位置:`backend/dto/parsemail/email.go:381-397`;`backend/listen/smtp_server/read_content.go:275-288`;`backend/models/email.go:19,67-86`;`backend/controllers/email/detail.go:29-36`。
- 前提:同一 SMTP 事务同时投递给可见收件人和密送收件人,原始邮件头不含密送地址。
- 原因:解析器把信封中未出现在 To 的地址推导为 Bcc,完整存入多用户共享的邮件内容记录;收件人详情响应直接序列化 Bcc,没有按收件身份隐藏。
- 实测:原始 MIME 不含测试密送地址,真实解析和入库生成两个用户关系;以普通可见收件人身份执行真实详情服务和 HTTP 响应序列化,响应包含该密送地址。
- 影响:普通收件人可以从 API 获知应保密的其他收件地址,无需拥有管理员权限,也不取决于页面是否展示 Bcc。
- 建议:区分投递信封元数据与用户可见邮件内容;发送者可见的密送清单不能共享给收件人,检查详情、列表、导出等所有序列化入口。
- 边界:隔离 SQLite 验证解析、真实入库、授权详情查询及响应编码;未运行网络 SMTP 投递或浏览器界面。
- 复现:`TestAuditInboundBCCMustNotLeakToVisibleRecipient`。
### R25 / P2:标准 Reply-To 在解析和邮件重建时丢失
- 位置:`backend/dto/parsemail/email.go:402,824-840`。
- 原因:读取的头名是 `ReplyTo` 而非标准 `Reply-To`,BuildBytes 也没有输出已有的 ReplyTo 字段。
- 实测:含标准 Reply-To 的原始 MIME 解析后回复地址数量为 0;单独补入合法 ReplyTo 后重建,结果头仍为空。
- 影响:SMTP 接收、POP3/IMAP 重建读取及发送流程会丢失指定回复地址,客户端可能回复到错误的 From 地址。
- 建议:按标准头名解析并保留回复地址列表,补充解析、持久化、重建的完整往返测试。
- 边界:真实解析和 MIME 重建验证,未实测外部客户端发送回复。
- 复现:`TestAuditReplyToMustSurviveParseAndBuild`。
### R26 / P2:带逗号的合法收件人姓名被拆成无效地址
- 位置:`backend/dto/parsemail/email.go:579-591`;`backend/utils/validation/email.go:40-44`。
- 原因:地址列表直接使用 `strings.Split(line, ",")`,忽略带引号的姓名、注释和标准地址列表语法。
- 实测:`"Doe, Jane" <jane@example.com>` 被解析为两个收件项,第一个是无效片段,第二个姓名也不完整。
- 影响:正常客户端提交这类常见显示姓名时,后续验证可拒绝原本合法的邮件;接收邮件的地址元数据也被污染。
- 建议:使用标准地址列表解析器,错误时明确拒绝,不能按逗号切割后把无效片段当邮箱地址。
- 边界:已验证真实 MIME 解析输出;验证拒绝的调用链来自源码,未操作外部 SMTP 客户端。
- 复现:`TestAuditQuotedRecipientNameMustNotSplitAddress`。
### R27 / P2:邮件重建丢失内嵌图片的 Content-ID
- 位置:`backend/dto/parsemail/email.go:689-700,890-902`。
- 原因:附件重建只写 Content-Type 和文件名,没有输出存储的 Content-ID,也没有保留内嵌附件语义。
- 实测:构造含 `cid:audit-inline` 的 HTML 和同名 Content-ID 的图片附件,真实 BuildBytes 后再次解析,HTML 引用仍在,但附件 Content-ID 变为空。
- 影响:POP3/IMAP 下载、重建转发或发送后,客户端无法把 HTML 内嵌图片引用匹配到附件。
- 建议:保留 Content-ID 和内嵌/下载附件类型,覆盖原始 MIME 到存储再到协议读取的往返流程。
- 边界:本项直接实测 BuildBytes;ForwardBuildBytes 有同样字段遗漏,实际转发客户端显示仍需回归。
- 复现:`TestAuditInlineAttachmentMustPreserveContentID`。
### R28 / P1:数据库迁移丢失自增历史高水位,复用已删除邮件的 UID
- 位置:`backend/services/dbmigration/portable_data.go:23-24,107-155`;`portable_executor.go:208-223`;`backend/services/group/group.go:576-577,600-601`。
- 前提:源库正常分配过邮箱关系 ID,随后删除最大 ID 对应的关系,再执行数据库迁移。
- 原因:迁移只复制现存业务行,不保存源库的自增序列高水位;SQLite 系统序列表被排除,目标序列由剩余行重新生成,文件夹 UIDVALIDITY 又没有重置。
- 实测:正常分配关系 ID 1、2,再删除 2,源序列仍为 2;真实 PortableExecutor 成功迁移后,目标序列为 1,新关系再次获得 ID 2。日志为 `switched=true removed_uid=2 source_sequence=2 target_sequence=1 newly_allocated_uid=2`。
- 影响:迁移成功后复用历史邮件 UID,可能让客户端把新邮件与已缓存或已删除邮件混淆;其他依赖历史唯一身份的自增表也需逐一核验。
- 建议:迁移并校验源库序列及自增历史高水位,目标下一值必须超过历史已分配范围,而不能只使用现存行的 MAX(id)。
- 边界:只实测 SQLite→SQLite,没有执行迁移前后真实 IMAP 客户端缓存验证;与 R13 的 UIDNEXT 倒退不同,本项已经实测数据库 ID 重用。PostgreSQL 的 MAX(id) 序列重建仅有源码证据,未做实库验证。
- 复现:`TestAuditMigrationMustPreserveAllocatedMailboxUIDs`。
### R29 / P1:配置提交失败后的成功重试,会在重启时被旧事务回滚
- 位置:`backend/config/manager_v2.go:163-166,395-397`;`backend/config/transaction.go:101-106,146-161,229-239`;`backend/controllers/config.go:130-150`。
- 前提:一次配置应用在分区提交阶段中断,管理员在同一进程中重新应用,收到成功结果,之后重启服务。
- 原因:失败事务留在事务目录;后续重试新建事务并成功提交,但启动恢复仍对旧的未完成事务无条件恢复原始文件,没有识别它已被更新的成功提交取代。
- 实测:真实 manager 和 loader 配合项目已有故障注入钩子,第一次应用失败、重试成功;重启前磁盘代次为 2、邮件大小策略为 8 MiB,新建 manager 触发启动恢复后变为代次 1、旧值 0。
- 影响:用户已经确认保存成功的配置在下次启动时丢失,当前代次和安全、邮件等配置可能退回旧状态。
- 建议:允许重试或新提交前处理未决事务;恢复时核对代次、内容摘要与事务关系,不能让过期失败事务覆盖后续已确认提交。
- 边界:只操作临时配置目录,使用真实提交、加载和恢复实现;没有重启用户后端,也没有模拟物理磁盘故障。
- 复现:`TestAuditSuccessfulConfigRetryMustSurviveRestart`。
### R30 / P2:保存候选配置后编辑或刷新,页面失去继续操作入口
- 位置:`frontend/src/views/components/ConfigSettings/ConfigSettings.js:131-149,188-200,468-496,587-588`;`backend/config/manager_v2.go:299-300,328-332,368-369,608-609`。
- 前提:保存候选配置成功,但尚未应用;用户继续编辑字段,或确认刷新配置页面。
- 原因:前端清空候选代次及对应输入,后端却继续保留唯一候选并拒绝后续保存;应用方法又在没有本地候选代次时直接返回。重新登录还会因候选绑定旧 SessionID 而不能应用。
- 实测:两种操作均得到 `server_pending=true saved_generation=0 apply_requested=false`,再次保存返回代次冲突;真实后端 manager 另行验证原会话再次保存冲突、新会话保存冲突及应用拒绝。保留代次的原会话可以成功应用,作为对照组。
- 影响:正常编辑或刷新即可让在线配置管理卡住,影响所有共享该候选状态的分区,不是单次按钮提示错误。
- 建议:设计有明确授权的候选查询、取消和重新保存流程,并在刷新后恢复状态;不能简单允许其他会话无条件应用敏感配置。
- 边界:执行真实前端组合函数和表单工具,网络按真实 manager 已验证的约束隔离;未做浏览器与后端连通验收,不宣称重启是唯一恢复方式。
- 复现:前端 `edit`、`refresh` 两项候选测试;后端 `TestAuditPendingConfigBlocksFurtherUpdates` 是约束对照测试,当前通过不代表业务问题已修复。
### R31 / P2:外部邮箱验证码输错一次后,页面不能继续验证原绑定
- 位置:`frontend/src/views/components/SecuritySettings/SecuritySettings.js:212-240`;`SecuritySettings.vue:60-69`;`backend/services/externalemail/repository.go:60-77`;`backend/controllers/security.go:631-673`。
- 前提:第一次绑定已成功发码,用户输错验证码,随后再次点击“绑定并验证”。
- 原因:前端虽然保存了挑战和绑定 ID,但每次点击都重新请求绑定;后端拒绝同地址重复绑定,因而第二次不会弹出验证码输入框。待验证列表也没有继续验证或重发入口。
- 实测:真实前端函数输出 `binding_pending=true prompt_count=1 verify_count=1`,提示先为验证失败、再为绑定失败。真实控制器、仓储及验证码服务对照输出 `after_wrong_status=pending retry_bind_error=100 continued_verify_error=0`,证明服务端允许原挑战正确重试,问题出在页面无法进入该路径。
- 影响:一次普通输入错误即可中断该地址的安全邮箱配置;待验证记录继续占用相应用途名额。取消、刷新及验证码过期场景也需要在修复时补齐恢复策略。
- 建议:显式区分创建绑定、输入验证码、重发和取消状态,失败后保留原挑战并允许再次输入;为已有待验证记录提供受授权的恢复和取消流程。
- 边界:前端网络隔离,后端使用临时 SQLite 和测试发送器,未连通真实浏览器或投递真实邮件;不把停用后重绑定策略直接列为另一项已确认缺陷。
- 复现:前端“外部邮箱验证码输入错误后必须能够再次输入”失败测试;后端 `TestAuditPendingExternalBindingRequiresExistingChallenge` 为当前通过的约束对照测试,不代表页面问题已修复。
### R32 / P1:外部安全邮箱绑定没有执行近期重新认证
- 位置:`backend/controllers/security.go:631-641,655-665`;`backend/listen/http_server/https_server.go:157-165,180`。
- 要求:`docs/超级管理员重置与邮箱验证码需求文档.md:418-437` 明确要求绑定及更换外部邮箱先完成近期重新认证。
- 原因:中间件正确计算 `RecentAuth`,但绑定和验证控制器只检查登录用户与服务是否存在,没有执行近期认证授权。
- 实测:真实 TLS HTTP、版本化路由、CookieJar、会话和 CSRF 配合临时 SQLite。1 分钟会话为成功对照;11 分钟会话已经超过 10 分钟近期认证时限,仍返回 `bind_error=0 verify_error=0 verified_binding=true`。同一会话不携带 CSRF 时返回 100,证明不是绕过 CSRF 得出的结果。
- 影响:持有仍有效但未近期认证会话的人,可以将自己控制的邮箱注册为账户恢复凭据,绕过设计要求的再次确认身份边界。
- 建议:绑定前统一执行近期认证要求,并明确挑战期间认证过期的处理方式;不能把前端弹窗或 CSRF 当成重新认证。
- 边界:测试入口只签发确定年龄的真实会话,被审查接口及安全中间件没有替换;测试发送器捕获验证码,不投递邮件,未实测浏览器或完整账户接管。
- 复现:`TestAuditExternalBindingMustRequireRecentAuthentication`,近期会话及缺失 CSRF 为对照。
### R33 / P2:用户的 Smail 主邮箱可被绑定为外部安全邮箱
- 位置:`backend/controllers/security.go:641-646`;`backend/services/externalemail/repository.go:40-60`;`backend/controllers/login_verification.go:90-103`。
- 要求:`docs/超级管理员重置与邮箱验证码需求文档.md:428,481-487` 禁止外部邮箱与当前用户主邮箱相同,也禁止将登录验证码发回正在登录的 Smail 邮箱。
- 原因:绑定只验证地址格式、指纹和名额,没有与用户主邮箱及认证地址映射比较;后续登录验证直接使用该主要外部邮箱。
- 实测:临时库建立真实主邮箱关系,`auth.ResolveIdentifier` 确认其对应当前用户,再经真实 TLS 版本化接口绑定并验证,结果 `primary_identity_resolved=true sent_to_own_mailbox=true bind_error=0 verify_error=0 verified_primary_external=true`。
- 影响:系统接受不能独立于 Smail 登录访问的恢复渠道;之后登录或忘记密码时可能需要先进入同一个邮箱取码,形成无法完成的流程,并违反外部验证渠道要求。
- 建议:规范化后与用户当前主邮箱及相关认证地址核验,绑定及使用阶段都应保证目标符合外部渠道要求。
- 边界:实测完整绑定及验证,发送目标由测试发送器捕获;登录验证目标选择已核对源码,未执行真实邮件投递和完整登录恢复流程。
- 复现:`TestAuditExternalBindingMustRejectOwnPrimaryMailbox`。
### R34 / P2:邮件详情页的正常附件下载链接被后端拒绝
- 位置:`backend/controllers/attachments.go:37-44`;`backend/listen/http_server/http_server.go:93-94`;`frontend/src/views/EmailDetail/EmailDetail.vue:59`。
- 原因:前端生成 `/attachments/download/{邮件ID}/{索引}`,去掉两端斜杠后实际只有四段;控制器却要求五段并读取索引 3、4,尚未访问附件服务即返回参数错误。
- 实测:临时 SQLite 存入真实附件和用户邮件关系;服务层正确返回内容,其他用户访问被拒绝。相同已登录用户经真实 TLS、Cookie 和完整路由请求 `/attachments/download/1/0`,得到 HTTP 200 和 `errorNo=100`,没有下载响应头或附件内容。
- 影响:从普通邮件详情页点击附件无法下载,不是缺少附件数据、服务未运行或所有权检查导致。
- 建议:统一生成与解析下载路由,严格解析正整数邮件 ID 和非负索引;补充正常链接、越界和非所有者请求的端到端测试。
- 边界:真实 HTTP 层验证,无浏览器点击及真实用户数据操作;未将所有权校验误报为缺失。
- 复现:`TestAuditAttachmentPageDownloadMustReturnStoredContent`。
### R35 / P2:邮件列表表格引用断连,选择和批量操作失效
- 位置:`frontend/src/views/List/List.vue:43,100-117`;`frontend/src/views/List/List.js:26,91-105,120-123,142-145,195`。
- 原因:组合函数返回了 `taskTableDataRef`,但页面没有解构;模板同名字符串引用没有写入组合函数持有的 ref。
- 实测:编译并挂载真实 SFC 和组合函数,模拟表格正常发出选中事件,`selectedRows` 已有一行;点击全选、反选以及三种批量操作,结果 `connected=false cleared=0 toggled=0 requests=[]`,连续三次提示“请先选择邮件”。仅测试内手工连接该引用后,全选及已读、移动、删除请求均正常,作为对照。
- 影响:全选和反选无效果,即使用户已在表格勾选邮件,批量已读、移动、删除仍无法执行。
- 建议:在页面显式绑定组合函数返回的 ref,补真实页面挂载后的操作测试,而非只测试孤立函数。
- 边界:使用真实 Vue 编译器和渲染器,Element Plus 表格与网络为隔离桩;尚未在浏览器用实际表格验收。与 R07 写信页引用遗漏为不同页面的问题。
- 复现:前端“邮件列表挂载后,表格选中项必须用于全选和批量操作”。
### R36 / P1:Windows 升级在备份前失败,回滚会删除旧程序
- 位置:`deploy/install-windows.ps1:131-138,171-183`。
- 前提:已安装实例执行安装或升级,停止服务、复制候选程序或备份旧程序时发生异常。
- 原因:异常处理没有记录哪些目标已备份或替换,直接删除两个现有目标;只有备份存在才恢复。提前失败时,删除的就是尚未替换的旧程序。
- 实测:从当前脚本 AST 抽取真实事务语句,所有文件及服务命令改由内存函数桩执行。停止服务、两次复制、第一次备份失败均让两份旧程序丢失;第二次备份失败只恢复主程序,旧管理程序丢失。两份备份都完成后再失败则能恢复,作为对照。
- 影响:本应保持原安装的普通升级失败会破坏可用安装,导致服务下次无法启动或本地管理程序丢失。
- 建议:按目标记录备份和替换状态,仅清理本次真正安装的新文件;成功备份后才能覆盖,回滚失败时必须保留恢复材料并明确报告。
- 边界:没有执行真实安装脚本主体、删除磁盘文件、停止服务、调用系统安装工具或改变 ACL;此处确认的是实际控制流在指定异常下的结果,双平台部署验收仍未完成。
- 原始复现:`node test/review/run-windows.mjs`,当时六个失败阶段内存验证;修复后已扩展为八阶段,结果见最终回归节。
### R37 / P2:规则编辑取消后点击新建,提交的仍是对旧规则的修改
- 位置:`frontend/src/views/components/RuleSettings/RuleSettings.js:76-83,93-98`;`RuleSettings.vue:35,101`;`backend/controllers/rule.go:122-128`。
- 原因:编辑表单直接共享原列表的条件数组,取消只隐藏弹窗;新建按钮又只显示弹窗,没有重置旧规则 ID 和草稿。
- 实测:编译并挂载真实 SFC 和组合函数,修改条件后通过真实模板点击取消、再点击新建和提交。原列表已变成未保存条件 `unsaved`,新建表单 ID 仍为 7,实际请求为 `/api/v1/rule/update` 且携带旧 ID。后端已核对会按该 ID 更新当前用户的原规则。
- 影响:用户取消的修改没有被丢弃,之后原本想新增的规则会覆盖已有规则,而非创建新记录。
- 建议:编辑时深拷贝条件;新建、取消和关闭均显式管理草稿生命周期,保证新建 ID 为 0;覆盖取消、关闭图标、遮罩关闭后重新新建的测试。
- 边界:真实 Vue 渲染器和模板事件,表格、弹窗容器和网络为桩;没有操作真实用户规则,尚未进行浏览器验收。早期渲染器桩错误不作为业务证据。
- 复现:`node --test test/review/rule-frontend.test.mjs`。
### R38 / P1:ACME 正常返回的完整证书链被重复拼接,自动签发和续期无法保存
- 位置:`backend/services/setup/ssl/ssl.go:366-376,430-446`;`backend/config/ssl_storage.go:221-230,282-290`。
- 原因:签发请求设置 `Bundle: true`,lego 返回的 `Certificate` 已包含中间证书,`IssuerCertificate` 又提供相同链。导入函数把 issuer 无条件追加到已完整的链尾,随后按相邻证书校验签名,最终尝试让非自签中间证书验证自身签名。
- 实测:生成根、中间及叶证书,完整链独立通过私钥匹配及域名、签名链校验;将与 lego `Bundle=true` 一致的完整链及独立 issuer 一起传入真实 `writeCertificateFiles`,返回 `invalid SSL certificate material`。当前依赖 lego v4.34.0 的 `acme/api/certificate.go:83-94` 与 `certificate/certificates.go:409-411` 已核对这种响应结构。
- 影响:常见的叶证书加非自签中间证书响应会被拒绝,初次自动签发不能进入完成阶段,自动续期也不能安装新证书。
- 建议:明确叶证书、完整链与 issuer 的接口语义,按证书 DER 去重或只使用已完整的链;保留真正的不匹配、过期和非法链拒绝测试。
- 边界:使用本地生成的证书复现实际存储入口,没有请求真实 CA、提交 DNS 挑战或消耗签发配额;不声称所有可能的证书链均会失败。
- 复现:`TestAuditACMEBundledChainMustBeImportable`。
### R39 / P2:有效泛域名证书被判定名称不匹配,每次检查都触发续期
- 位置:`backend/services/setup/ssl/ssl.go:298-315,512-518,530-548`;`backend/listen/cron_server/ssl_update.go:69-76`。
- 原因:DNS 模式主动申请 `*.example.test`,但健康检查只判断 SAN 文本是否包含 `imap`,而不是按 X.509 域名匹配规则判断服务名称。
- 实测:通过真实受控存储写入 90 天有效的 `*.example.test` 完整链,真实 `VerifyHostname("imap.example.test")` 成功;`CheckSSLCrtInfo` 却返回 `days_remaining=89 name_match_fail=true`,续期条件为 true。
- 影响:DNS 泛域名模式在证书足够有效时仍反复申请证书,需要重复人工 DNS 挑战,并可能消耗签发配额;成功更新时还触发不必要的重启。
- 建议:根据实际服务域名调用标准域名校验,支持泛域名,不使用名称子串推断证书有效性。
- 边界:为隔离 R38,本测试不重复附加 issuer;只验证真实检查结果与续期条件,没有向 CA 发请求或实际重启服务。
- 复现:`TestAuditFreshWildcardCertificateMustNotRequireRenewal`。
### R40 / P1:分区配置下自动续期调用旧写入器,报错后证书目录却已切换
- 位置:`backend/services/setup/ssl/ssl.go:256-267,376-377,541-548`;`backend/config/config.go:281-288`;`backend/config/ssl_storage.go:137-160`。
- 前提:已经迁移为分区配置,使用系统管理的自动证书;证书签发结果本身有效。
- 原因:续期先调用 `ImportManagedSSL` 切换受控 `current` 目录,再调用明确禁止覆盖分区配置的旧版 `WriteConfig`。后者失败后没有回滚证书,调用方按失败退出,不发送成功更新后的重启信号。
- 实测:临时根目录中生成有效证书、真实迁移并加载 ready 状态分区配置,再调用真实续期存储入口。结果为 `root_unchanged=true current_changed=true current_is_new=true loaded_tls_is_old=true`,错误为“分区配置不能通过旧版写入接口覆盖,请使用配置管理”。
- 影响:续期无法完成正常提交和重启路径,磁盘证书已经换新,但已加载的 TLS 配置仍持有旧证书;外部看到失败状态,实际受控存储却已变化。
- 建议:接入分区配置管理器的提交及运行时应用流程,协调证书切换、配置提交和失败回滚;不能简单移除旧写入器对分区根的保护。
- 边界:为独立于 R38,本测试不重复附加 issuer;未请求 CA、启动真实 TLS 监听或重启用户服务,已加载证书对象作为运行时持有旧材料的对照。
- 复现:`TestAuditPartitionedRenewalMustCommitBeforeSwitch`。
### R41 / P2:旧配置迁移静默将自动 DNS 证书改成 HTTP 挑战
- 位置:`backend/config/migrate.go:717-733`;`backend/config/partitioned_compatibility.go:75-78`;`backend/services/setup/ssl/ssl.go:337-347,400-409`。
- 原因:迁移仅区分系统管理和用户上传,不保存原来的自动 HTTP / DNS 签发方式;兼容加载将所有 `managed_default` 固定映射成 `SSLTypeAutoHTTP`。
- 实测:真实迁移输入 `SSLTypeAutoDNS` 并重读分区配置,得到 `before_ssl_type=2 after_ssl_type=0 before_dns_challenge=true after_http_challenge=true`;迁移没有拒绝或提示这一配置丢失。
- 影响:原本依赖 DNS 挑战的部署后续走 HTTP 挑战,在没有公网 HTTP 可达条件时不能续期,泛域名申请策略也改变。
- 建议:把 ACME 挑战方式作为独立配置保留和校验;暂不支持时明确阻止迁移并提示人工处理,不能静默换成另一种方式。
- 边界:验证配置迁移、兼容加载和挑战分支选择;未进行真实网络挑战。与 R40 保存阶段失败属于不同根因。
- 复现:`TestAuditPartitionedMigrationMustPreserveDNSChallengeMode`。
### R42 / P1:旧配置迁移丢弃 SQLite 目录,现有数据库被改指向空库
- 位置:`backend/config/migrate.go:581-592`;`backend/config/partitioned_compatibility.go:89-92`;`backend/config/config.go:370-372,403-428`。
- 前提:旧版 SQLite 数据库使用带子目录的有效路径,例如 `./config/archive/review.db`。
- 原因:迁移通过 `filepath.Base` 只保留文件名,兼容加载再将文件名固定拼到根目录的 `config` 下,没有搬迁原库或验证路径等价。
- 实测:临时原库保存真实 SQLite 标记表,旧配置能够正确解析其位置;执行真实配置迁移后重读得到另一个路径。结果 `source_exists=true target_missing_before_open=true same_path=false marker=""`,查询报 `no such table: audit_marker`。
- 影响:配置格式迁移成功,但后续数据库连接指向新建空库,原有账号和邮件不再从原路径读取。原数据库仍存在,不是文件被删除。
- 建议:完整保留并验证已有数据库位置;若新格式只允许固定位置,应明确拒绝迁移或在备份及校验后受控搬迁,不能静默改变连接目标。
- 边界:使用临时标记表验证配置迁移和真实 SQLite 打开行为,不是三种数据库搬迁服务测试,也未启动正式用户实例。
- 复现:`TestAuditLegacyMigrationMustPreserveSQLiteLocation`。
### R43 / P1:生产 DKIM 校验保留特殊域名放行分支,无效签名被判为通过
- 位置:`backend/dto/parsemail/dkim.go:134-142`;`backend/utils/consts/consts.go:4`;`backend/listen/smtp_server/read_content.go:121,131-148,295`。
- 原因:检查签名错误之前,只要验证结果中的域名等于测试常量就立即返回 true。该函数由正常匿名 SMTP 收信路径调用,不受测试构建或开发模式限制。
- 实测:构造缺少必要签名标签的邮件,真实 DKIM 库返回 `signature missing required tag`;普通保留示例域名的应用结果为 false,相同无效结构改用特殊常量域名则为 `application_passed=true`。
- 影响:外部发件人可以使无效 DKIM 被记录为通过,污染认证结果及依赖该结果的插件判断;不能把该结果当作可信邮件认证依据。
- 建议:删除运行时代码中的测试域名放行,只依据实际校验结果判断;测试通过注入验证器或真实隔离密钥实现,不能保留生产后门分支。
- 边界:直接执行真实 DKIM 校验入口,验证器在 DNS 查询前失败,不请求外网。SMTP 调用、认证对象和落库字段已核对源码,未做真实外部 SMTP 投递;不声称绕过所有其他反垃圾检查。
- 复现:`TestAuditInvalidDKIMMustNotPassForSpecialDomain`,包含普通域名拒绝对照。
### R44 / P2:IMAP SEARCH 的多个独立搜索键被错误取并集
- 位置:`backend/services/list/imap_search.go:172-187,191-205,465-512`;`backend/listen/imap_server/session_search.go:20`。
- 原因:UID、序号过滤命中任意一个集合就保留;多个 OR 键也共享同一个累计结果集合。同一 SEARCH 中独立键应互相 AND,只有单个 OR 键内部两项取并集。
- 实测:真实本地 TLS IMAP 创建三封邮件,对 `{A,B}` 和 `{B,C}` 两个 UID 键搜索,返回 A/B/C;两个独立 OR 键的相同组合也返回 A/B/C,均应只返回 B。
- 影响:客户端组合筛选返回不符合全部条件的邮件,搜索结果集合错误。
- 建议:按搜索语法树逐键求交,OR 节点内部求并,并覆盖多个 UID、序号、OR 及其嵌套组合。
- 边界:真实 TLS 协议验证 UID 和 OR 组合;序号分支具有相同静态逻辑,但未在本用例单独实测。不涉及跨用户访问结论。
- 复现:`TestAuditSearchKeysMustIntersect`。
### R45 / P2:IMAP SEARCH DELETED 与 STORE/FETCH 使用不同状态字段
- 位置:`backend/services/list/imap_search.go:239-245`;`backend/listen/imap_server/session_store.go:154-170`。
- 原因:STORE 持久化 `is_deleted`,FETCH 据此返回删除标记;SEARCH 却检查用于网页文件夹分类的 `status == 3`。
- 实测:真实 TLS IMAP 对收件箱邮件执行 `STORE +FLAGS (\Deleted)` 后,FETCH 明确返回该标记,但同 UID 的 `SEARCH DELETED` 返回空集合。
- 影响:客户端不能正确搜索已标记删除的邮件,`UNDELETED` 的判断也与实际标记不一致。
- 建议:协议层统一使用独立的 `is_deleted` 标记,不混用网页垃圾箱状态;补充设置、搜索、撤销、再次搜索的协议测试。
- 边界:使用隔离 SQLite 和本地 TLS,验证 STORE、FETCH、SEARCH,未对真实用户邮箱执行删除或 EXPUNGE。
- 复现:`TestAuditSearchDeletedMustMatchStoredFlag`。
### R46 / P1:关闭 Telegram 附件推送仍上传邮件附件
- 位置:`plugins/pmail_telegram_push/hook/hook.go:83-94`;`hook/send.go:172-177,181-218`。
- 原因:用户设置正确传入 `SendAttachments=false`,但通知发送和附件批次发送均不检查该字段。
- 实测:通过真实 `ConfigureUserSettings`、`ReceiveSaveAfter` 和 Telegram SDK,在内存 HTTP 客户端捕获到 `enabled=false messages=1 uploaded_files=2`;启用开关的对照同样发送两个附件。
- 影响:用户明确关闭附件外发后,附件内容仍进入第三方上传请求,违反隐私设置。
- 建议:在附件发送入口检查用户开关,并覆盖直接调用和收信钩子两条路径。
- 边界:使用虚构测试 token 和内存客户端,未向 Telegram 发送真实请求;生产插件的启动与隔离仍需平台验收。
- 复现:`TestAuditDisabledAttachmentsMustNotBeSent`。
### R47 / P2:Telegram 普通主题和正文未转义却按 HTML 发送
- 位置:`plugins/pmail_telegram_push/hook/send.go:24-28,87-95,157-160`。
- 原因:邮件主题和 `Text` 原样插入通知,`sendText` 固定指定 `ParseModeHTML`。
- 实测:真实 SDK multipart 请求中 `parse_mode="HTML" raw_subject=true raw_body=true`;正常文本里的尖括号和 `&` 没有转义。
- 影响:普通文本可能被解释为格式或链接,含不支持标签时会导致 Telegram 拒绝通知;附件依赖文本发送成功,也会受影响。
- 建议:普通文本统一 HTML 转义,只对经过白名单处理的邮件 HTML 保留受支持标记。
- 边界:已验证真实请求构造,未请求真实 Telegram API,不能将模拟响应当成 Telegram 接收结果。
- 复现:`TestAuditTelegramPlainTextMustBeEscaped`。
### R48 / P2:Telegram 长中文通知截断破坏 UTF-8
- 位置:`plugins/pmail_telegram_push/hook/send.go:120-128`。
- 原因:用字节长度和 `text[:4076]` 截断 Unicode 文本,没有字符边界检查。
- 实测:合法中文输入在三种前缀长度中有两种生成 `output_bytes=4079 output_valid=false`,另一种恰好对齐字节边界的对照通过。
- 影响:正常中文通知末尾损坏;还需独立检查截断后的 HTML 标签与实体完整性。
- 建议:按 Telegram 文本限制与 Unicode 边界处理长度,再保证 HTML 标签、实体和 spoiler 闭合。
- 边界:证明函数输出无效 UTF-8,不声称真实 API 必然返回某个错误码。
- 复现:`TestAuditTelegramTruncationMustKeepUTF8`。
### R49 / P2:第三方协议关闭失败后仍显示已关闭
- 位置:`frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.js:41-56`;`ThirdPartyAuthorization.vue:34-39`。
- 原因:`v-model` 在 `change` 前更新 `item.enabled`,处理函数保存的 `previous` 已是新值,失败回滚不能恢复原状态。
- 实测:编译并挂载真实 Vue 模板,按已核对的 Element Plus 事件顺序触发开关;模拟请求失败后 `serverEnabled=true uiEnabled=false`。成功请求的对照通过。
- 影响:用户以为 IMAP 等客户端访问已关闭,但服务端没有关闭;虽出现失败通知,页面持续状态仍然误导。
- 建议:改用受控状态或 `before-change`,请求成功才提交新值;失败时回读服务端状态。
- 边界:组件事件与请求失败为隔离模拟,未做浏览器或真实协议关闭验收。
- 复现:`test/review/protocol-frontend.test.mjs`。
### R50 / P2:Telegram 设置页与 iframe 隔离策略不兼容,初始化和 API 均被阻断
- 位置:`frontend/src/views/components/PluginSettings/PluginSettings.vue:13-16`;`plugins/pmail_telegram_push/src/App.vue:241-249`;`backend/listen/http_server/https_server.go:244-246`。
- 原因:插件 iframe 只有 `allow-scripts`,页面挂载时却先读取 `localStorage`,之后才获取设置;同时插件响应的 CSP 明确 `connect-src 'none'`,页面却直接通过 Axios 调用主程序 API。
- 实测:在 Smail 专属 MCP 浏览器中运行源码提取的真实挂载函数、sandbox 和 CSP。`localStorage` 产生 `SecurityError`,后面的获取设置函数未执行;独立请求探针触发 `connect-src` 违规,服务端未收到 API 请求。
- 影响:Telegram 设置页无法正常初始化、读取或保存设置;不是简单重启或清浏览器缓存能解决的问题。
- 建议:保留插件隔离,采用主程序原生配置页面或严格限定来源、动作和字段的消息桥接。不能简单同时给不可信插件添加 `allow-same-origin` 和 `allow-scripts` 来绕过安全边界。
- 边界:这是使用源码策略和挂载函数的浏览器隔离夹具,不是当前生产插件二进制或完整页面部署验收;未修改真实配置和数据库。
- 复现:`node test/review/plugin-browser.mjs`;MCP 内联结果转录在 `test-log/review-plugin-browser-1788639873115/browser-evidence.json`。MCP 自带文件输出因工具工作区限制失败,随后读取内联结果并明确标记转录来源。
### R51 / P2:投递失败信息不能通过插件 RPC,发送后钩子被跳过
- 位置:`backend/hooks/framework/framework.go:40-46,162-170,195-202`;`backend/hooks/base.go:67,131-132`。
- 原因:通信对象直接使用 `map[string]error`。非空 Go 错误被 JSON 编码为对象,接收端不能将该对象反序列化到 `error` 接口,因此尚未执行钩子就返回 400。
- 实测:使用真实 DTO、JSON 编解码和 `handleHookDTO`,成功投递对照为 `status=200 callback=true`;投递失败为 `status=400 callback=false`,错误消息没有到达插件。
- 影响:插件不能可靠接收发送失败通知或执行其失败处理逻辑,恰好在需要错误信息时丢失回调。
- 建议:为 RPC 定义明确的可序列化错误对象或字符串映射;接口适配层负责与 Go `error` 转换,并保留成功和失败对照测试。
- 边界:验证的是实际处理函数和序列化链,不是外部邮件投递或生产插件进程验收;不声称现有插件都实现了发送失败通知。
- 复现:`node test/review/run.mjs --framework-only`;日志 `test-log/run-2026-09-05T20-29-56-638Z-30704/backend.log`。
## 已排除线索与通过的对照
- 首次管理员角色:使用全新临时 SQLite、真实 15 次版本化迁移、真实 `SetAdminPassword` 并关闭重开数据库。新建和重启后均为 `is_admin=1 role="" effective_role="super_admin" config_read=true role_manage=true`。xorm 实际写入空角色,兼容回退生效,不能仅凭 schema 默认值推断管理员降为普通用户。不登记为缺陷,也不泛化为 MySQL/PostgreSQL 实库已验证。
- TOTP 登记密钥并发线索:16 个 goroutine 各执行 150 次真实 `BeginEnrollment`,`-race` 通过,没有复现数据竞争;当前全仓 Go 调用搜索也未找到该服务被业务包引用。不列为确认问题,不能据此声称完整 TOTP 用户流程已经接入或通过验收。
## 待诊断项,不列为已确认问题
- 同文件夹 COPY 的身份覆盖已验证为 R11,其他批量副本操作仍需分别验证。
- Windows 正式模式插件隔离存在明确不可用分支,仍需核对功能开关、实际调用链和文档承诺。
- SMTP MX 回退、IMAP 动态序列其他命令、HTML 入库与插件路径等尚需继续检查;UIDNEXT 和 UID FETCH 通配符的已验证问题见 R13、R14。
## 原始复现与验证记录
以下命令和日志保留审查阶段的先失败证据;修复后的同名测试现已通过,不应将历史失败日志解释为当前程序仍出现相同故障。
```powershell
# 原有测试基线
node test/run.mjs all -count=1 ./...
# 独立审查测试:审查阶段曾失败,用于暴露当时尚未修复的问题
node test/review/run.mjs
node test/review/run.mjs -race
node test/review/run.mjs --pop3-only
node test/review/run.mjs --message-only
node test/review/run.mjs --migration-only
node test/review/run.mjs --config-only
node test/review/run.mjs --controllers-only -run=^TestAuditPendingExternalBindingRequiresExistingChallenge$
node test/review/run.mjs --http-only -run=^TestAuditExternalBinding
node test/review/run.mjs --http-only -run=^TestAuditAttachment
node test/review/run-windows.mjs
node test/review/run.mjs --setup-only -run=^TestAuditFirstAdministrator
node test/review/run.mjs --ssl-only
node --test test/review/rule-frontend.test.mjs
node --test test/review/frontend-findings.test.mjs
node test/review/run-telegram.mjs
node --test test/review/protocol-frontend.test.mjs
node test/review/run.mjs --framework-only
# 逐行覆盖账本,不代表测试覆盖率或功能验收率
node test/review/coverage.mjs
```
- 原有前端 50/50 及后端全量通过:`test-log/run-2026-09-05T17-49-53-054Z-47380/`。
- 重点包原有 race 基线:`test-log/run-2026-09-05T17-51-55-556Z-29640/backend.log`,通过不代表新增并发路径已覆盖。
- 已确认后端复现:`test-log/run-2026-09-05T18-13-47-945Z-448/backend.log`。
- 运行时 race 证据:`test-log/run-2026-09-05T18-09-57-603Z-29380/backend.log`。
- 新增副本身份复现:`test-log/run-2026-09-05T18-20-32-820Z-49756/backend.log`。
- 三项标准 IMAP 行为及 race 重跑:`test-log/run-2026-09-05T18-23-59-068Z-51124/backend.log`。
- 包含 R15 的后端复现:`test-log/run-2026-09-05T18-25-37-339Z-30964/backend.log`。
- 包含 R16、R17 的后端复现:`test-log/run-2026-09-05T18-32-47-015Z-27832/backend.log`。
- POP3 的 R18–R23 隔离复现:`test-log/run-2026-09-05T18-44-16-845Z-41276/backend.log`。
- MIME 与密送隐私的 R24–R27 隔离复现:`test-log/run-2026-09-05T18-48-38-700Z-47076/backend.log`。
- 迁移的 R01、R03、R28 隔离复现:`test-log/run-2026-09-05T18-55-32-900Z-28932/backend.log`。
- 配置事务的 R29 隔离复现:`test-log/run-2026-09-05T19-02-28-851Z-43688/backend.log`。
- 配置事务重跑与 R30 后端约束:`test-log/run-2026-09-05T19-09-01-546Z-43540/backend.log`。
- R31 后端真实约束及已知问题重跑:`test-log/run-2026-09-05T19-17-39-345Z-44436/backend.log`。
- R31 修正测试筛选参数后精确重跑通过:`test-log/run-2026-09-05T19-19-04-347Z-42100/backend.log`;仅对照测试通过,不表示页面问题修复。
- R32、R33 真实 TLS 和 CSRF 隔离复现:`test-log/run-2026-09-05T19-26-18-382Z-44472/backend.log`;近期会话对照通过,两项安全边界断言预期失败。
- R34 附件下载及 R32、R33 完整路由重跑:`test-log/run-2026-09-05T19-34-58-471Z-48248/backend.log`。
- R35 列表引用挂载复现及手工连接对照:`test-log/review-list-1788636898383.log`。
- R36 Windows 安装事务内存隔离验证:`test-log/review-windows-1788637055598/windows.log`;前五种阶段暴露缺陷,备份完成后的失败能恢复。
- R37 规则编辑取消再新建:`test-log/review-rules-1788637601791.log`;旧 ID 与未保存条件一起提交到更新接口。
- 首次管理员角色排除对照:`test-log/run-2026-09-05T19-44-36-177Z-37720/backend.log`,新建及重启均通过。
- R38 完整链重复与 R39 泛域名误续期:`test-log/run-2026-09-05T19-51-15-151Z-40896/backend.log`,两项缺陷断言失败。
- TOTP 并发线索隔离对照:`test-log/run-2026-09-05T19-53-03-838Z-34048/backend.log`,`-race` 通过,未确认缺陷。
- R40 分区续期保存与 R41 DNS 挑战方式丢失:`test-log/run-2026-09-05T19-58-24-637Z-33360/backend.log`,两项缺陷断言失败。
- R42 SQLite 配置迁移路径丢失:`test-log/run-2026-09-05T20-02-12-179Z-39988/backend.log`,真实原库仍存在,新连接目标缺少标记表。
- R43 无效 DKIM 放行、R44 搜索键并集、R45 删除标记搜索:`test-log/run-2026-09-05T20-07-31-623Z-3388/backend.log`,三个问题的安全或语义断言均失败;普通域名 DKIM 拒绝对照通过。
- R46–R48 Telegram 附件开关、HTML 与中文截断:`test-log/review-telegram-2026-09-05T20-16-00-530Z/telegram.log`,三个缺陷断言失败,附件开启与对齐截断对照通过。
- R49 协议开关失败回滚:`test-log/review-protocol-20260906-041737.log`,失败请求状态一致性断言失败,成功请求对照通过。
- 包含候选配置与外部邮箱重试的五项前端失败复现:`test-log/review-frontend-2026-09-05T19-15-56-197Z/frontend.log`。
- 前端两项复现日志:`test-log/frontend-review-20260906-022032.log`。
- 测试源码:`test/review/`;Go overlay 将测试注入对应包,不为执行测试改写业务源码;修复阶段的业务修改另由 Git 差异记录,运行产物放在 `test-log/`。
- `^TestAudit` 还会匹配项目原有审计接口测试,因此测试用例总数不等于新增问题数。
- 未运行带 `--fix` 的 lint,不用自动格式化或文本替换悄悄改动业务源码。
## 修复后最终回归与构建
### 自动化回归
本批测试使用隔离库、临时配置及本地协议夹具,没有清空真实运行库,也没有执行用户要求的“清库后按生产环境重新初始化”验收。
| 验证 | 最终结果 | 本地证据 |
| --- | --- | --- |
| 前后端全量 | 前端 55/55,后端全量通过 | `test-log/run-2026-09-06T04-34-53-174Z-29120/` |
| 后端全量 race | 通过 | `test-log/run-2026-09-06T04-34-53-169Z-15200/backend.log` |
| 独立审查 race | 通过,包含最后三项补充修复 | `test-log/run-2026-09-06T04-34-53-289Z-17348/backend.log` |
| 独立审查普通 | 通过,包含最后三项补充修复 | `test-log/run-2026-09-06T04-40-27-845Z-16924/backend.log` |
| 独立前端审查 | 11/11 | `test-log/review-frontend-final-20260906-123539.log` |
| Windows 升级回滚 | 八个失败阶段,两份旧程序均保留 | `test-log/review-windows-1788669339410/windows.log` |
| Telegram | 隔离测试通过 | `test-log/review-telegram-2026-09-06T04-35-50-100Z/telegram.log` |
主要复核命令:
```powershell
node test/run.mjs all -count=1 -timeout=180s ./...
node test/run.mjs backend -race -count=1 -timeout=240s ./...
node test/review/run.mjs -race -timeout=240s
node test/review/run.mjs -timeout=240s
node --test test/review/frontend-findings.test.mjs test/review/rule-frontend.test.mjs test/review/protocol-frontend.test.mjs
node test/review/run-windows.mjs
node test/review/run-telegram.mjs
```
独立审查 race 日志中的隔离 IMAP 连接空闲超时不代表用户后端故障;相关用例和包最终通过。`^TestAudit` 也会运行项目原有审计接口测试,因此测试数量不等于问题数量。
### 构建产物
- 前端:`out/frontend`,已同步到 Go 嵌入目录 `backend/listen/http_server/dist`;1858 个模块转换,构建 9.03 秒,postbuild 成功。日志:`test-log/review-build-frontend-20260906-124143.log`。
- 前端仍有超过 500 kB 的分块警告,属于待优化项;PowerShell 将标准错误流显示为 `NativeCommandError`,但本次构建及 postbuild 已成功,不能据此判为构建失败。
- 后端:`out/review-fixes-20260906-124516/`,包含主程序、管理员工具及配置工具,均包含最后三项补充修复。日志:`test-log/review-build-backend-20260906-124516.log`。
- 构建元数据:`Version=review-fixes-20260906-124516`,`GitHash=addfcea1cdb59f1fd28bd1639c54502cb05990de-dirty`,`BuildTime=2026-09-06T04:45:17Z`,`GoVersion=go1.26.6`。这是最终提交前工作树的验证产物,不冒充最终提交哈希构建。
| 文件 | SHA-256 |
| --- | --- |
| `smail.exe` | `67BA0822498E8724CC51E811793CFE2347D4AC642CE8EAB82B4062DBDC63AF52` |
| `smail-admin.exe` | `F1F2F7655224661276AF44A16947303405B275938D3C241CFC0A6DF32785EC6B` |
| `smail-config.exe` | `FB05E8673E2B807F46CC51D0366E6269841927D3AA82D6320249C6699371C228` |
旧目录 `out/review-fixes-20260906-121853/` 不含最后三项修复,不用于本节结果对应的部署。旧产物保留,没有清理用户文件。根 `config/`、`ssl/`、`test-log/`、`out/` 及嵌入产物不纳入本次源码提交。
### 仍需继续的验收
- R31 外部邮箱重新发码、输错重试及倒计时过期提示待浏览器 MCP 模拟键鼠真实验收。
- R40 的已提交候选受授权恢复需要真实运行验证,自动恢复和真实 CA 签发、续期未闭环。
- 登录刷新保持会话、setup、忘记密码、登录二次验证、强制改密、普通邮箱列表、审计 Blob 下载、自签名 HTTPS Cookie/CSRF 尚需浏览器 MCP 真实验收。
- MySQL/PostgreSQL 的索引与自增迁移、角色变更后的 HTTP 会话失效、Linux/Windows 正式部署、真实邮件及插件投递待验。
- HTTP 完整在途排空、POP3 字节大小及转义、网页内嵌图片显示等专项仍需补充。
- **用户正在运行的实例尚未部署本批新程序;测试通过及构建成功不意味着页面或运行中的后端已更新。**
## 覆盖说明
- 覆盖记录源:`test/review/reviewed-ranges.json`。
- 账本基线仍为 `9849f8f11c9f49efd1bc877d7fedd8da265e32df`,只对应原始审查版本;没有修改基线来伪造修复后的覆盖率。覆盖工具会拒绝与该基线不一致的 HEAD,当前修复差异复核也不等于新版本全仓逐行覆盖。
- 生成账本包含每个受版本控制文件的路径、分类、行数、SHA-256、阅读行范围及发现编号。
- `read-not-accepted` 只表示全文已读,不表示无问题,也不表示复审通过。
- `partial-read` 表示只读取部分行;`not-recorded` 表示没有登记,接续摘要中模糊描述的已读内容不冒充全文覆盖。
- 新增且尚未提交的审查文件不属于基线受控文件,单独验证;日志和运行数据库不纳入业务源码逐行统计。
- **当前不能声称“每一行都审查完成”或“全部功能已实现并验收”。**