Skip to content

Feature Message body cache zh

SkimMail docs edited this page Sep 15, 2026 · 5 revisions

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_MBBODY_TTL_DAYS 属于 Plane 2 设置:environment 提 供的是初始默认值,而你在界面上保存的值——或通过 PUT /api/settings/cache 保存的值——写进数据库,从此以后由它说了算。从 1.11.1 起,那个界面就是 Settings ▸ Security ▸ 邮件正文缓存。在 1.11.0 里,同一个面板被放在隐藏的 Storage 标签页中,所以那时 environment 确实就是全部。
  • BLOB_ENCRYPTboot-only 的。没有 UI、没有 API、也没有 skimmail config;它只在启动时从 environment 读取,别无他处。它除非你明确关闭,否则 始终开启:只有 false0nooff 会关闭它(不区分大小写,忽略首尾 空格)。其它任何值——1TRUEyeson,或者一个拼写错误——都让加密保持 开启,因为一个默认开启的标志必须朝安全方向失败。

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,读取也不例外——所以对 operatorviewer 来说,面板根本不会出现,而不是出现之后再拒绝。这是有意为之:一个你按 了只会被拒绝的控件,比没有这个控件更糟。

静态加密

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 后首次启动的一次性清理

升级到 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.15.0 修复

直到 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

Clone this wiki locally