| Files | Last commit | Last update |
|---|---|---|
feat(mail): 邮件列表显示入站安全校验徽标 列表每封邮件的主题行首加一个圆形徽标,四档: 已验证 ✓ / 存疑 ! / 未通过 ✕ / 未验证 ? 悬停用原生 tooltip 显示原因,如「SPF 通过 · DKIM 通过 · DMARC 未通过」。 结论来自收件服务器写在 Authentication-Results 头里的判断(微软、谷歌在收信 那一刻做的),本程序自己不做验证——SPF 的输入是发信 IP,客户端拿不到这个 信息,只有接收方能在 SMTP 会话里看到。 为什么必须先解析:原始头最长 2000 字符,50 封就是 100KB,不能跟着列表一起 返回。所以同步时就解析成 AuthVerdict(枚举)+ AuthSummary(中文摘要), 列表只返回这两个小字段。 取值规则(AuthResultsParser): - 只认 spf / dkim / dmarc。compauth= 等各家私有字段没有公开语义,跳过—— 微软那类代发邮件 compauth=fail 但三项全过,照样是已验证 - 一封信常有多个 DKIM 签名,任意一个通过即算通过,全部 fail 才算失败 - 明确 fail 优先;其次 DMARC 通过(它要求 SPF 或 DKIM 至少一项对齐); 再次 softfail / neutral / policy 归存疑;temperror / permerror 归未验证 - 拿不准一律归未验证。宁可少给结论,也不给错误的安全感 SPF 软失败但 DKIM+DMARC 通过时算已验证:这是代发的正常形态,发送平台用 自己的 IP 发信,SPF 本就对不上,业界也照常投递。软失败这一细节仍留在摘要 里,悬停可见。没有 DMARC 兜底时才算存疑。 历史邮件必须补算:同步只处理新邮件,不补算的话升级前那些邮件的徽标会永远 停在「未验证」,看上去像功能没生效。启动时在后台分批补算,筛选条件是 AuthSummary 为空,补完自然收敛,重启也能接着上次的进度继续。 生成迁移时踩的坑,记在这里:ef 工具不读 appsettings 的 Database:Provider, 默认按 Sqlite 生成。第一次产出 126 个 AlterColumn,把 varchar(64) 改成 TEXT、tinyint(1) 改成 INTEGER,应用到生产 MySQL 库就是灾难。最后用一个 独立的临时启动项目给 ef 提供钉住 MySql 的工厂才得到正确结果(只有 2 个 AddColumn)。以后再生成迁移要沿用这个办法。 验证: - 解析器按真实格式跑 16 组:微软(两重 DKIM)、谷歌、SPF+DMARC 失败、 DKIM 签名验证失败、软失败(有/无 DMARC)、各项均为 none、头不存在、 空串、无法识别的内容、compauth=fail、两重 DKIM 一过一败、只有一项通过、 临时/永久错误,全部相符 - 端到端:起实库,插入 4 封「结论字段留空」的邮件模拟升级前的历史数据, 启动后由后台补算,列表接口返回的三档结论与摘要全部正确 - app.js 过 node --check,编译无错。浏览器里的徽标观感未验证 | 23 days ago | |
feat(mail): 补齐正文渲染——内嵌图片、<style> 样式与远程图片开关 用户反馈正文缺图片少样式。排查下来消毒这块砍得比该砍的多: - 远程图片 src 一律降级成 data-src 等确认(防追踪像素,这层保留), 但「显示远程图片」按钮从未被渲染出来,等于是条死路径——现在有图 待确认时给按钮,点开才加载,文案写明代价; - cid: 内嵌图片整个被剥掉。新增 GET /api/messages/{id}/inline/{cid}, 原始 .eml 就在磁盘上,现场按 ContentId 找出那一份按 inline 出图, 不必为 ContentId 改表;cid/data 都不出网,无需等确认直接显示; - <style> 块被白名单剥掉,商业邮件排版几乎全在里面。放开标签本身, 块内 CSS 仍走 HtmlSanitizer 的 CSS 白名单。 放开 <style> 的前提是把正文装进 sandbox iframe(无 allow-scripts,消毒之外 的双保险;allow-same-origin 仅为读内容高度撑起 iframe):否则一封带 body{display:none} 的邮件就能把整个界面抹掉。 | 3 days ago | |
fix(data-dir): 数据目录默认到程序二进制同级的 data/ 容器 / 镜像里 HOME 不可靠:非 root 镜像下 /root 不存在或不可写, HOME 也未必是 /root,结果 Program.cs 里 PersistKeysToFileSystem 和 SQLite 默认路径(~/.mailclient/)直接创建失败或落到不可写位置。 改成默认与可执行文件同级 data/——容器里这就是工作目录, 权限由部署者自己控制(通常就是 image 的 WORKDIR)。 - 默认值:Path.GetDirectoryName(Environment.ProcessPath) / "data" - 用 Environment.ProcessPath 而非 AppContext.BaseDirectory: 单文件发布下后者指向临时解包目录,写入会丢 - 仍支持 MMAIL_DATA_DIR 环境变量整体覆盖(开发用 dotnet run 时 也建议设这个,因为此时 ProcessPath 是 dotnet 主机的路径) - appsettings.json 里的 ConnectionStrings.Default / Storage.Root 一并删除:用代码默认即可,部署者无路径可填 - PathHelper.Expand / Home 字段同步删除:与新逻辑无关 实测:publish/data/ 下正确生成 keys/ + mmail.db + WAL, /root/.mailclient 不再被创建。 | 25 days ago | |
feat(mail): 邮件列表显示入站安全校验徽标 列表每封邮件的主题行首加一个圆形徽标,四档: 已验证 ✓ / 存疑 ! / 未通过 ✕ / 未验证 ? 悬停用原生 tooltip 显示原因,如「SPF 通过 · DKIM 通过 · DMARC 未通过」。 结论来自收件服务器写在 Authentication-Results 头里的判断(微软、谷歌在收信 那一刻做的),本程序自己不做验证——SPF 的输入是发信 IP,客户端拿不到这个 信息,只有接收方能在 SMTP 会话里看到。 为什么必须先解析:原始头最长 2000 字符,50 封就是 100KB,不能跟着列表一起 返回。所以同步时就解析成 AuthVerdict(枚举)+ AuthSummary(中文摘要), 列表只返回这两个小字段。 取值规则(AuthResultsParser): - 只认 spf / dkim / dmarc。compauth= 等各家私有字段没有公开语义,跳过—— 微软那类代发邮件 compauth=fail 但三项全过,照样是已验证 - 一封信常有多个 DKIM 签名,任意一个通过即算通过,全部 fail 才算失败 - 明确 fail 优先;其次 DMARC 通过(它要求 SPF 或 DKIM 至少一项对齐); 再次 softfail / neutral / policy 归存疑;temperror / permerror 归未验证 - 拿不准一律归未验证。宁可少给结论,也不给错误的安全感 SPF 软失败但 DKIM+DMARC 通过时算已验证:这是代发的正常形态,发送平台用 自己的 IP 发信,SPF 本就对不上,业界也照常投递。软失败这一细节仍留在摘要 里,悬停可见。没有 DMARC 兜底时才算存疑。 历史邮件必须补算:同步只处理新邮件,不补算的话升级前那些邮件的徽标会永远 停在「未验证」,看上去像功能没生效。启动时在后台分批补算,筛选条件是 AuthSummary 为空,补完自然收敛,重启也能接着上次的进度继续。 生成迁移时踩的坑,记在这里:ef 工具不读 appsettings 的 Database:Provider, 默认按 Sqlite 生成。第一次产出 126 个 AlterColumn,把 varchar(64) 改成 TEXT、tinyint(1) 改成 INTEGER,应用到生产 MySQL 库就是灾难。最后用一个 独立的临时启动项目给 ef 提供钉住 MySql 的工厂才得到正确结果(只有 2 个 AddColumn)。以后再生成迁移要沿用这个办法。 验证: - 解析器按真实格式跑 16 组:微软(两重 DKIM)、谷歌、SPF+DMARC 失败、 DKIM 签名验证失败、软失败(有/无 DMARC)、各项均为 none、头不存在、 空串、无法识别的内容、compauth=fail、两重 DKIM 一过一败、只有一项通过、 临时/永久错误,全部相符 - 端到端:起实库,插入 4 封「结论字段留空」的邮件模拟升级前的历史数据, 启动后由后台补算,列表接口返回的三档结论与摘要全部正确 - app.js 过 node --check,编译无错。浏览器里的徽标观感未验证 | 23 days ago | |
feat(mail): 邮件列表显示入站安全校验徽标 列表每封邮件的主题行首加一个圆形徽标,四档: 已验证 ✓ / 存疑 ! / 未通过 ✕ / 未验证 ? 悬停用原生 tooltip 显示原因,如「SPF 通过 · DKIM 通过 · DMARC 未通过」。 结论来自收件服务器写在 Authentication-Results 头里的判断(微软、谷歌在收信 那一刻做的),本程序自己不做验证——SPF 的输入是发信 IP,客户端拿不到这个 信息,只有接收方能在 SMTP 会话里看到。 为什么必须先解析:原始头最长 2000 字符,50 封就是 100KB,不能跟着列表一起 返回。所以同步时就解析成 AuthVerdict(枚举)+ AuthSummary(中文摘要), 列表只返回这两个小字段。 取值规则(AuthResultsParser): - 只认 spf / dkim / dmarc。compauth= 等各家私有字段没有公开语义,跳过—— 微软那类代发邮件 compauth=fail 但三项全过,照样是已验证 - 一封信常有多个 DKIM 签名,任意一个通过即算通过,全部 fail 才算失败 - 明确 fail 优先;其次 DMARC 通过(它要求 SPF 或 DKIM 至少一项对齐); 再次 softfail / neutral / policy 归存疑;temperror / permerror 归未验证 - 拿不准一律归未验证。宁可少给结论,也不给错误的安全感 SPF 软失败但 DKIM+DMARC 通过时算已验证:这是代发的正常形态,发送平台用 自己的 IP 发信,SPF 本就对不上,业界也照常投递。软失败这一细节仍留在摘要 里,悬停可见。没有 DMARC 兜底时才算存疑。 历史邮件必须补算:同步只处理新邮件,不补算的话升级前那些邮件的徽标会永远 停在「未验证」,看上去像功能没生效。启动时在后台分批补算,筛选条件是 AuthSummary 为空,补完自然收敛,重启也能接着上次的进度继续。 生成迁移时踩的坑,记在这里:ef 工具不读 appsettings 的 Database:Provider, 默认按 Sqlite 生成。第一次产出 126 个 AlterColumn,把 varchar(64) 改成 TEXT、tinyint(1) 改成 INTEGER,应用到生产 MySQL 库就是灾难。最后用一个 独立的临时启动项目给 ef 提供钉住 MySql 的工厂才得到正确结果(只有 2 个 AddColumn)。以后再生成迁移要沿用这个办法。 验证: - 解析器按真实格式跑 16 组:微软(两重 DKIM)、谷歌、SPF+DMARC 失败、 DKIM 签名验证失败、软失败(有/无 DMARC)、各项均为 none、头不存在、 空串、无法识别的内容、compauth=fail、两重 DKIM 一过一败、只有一项通过、 临时/永久错误,全部相符 - 端到端:起实库,插入 4 封「结论字段留空」的邮件模拟升级前的历史数据, 启动后由后台补算,列表接口返回的三档结论与摘要全部正确 - app.js 过 node --check,编译无错。浏览器里的徽标观感未验证 | 23 days ago | |
feat(mail): 补齐正文渲染——内嵌图片、<style> 样式与远程图片开关 用户反馈正文缺图片少样式。排查下来消毒这块砍得比该砍的多: - 远程图片 src 一律降级成 data-src 等确认(防追踪像素,这层保留), 但「显示远程图片」按钮从未被渲染出来,等于是条死路径——现在有图 待确认时给按钮,点开才加载,文案写明代价; - cid: 内嵌图片整个被剥掉。新增 GET /api/messages/{id}/inline/{cid}, 原始 .eml 就在磁盘上,现场按 ContentId 找出那一份按 inline 出图, 不必为 ContentId 改表;cid/data 都不出网,无需等确认直接显示; - <style> 块被白名单剥掉,商业邮件排版几乎全在里面。放开标签本身, 块内 CSS 仍走 HtmlSanitizer 的 CSS 白名单。 放开 <style> 的前提是把正文装进 sandbox iframe(无 allow-scripts,消毒之外 的双保险;allow-same-origin 仅为读内容高度撑起 iframe):否则一封带 body{display:none} 的邮件就能把整个界面抹掉。 | 3 days ago | |
style(ui): 「显示远程图片」改回有边框的常规按钮 误用了 link-button:那套样式是给「改用应用专用密码」这类无副作用的小操作 准备的,剥掉边框和底色后,一个要向外部发请求的动作看起来像随手可点的链接。 摘掉类名回落到全局 button 样式(自带边框、底色与悬停色),只补与正文之间的间距。 | 3 days ago | |
fix(publish): 单文件发布嵌入 wwwroot 的 glob 改成 /**/* 本机 .NET 10 SDK 下,原来的 <EmbeddedResource Include="wwwroot/**" /> 只能匹配到目录、不会递归到子文件——GenerateEmbeddedFilesManifest 拿不到任何源,运行时 Program.cs 的 ManifestEmbeddedFileProvider 直接抛 "Could not load the embedded file manifest ...Manifest.xml" 启动崩溃。 改成 /**/* 显式递归到文件即可,v0.2.1 release 二进制能跑说明旧 SDK 行为不同,迁到当前 SDK 后必须改。 | 25 days ago | |
fix(ui): 磁盘占用不再停在「统计中」,并给静态资源加 no-cache 上一版页面只补取一次就放弃:首次统计还没跑完时页面停在「统计中」,等统计真的 写进库了,页面也不会自己回来——看着和坏了没区别(库里明明已经有值)。 改成在设置页上每 3 秒问一次、最多两分钟,离开设置页即停;仍拿不到就明说 「稍后刷新本页」,而不是一直装作在统计。 顺带把静态资源的 Cache-Control 设成 no-cache:app.js 没有内容指纹,浏览器 的启发式缓存可能让它一直用着上一版——换个二进制重启之后界面还是老样子, 这类问题排查起来极费劲。带 ETag 时只是一次 304,代价可以忽略。 已用临时 SQLite 实例实测:后台统计 5 秒内完成并写库, GET /api/system/storage 返回 camelCase 的 mailBytes/otherBytes/totalBytes/measuredAt, 静态资源响应带 Cache-Control: no-cache 与 ETag。 | 16 days ago | |
feat(db): 默认数据库切到 MySQL,重建 Initial 迁移 仓库同时支持 Sqlite/MySql/PostgreSql,生产确定用 MySQL。 迁移是「生成时绑定 Provider」的:原来那套由 SQLite 生成,列类型写死 INTEGER/TEXT,且带 Sqlite:Autoincrement 注解。直接拿去跑 MySQL,INTEGER 会落成 INT(4 字节有符号,上限 21 亿),而 Messages.Uid / Folders.LastUid 是 long——IMAP 的 UID 是 32 位无符号,攒到 21 亿以上就溢出,而且是几个月 后才暴露、届时很难联想到迁移上的那类问题。重建后这些列是 bigint。 appsettings.json 同时补上 ConnectionStrings:Default。之前根本没有这个 节点,留空会静默回落到 DesignTimeDbContextFactory 的哨兵串 (root/root@localhost:3306),届时不是连错库就是连不上。 本地 mysql:8.0 容器实测(全新空库): - Migrate 成功,8 张表,__EFMigrationsHistory 记录到位 - Uid / LastUid / UidValidity 均为 bigint,长文本为 longtext, 日期为 datetime(6),布尔为 tinyint(1) - Messages 上 3 个复合索引全部建成。迁移里的 HasIndex("MessageId") 属于 Attachments 的外键(int),Messages.MessageId 那列并未建索引, 所以不存在 utf8mb4 下 varchar(998) 撞 3072 字节上限的问题 - 种子写入 + 登录 + 会话读回,端到端通过 | 25 days ago |
| Files | Last commit | Last update |
|---|---|---|
| 23 days ago | ||
| 3 days ago | ||
| 25 days ago | ||
| 23 days ago | ||
| 23 days ago | ||
| 3 days ago | ||
| 3 days ago | ||
| 25 days ago | ||
| 16 days ago | ||
| 25 days ago |