# 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=`。 - 影响:迁移被报告成功,但源库禁止的重复值可以写入目标库。 - 建议:无法忠实迁移的结构在预览和执行时明确拒绝,或完整迁移并比较索引、约束。 - 边界:只实测 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" ` 被解析为两个收件项,第一个是无效片段,第二个姓名也不完整。 - 影响:正常客户端提交这类常见显示姓名时,后续验证可拒绝原本合法的邮件;接收邮件的地址元数据也被污染。 - 建议:使用标准地址列表解析器,错误时明确拒绝,不能按逗号切割后把无效片段当邮箱地址。 - 边界:已验证真实 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` 表示没有登记,接续摘要中模糊描述的已读内容不冒充全文覆盖。 - 新增且尚未提交的审查文件不属于基线受控文件,单独验证;日志和运行数据库不纳入业务源码逐行统计。 - **当前不能声称“每一行都审查完成”或“全部功能已实现并验收”。**