diff --git a/docs/远程服务器测试记录.md b/docs/远程服务器测试记录.md index 5f7c58f..18e6310 100644 --- a/docs/远程服务器测试记录.md +++ b/docs/远程服务器测试记录.md @@ -17,7 +17,9 @@ - [✓] 主界面使用中文组件语言,空列表、关闭对话框、关闭标签、数值按钮均有实际页面证据。 - [✓] 配置、安装及管理员恢复相关 Linux 测试包在远程执行通过。 - [✓] 恢复 socket 为 `smail:smail 0600`;非授权服务账号直接运行恢复工具被拒绝。 -- [!] 邮件实际投递未通过:网络连接与证书信任阻塞,未绕过校验。 +- [✓] 内部 25 端口真实收信、中文正文、多收件人、中文附件及下载内容通过。 +- [✓] 普通用户不能读取管理员专属邮件或下载其真实附件。 +- [!] 网页按 MX 路由的实际投递未通过:网络连接与证书信任阻塞,未绕过校验;按最新要求不再要求公网 25 端口连通。 - [✓] 修复异步发信提前显示成功:网页提交显示正在投递,列表先显示待投递,手动刷新后显示实际失败。 - [✓] 修复 587 端口误用隐式 TLS,远程 STARTTLS 已进入证书校验阶段。 - [!] 当前仍使用原有定时与手动刷新机制;快速状态轮询、进程中断后的待投递恢复未验收。 @@ -33,12 +35,14 @@ `49ab24b` 修复异步邮件投递状态与 587 端口加密协议。邮件与用户关联先以既有待投递状态入库;列表接口返回真实状态,页面不再将错误文本为空当作成功;587 使用 STARTTLS,保留证书校验及禁止明文回退的边界。 +`416b849` 修复标准 MIME 中文附件名称解析。原正则无法识别 RFC 2231 的 `filename*=`,导致真实收信附件名变成 `no_name_file`;改为使用现有 `go-message` 参数解析器,保留 `Content-Type name` 优先级,新增普通名称、带分号名称、UTF-8 扩展参数、分段参数、编码词、旧式名称、优先级、缺省及非法参数共九个用例。 + 两次清库安装后,前端修复及邮件状态修复均采用备份后保留数据库的升级部署,用于同轮故障回归和验证会话、配置保持,不宣称这些版本又进行了一轮全新清库安装。 最终 Smail 二进制 SHA-256: ```text -20a0d08dd9f2c2b6bb60e7c1e36f9c09b571c0c32f35d787d06cbe5792c6a214 +e0455f8156f4bc0ff25136ffb39e42566b0b154165eb3ce8eb4fb937ba6fe1f6 ``` 远程服务在部署后保持 active,HTTP 80、HTTPS 443 正常监听。`config.json`、`email.json`、`plugins.json`、`smtp.json`、`ssl.json` 均为 `smail:smail 0600`。 @@ -66,12 +70,37 @@ - `setup.test`、`config.test` 在远程以 `smail` 身份执行通过。 - `adminreset.test` 在远程以 `smail` 身份执行全部通过,覆盖调用身份、一次性挑战、防重放、过期、socket 权限、重置事务与凭据撤销。 - `email.test`、`send.test`、`smtp.test` 在远程以 `smail` 身份执行全部通过,新增实际持久化函数待投递状态、关联写入失败回滚、列表接口三种状态回归。测试使用隔离内存数据库,不写部署业务库。 +- `parsemail.test` 在远程以 `smail` 身份执行全部通过,包含九个新增附件名称用例以及既有正文、隐私和 XSS 用例。 - 配置测试曾因从 `/root` 继承工作目录而失败,改在服务账号可访问的测试目录执行后通过,没有放宽权限。 - 前端生产构建与 Linux Go 编译通过;构建有编辑器块超过 500 kB 的提示。 - 本次没有重新执行全部后端包、竞态测试或前端自动测试,不能沿用此前本机全量测试作为远程全量结论。 - 桌面浏览器已检查页面快照、截图和发件页控制台,所查控制台没有错误;未进行移动端检查。 - 键盘选择回归仅检查表单行为,未向不存在的选择测试地址发送邮件。 +## 内部 25 端口验收 + +根据用户最新要求,仅从 47 服务器连接 `127.0.0.1:25`,不要求公网 25 端口连通。测试脚本为 `test/tools/internal_smtp_check.py`,本机没有执行该脚本。测试客户端显式信任服务器现有测试证书,仍校验 `mail.solit.cn` 主机名、证书有效期和证书链;没有修改应用证书校验、系统 CA、DNS 或中继配置。 + +21:26:17 最终一轮五项测试全部完成,全部使用 TLS 1.3,SMTP DATA 返回 250,随后核对真实 SQLite 数据与用户关联,并非仅以协议返回码判定成功: + +| 用例 | 邮件 ID | 实际结果 | +| --- | --- | --- | +| 中文纯文本 | 15 | 仅普通用户收件,正文一致 | +| HTML 与中文附件、多收件人 | 16 | 管理员和普通用户均关联,正文、附件名及附件字节一致 | +| 管理员专属附件 | 17 | 仅管理员关联,用于附件越权对照 | +| 未知本域收件人 | 18 | 当前过滤级别 0,转管理员收件箱 | +| 未认证外域收件人 | 19 | 当前策略转管理员收件箱,仅有收信记录,没有发信记录 | + +TLS 前未公布 AUTH,主动尝试明文 AUTH 返回 523。未认证外域邮件走收信分支,不调用发信路径;本轮没有公网抓包,不将该检查描述为所有中继场景的完整安全验收。当前未知地址和外域地址均不是在 RCPT 阶段拒绝,管理员兜底行为保持原样。 + +早期脚本误以为未知收件人必定丢弃,实际只读诊断确认当前过滤级别为 0、SPF 状态为通过、DKIM 状态为未通过,且邮件仅属于管理员。修订脚本显式要求当前过滤级别为 0,再验证准确归属,没有为让测试通过而修改业务策略。 + +桌面浏览器无缓存刷新后保持普通用户登录,列表可见中文邮件,详情显示中文 HTML 和“中文验收附件.txt”。附件 12 的下载内容与发送字节一致。管理员专属附件 17 在管理员会话中返回正确内容,在普通用户会话中返回业务错误“附件不可用”,没有附件数据。普通用户访问管理员邮件详情 13 返回“读取邮件失败”,自己的详情 12 正常返回。权限对照使用正常 CSRF 请求头;首次缺少该头的来源校验拒绝不作为用户隔离证据。 + +以上拒绝接口当前 HTTP 状态仍为 200,应依据业务错误码和无数据结果判断,不能仅看 HTTP 状态。详情页面未发现横向溢出。本轮未进行移动端验收,也未重新执行完整清库安装:保留同轮故障邮件和会话,完成备份后升级回归。 + +本轮内部 SMTP 结果及双库备份保留于 `/var/backups/smail-internal-smtp-20260916-212617`,目录内 `result.json` 可复核。本轮通过的是内部接收链路,不代表网页按 MX 路由发信闭环或公网受信任 TLS 已通过。 + ## 备份与保留 以下目录均位于远程服务器,包含敏感运行数据,不提交仓库: @@ -80,13 +109,14 @@ - `/var/backups/smail-before-reset-20260916-202943`:第二轮清库前备份。 - `/var/backups/smail-before-reset-20260916-205033`:前端修复升级前备份,本次未执行清库。 - `/var/backups/smail-before-reset-20260916-210318`:邮件状态及 STARTTLS 修复升级前备份,本次未执行清库。 +- `/var/backups/smail-before-reset-20260916-212132`:中文附件解析修复升级前完整备份,本次未执行清库。 备份均对完整归档进行比较与 SHA 校验。两份数据库 inode 保持为 `smail.db=915735`、`smail-state.db=915727`。旧配置及迁移源已归档,没有删除其他项目文件,没有操作同机 Scert、MySQL 或 PostgreSQL 数据。 最新备份归档 SHA-256: ```text -27a81af2b52946f1988173eecfb290e505ceca50de3634ef3d77013a1f1279dd +9f712cb881db6acdcdefb1c20c5244524107529409196be810f8bb4d4c832c58 ``` 测试用户与失败邮件保留用于后续复核,密码、密钥及会话凭据不写入记录。 diff --git a/test/tools/internal_smtp_check.py b/test/tools/internal_smtp_check.py index b5e569f..fc7749d 100644 --- a/test/tools/internal_smtp_check.py +++ b/test/tools/internal_smtp_check.py @@ -1,5 +1,6 @@ """仅在指定远程部署机执行内部 SMTP 收信回归,不修改业务配置。""" +import base64 import hashlib import json import smtplib @@ -74,14 +75,17 @@ def main(): (backup / name).chmod(0o600) with sqlite3.connect(f'file:{ROOT / "data/smail.db"}?mode=ro', uri=True) as database: database.row_factory = sqlite3.Row + policy = json.loads((ROOT / 'config/email.json').read_text(encoding='utf-8')).get('policy', {}) + assert policy.get('spamFilterLevel', 0) == 0, 'This fixture requires the current unfiltered policy' users = dict(database.execute("SELECT account,id FROM user WHERE account IN ('admin','remotecheck')")) assert set(users) == {'admin', 'remotecheck'} results = [] cases = [ ('text', ['remotecheck@solit.cn'], False, {users['remotecheck']}), ('multipart', ['remotecheck@solit.cn', 'admin@solit.cn'], True, set(users.values())), - ('unknown', [f'missing-{RUN}@solit.cn'], False, set()), - ('relay', ['nobody@external-check.invalid'], False, set()), + ('admin_only', ['admin@solit.cn'], True, {users['admin']}), + ('unknown', [f'missing-{RUN}@solit.cn'], False, {users['admin']}), + ('relay', ['nobody@external-check.invalid'], False, {users['admin']}), ] for label, recipients, multipart, expected_users in cases: result = deliver(label, recipients, multipart) @@ -98,13 +102,16 @@ def main(): attachments = json.loads(email['attachments']) assert len(attachments) == 1 assert attachments[0]['Filename'] == '中文验收附件.txt', repr(attachments[0]['Filename']) + assert base64.b64decode(attachments[0]['Content']) == ATTACHMENT result.update(email_id=email['id'], owners=sorted(expected_users), mailbox_status=[row['status'] for row in relations], stored=True) + if label in ('unknown', 'relay'): + result['outcome'] = 'administrator_fallback_no_outgoing_record' else: assert not emails, f'{label}: unexpected stored message' result.update(stored=False, outcome='accepted_then_discarded') results.append(result) - summary = {'run': RUN, 'backup': str(backup), 'target': '127.0.0.1:25', + summary = {'run': RUN, 'backup': str(backup), 'target': '127.0.0.1:25', 'spam_filter_level': 0, 'certificate_sha256': hashlib.sha256((ROOT / 'ssl/custom.crt').read_bytes()).hexdigest(), 'results': results} (backup / 'result.json').write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding='utf-8')