-
Notifications
You must be signed in to change notification settings - Fork 0
Feature Message body cache zh
English · Tiếng Việt · 中文
你第一次打开一封邮件时,SkimMail 会把它的完整正文留在磁盘上,这样第二次打开就 不必等 IMAP。自 1.11.0 起,这份缓存默认静态加密,有了容量上限和 保留期限,并且在它所属的邮件被删除时终于会被一起删掉。在 1.11.0 里,你 用三个环境变量配置它——对应的 Settings 面板已经做好,但这一版里不可见。
- 一封邮件解析后的正文(HTML 与纯文本),在该邮件第一次被打开时写入,以及 由下文所说的后台预取写入。
- 它存放在 blob store 里:默认是
DATA_DIR下的一个目录,如果你切换过存储 引擎,则是你的 S3 bucket。 - 主题、发件人、日期、flag、摘要片段和搜索索引都在数据库里,不在这里,也不 受本页任何内容影响。
- 附件不缓存。 每次打开附件都会重新从邮件服务器取。
缓存之所以存在,是因为一次未命中的代价,是任何存储后端都修不好的:那是一次同步 的 IMAP 往返,在一个 HTTP 请求里把整封邮件拉回来,大约是从磁盘读回正文的一千 倍。
缓存始终开启。由三个环境变量控制:
| 变量 | 作用 | 默认值 |
|---|---|---|
CACHE_MAX_SIZE_MB |
缓存的邮件正文总量超过此值时开始清理;0 = 不限制 |
2048 |
BODY_TTL_DAYS |
删除缓存超过此天数的邮件正文;0 = 永不过期 |
90 |
BLOB_ENCRYPT |
对新写入的邮件正文做静态加密 | true |
前两个和第三个的行为不同,而这个区别很关键:
-
CACHE_MAX_SIZE_MB和BODY_TTL_DAYS属于 Plane 2 设置:environment 提 供的是初始默认值,而你在界面上保存的值——或通过PUT /api/settings/cache保存的值——写进数据库,从此以后由它说了算。从 1.11.1 起,那个界面就是 Settings ▸ Security ▸ 邮件正文缓存。在 1.11.0 里,同一个面板被放在隐藏的 Storage 标签页中,所以那时 environment 确实就是全部。 -
BLOB_ENCRYPT是 boot-only 的。没有 UI、没有 API、也没有skimmail config;它只在启动时从 environment 读取,别无他处。它除非你明确关闭,否则 始终开启:只有false、0、no和off会关闭它(不区分大小写,忽略首尾 空格)。其它任何值——1、TRUE、yes、on,或者一个拼写错误——都让加密保持 开启,因为一个默认开启的标志必须朝安全方向失败。
1.11.0 把最后这条规则搞反了。 那个版本把值与
true这个词逐字比较,于是BLOB_ENCRYPT=1、=yes、=on和=TRUE全部被读成关闭——加密恰恰停在操 作者相信自己刚刚确认开启的那一刻。1.11.1 已修复。在关闭期间缓存下来的正文 不会被就地重写;它们会一直保持未加密,直到离开缓存为止。在 Settings ▸ Security 里调低容量上限或保留天数即可把它们清理掉,再回来时就是加密的了。
关于 "Plane 2" 的含义,以及这三个变量在完整环境变量对照表中的位置,见 配置。
Settings ▸ Security ▸ 邮件正文缓存,自 1.11.1 起提供。它显示四项内容, 并提供两个操作:
| 行 | 含义 |
|---|---|
| 已缓存正文 | 存了多少份,以及合计多少字节 |
| 等待删除 | 已被判定要删、但尚未从存储中移除的正文;有才显示 |
| 后台预取 | 开/关,实例级。自 1.16.0 起 —— 见下文"新邮件的预取" |
| 容量上限(MB) |
0 = 不限制。超出上限时,最久未读的先被清除 |
| 保留天数 |
0 = 永不过期 |
保存会立即应用新的限制,所以它一返回,用量数字就会变动。清理无归属对象 就是下文所说的 S3 清理。
这个面板背后的每一条路由都仅限 owner,读取也不例外——所以对 operator 或
viewer 来说,面板根本不会出现,而不是出现之后再拒绝。这是有意为之:一个你按
了只会被拒绝的控件,比没有这个控件更糟。
在 BLOB_ENCRYPT=true(默认)下,每一份新写入的正文都用 AES-256-GCM 封装。
密钥由你的主密钥派生,因此:
- 没有任何新东西需要备份、丢失或轮换。 不存在第二个秘密。丢掉主密钥本来就 会让数据库无法读取,所以这并没有增加任何你原本没有的丢数据方式。
- 升级不需要迁移,也没有停机。 每个 blob 自己说明它是否被封装过,所以一个 store 里可以同时放着 1.10.0 写的正文和 1.11.0 写的正文。读取两者都能处理。
- 关掉加密不会让已有缓存作废。 读取始终解密;这个开关只决定新正文怎么 写。
- 万一密钥不对,一份正文会被当作缓存未命中,重新从邮件服务器取回。产品退化 成"慢",而绝不会退化成"丢信"。
它不覆盖的是:数据库。主题、地址、摘要片段和搜索索引在磁盘上都是明文。那些 东西请对卷加密——见安全。
备份文件从不携带这个密钥。正文以解密后的形式进入归档(归档自身另有加密),在恢 复时用目标实例自己的密钥重新封装,因此一个归档在从未见过本实例主密钥的机器上依 然可以恢复。
- 当缓存超过
CACHE_MAX_SIZE_MB时,最久没被读过的正文先走,清理到上限的 90% 而不是恰好等于上限——缓存不该一辈子都停在"再来一封就满"的状态。 -
BODY_TTL_DAYS从正文被缓存的时刻算起,而不是从最后一次读取算起:天天被 读的正文和没人打开过的正文一样陈旧。 - 保留期限不只是打扫卫生。重置了 UIDVALIDITY 的邮件服务器可能把同一个 UID 分给 另一封邮件,而缓存是按账户、mailbox 和 UID 作键的——所以没有保留期限的话,一 份陈旧正文可能被永远显示在一封新邮件之下。TTL 把这件事限制在 N 天之内。
- 没有任何后台定时器。 大约每积累 64 MiB 新正文就跑一次清理,通过 API 保存 新限制时则立刻跑一次。删除是分批的(每次 500 个 blob,外加每轮 sync poll 50 个),因此一次清理不会卡住任何人的请求;剩下的由下一次接着做。
- 被清理掉只让你多一次重新获取,别无代价。缓存从不持有你邮件的唯一副本。
在 1.11.0 之前,删除一个账户会把它的邮件从数据库里清掉,却把每一份缓存正文 永远留在磁盘上——看不见、找不回,而且仍然计入 Community 的存储上限。从 1.11.0 起,这些字节随账户一起被清除。把一封邮件移到另一个 mailbox 也会让旧正文 退役,因为它在目的地拿到的是新的 UID。
使用文件系统 blob store 时,缓存正文位于 DATA_DIR 之下,因此会计入你所在档位
的存储上限。使用 S3 引擎时,它们在 bucket 里,不计入。
升级到 1.11.0 的实例,store 里满是正文,却没有关于它们的任何账本。首次启动时, SkimMail 从你的邮件推导出本应存在的那些 blob,把它们记上账,其余的一律视为无 主。它只跑一次,在后台跑,因此绝不会拖慢监听端口,并写下日志:
reconciled body cache store=fs indexed=1843 bytes=284127744 extra=12 deleted=12 needs_purge=false
两种存储引擎被刻意区别对待:
- 文件系统 —— blob 目录只属于 SkimMail,所以无主的 blob 确实就是孤儿。它们 会被删除。
-
S3 —— 这个 bucket 可能同时是你的备份目的地。无主对象只被计数并原样
留下;日志里写
needs_purge=true。什么都不会删,因为在那里删错一个对象, 可能毁掉你邮件的唯一副本。
在 S3 上这个缺口还会自行合拢:一份正文会在下一次被读取时记上账,所以一个正常使 用的实例不必碰 bucket 也会自行收敛。
如果你确定这个 bucket 里除了 SkimMail 的缓存别无他物,有一个手动 purge: Settings ▸ Security ▸ 邮件正文缓存 ▸ 清理无归属对象。它会告诉你删除了多少 个已存对象。
同一件事的 API 形式,供脚本使用:
curl -sS -b /tmp/skim.cookies -X POST http://localhost:8080/api/settings/cache/purge两者都仅限 owner。获取 session cookie 的方法见
邮件中的远程图片。
一次 sync 带回新邮件之后,SkimMail 会在后台预热最多 10 封最新邮件的正文, 让第一次点击是即时的,而不是在等 IMAP。
有三条"拒绝"比功能本身更重要:
- 只在有人连着的时候做。 没有浏览器打开,就没有哪一次"第一次打开"需要变快, 而在按流量计费的线路上,这笔交换只是账单。
- 对已被自动停止同步的账户绝不做。 一个正在挣扎的邮箱最不需要的就是投机性 的工作。
- 绝不计为一次同步失败。 失败的预取被静默丢弃,因为让可选的后台工作去推动 连续失败计数器,可能把一个完全健康的邮箱自动停掉。
当缓存已经到达上限时它也会停——往满了的缓存里塞东西,等于赶走某个人刚读过的内 容,去装没人要的东西。两次获取之间间隔 250 ms,免得在服务商那边看起来像一阵突 发流量。
自 1.16.0 起,多了一个开关:Settings ▸ Security ▸ 邮件正文缓存,就在容量
上限和保留期限旁边——因为这本来就是同一个问题:你有多少邮件最终落在这块磁盘上,
又是怎么落上去的。它默认开启,并且在升级之后也保持开启:值得关掉它的那个理由
(下文说的那个 \Seen 泄漏)已经修复了,而在用户不知情的情况下悄悄改动一个性能默
认值,本身就是另一种意外。关掉之后,后台预取完全不做任何工作——不计算目标、不测
量缓存、不打开任何连接——但打开一封邮件仍然会按需取正文,和任何其它一次缓存未命
中一样。这个开关和这个面板的其它部分一样,仅限 owner。
直到 1.14.0 为止,SkimMail 取回的每一封邮件都是用
FETCH BODY[...]请求的, 而这种写法会在邮件服务器上隐式设置\Seen标记(RFC 3501 §6.4.5)。BODY.PEEK[...]是同样的取回但没有这个副作用,而 SkimMail 里没有一处用它。由于预取是在后台对最新邮件运行的,没有人打开过的邮件就在真实服务器上被标成了 已读——在 webmail 里和手机上都看得到,不只是这里。随后的同步又把这个状态读了 回来,所以 SkimMail 内部始终自洽,看上去哪里都没问题。下载附件和一键退订也是 同样的情况。
请升级到 1.15.0。 三条取回路径现在都会 PEEK。
已经被这样标成已读的邮件无法修复。 它和你真正读过的邮件无法区分,所以也没 有什么可供迁移撤销的。这个修复只能从此刻起生效,够不到过去。
- 它不是归档。 被清理掉的正文会重新从邮件服务器取;如果邮件在服务器上已经 没有了,那就是没有了。
- 它没有按账户或按用户的限制。 容量上限和保留期限都是实例级的。
- 它不缓存附件。
- 它不会告诉你哪些邮件被缓存了。 面板报告条目数和总大小,purge 作用于无归 属的对象;没有按邮件的列表,也没有办法把某一份正文钉住。
- 配置 —— 环境变量对照表与三个配置平面
-
安全 —— 静态加密,以及
DATA_DIR被偷会暴露什么 - 故障排查 —— 升级之后磁盘占用没有下降
-
用户与角色 —— 为什么缓存相关的 endpoint 仅限
owner
SkimMail · skimmail@base101.app · 2026-09-15 · commit 767741a