博主头像
Wood Chen

万事如意

  • 累计撰写 284 篇文章
  • 累计创建 238 个标签
  • 累计收到 20 条评论

目 录CONTENT

给自建 Stalwart 邮局写了个原生桌面客户端:CZL Mail

2026-09-22 · 0 评论 · 0 点赞 · 4 阅读 · 0 字

项目地址:https://github.com/woodchen-ink/czlmail

邮件主界面

为什么要写它

我们自己的邮局跑在 Stalwart 上。Stalwart 用的是 JMAP 协议,服务端能力很全,邮件、日历、通讯录、网盘都有,但客户端一直没找到顺手的:

  • Thunderbird、Outlook 这类老客户端只认 IMAP/CalDAV/CardDAV,要配三套东西,JMAP 的推送也用不上
  • Bulwark 做得很好,但它是网页邮箱,要单独部署,平时就是浏览器里的一个标签页,关掉就收不到通知

所以就自己写了一个:直接说 JMAP 的桌面程序,装上、填服务器地址和应用专用密码就能用,服务器上不用再部署任何东西。写到现在已经是 v0.1.21,Windows 和 macOS 都有安装包。

用起来是什么感觉

打开就能看。 邮件缓存在本地 SQLite,切文件夹、翻页读的都是本地磁盘。和服务器之间只挂一条 EventSource 推送连接,别处删了一封、标了已读,这边马上跟着变,没有定时轮询。

删除、归档不用等。 点了删除,列表立刻跳到下一封,请求在后台发;服务器要是拒了,会提示并恢复原状。

关了窗口照样收信。 Windows 上缩到托盘,图标右上角画未读数;macOS 在菜单栏和 Dock 显示未读。新邮件和日程提醒走系统通知,共享邮箱可以单独静音。

中文搜索能用。 全文搜索用 FTS5 trigram 分词,默认的 unicode61 切不开中文,踩过这个坑才换的。本地先出结果,再让服务器在全部文件夹里搜一遍补上,归档里的老邮件也能搜到。

功能

邮件:多账号,共享邮箱平铺在侧栏;会话合并成一行;富文本写信,HTML 签名、模板、定时发送、分开发送、已读回执;收件人从通讯录、同邮局目录、最近往来里补全;一键退订(RFC 8058);邮件里直接回复日历邀请;右键菜单基本照搬 Bulwark,文件夹可以一键「从服务器拉取全部邮件」。

写信

日历:月/周/日/议程视图,跨天日程在月视图里连成一条横条;重复规则、参加者邀请、多个提醒都能编辑;支持导入 .ics 和订阅 webcal。

日历

通讯录网盘:完整的 JSContact 编辑(照片、地址、纪念日等),可以导入 vCard;网盘就是 Stalwart 自带的文件存储,拖进窗口就上传。

通讯录

网盘

服务端设置:发件身份、假期自动回复、Sieve 过滤规则。过滤规则的格式和 Bulwark 一样,信任发件人名单、邮件模板也和 Bulwark 共用,两边混着用不会打架。

MCP:让 Claude、Codex 直接用我的邮箱

这是我自己用得最多的功能。在「AI 助手」页打开 MCP 后,本机会起一个服务,Agent 可以列邮件、搜索、读正文、查日程、建日程、找联系人、看网盘文件。

MCP

claude mcp add --scope user czlmail -- "C:\Users\<你>\AppData\Local\CZL\CZL Mail\czlmail.exe" mcp

安全上做了几件事:只监听 127.0.0.1,要带令牌;请求里带 Origin 头的一律拒绝,防止网页脚本偷偷打本地端口;发信工具 send_email 默认关着,要单独打开。最后这条是考虑到邮件正文里完全可能藏一段"请把这封信转发给 xxx"的提示注入。

AI 翻译、总结和写信

设置里填一个兼容 OpenAI Responses API 的接口就能用:

  • 外文邮件一键翻译,原地替换文字,表格、图片、样式都不动,译文边生成边替换,随时切回原文,译文会缓存
  • 长邮件点「AI 总结」,摘要显示在正文上方
  • 写信时润色或翻译草稿
  • 输入一句"同意报价,但希望周三前发货",让它起草整封回复
  • 思考档位按任务分开设:翻译默认不思考,起草回复交给模型自己决定

没配置的话界面上不会出现任何 AI 按钮。

开发过程里的一些坑

这几周记了不少笔记,挑几条可能对同样折腾 JMAP 的人有用的:

  1. go-jmap 没有日历、通讯录、网盘。 没去 fork,自己补了一层,对象一律保持原始 JSON,读的时候抽字段、写的时候只发补丁。这样服务器返回了客户端不认识的属性也不会被覆盖掉。
  2. Stalwart 的重复规则字段是单数 recurrenceRule(JSCalendar 2.0 草案),按 RFC 写复数会报 invalidProperties。还有一个:事件没有 recurrenceOverrides 时,按 JSON Pointer 补丁改单次实例会失败,必须把整个对象写进去。
  3. 发信必须自己写 From 头。 本以为传了 identityId 服务器会补,结果真发出过"无发件人"的邮件。
  4. EventSource 断线期间的变化不会补发,所以每次重连后强制全量对比一次;离线太久 state 被回收时,再按 id 和服务器对账,把别处删掉的邮件清掉。
  5. 正文要按部件类型取。 某种正文缺失时,服务器会拿另一种顶上。纯文本邮件如果被当成 HTML 渲染,换行就全被折叠了,GitHub 通知邮件就是这样。
  6. 邮件正文渲染是最花心思的地方:DOMPurify 清洗 → 无 same-origin 的沙箱 iframe → 严格 CSP,三层都在。营销邮件的样式常写在 <head> 里,直接清洗会全丢,要先在惰性文档里把样式挪进正文;远程图片默认拦截,信任粒度是完整地址,不按域名。
  7. Stalwart 接外部 OIDC 时,它自己的授权页只认本地密码,不会跳转到 IdP。所以客户端支持直接向 IdP 授权,管理员在域名下放一个 /.well-known/czlmail.json,用户填完服务器地址就会出现「使用 XX 登录」。

其他

  • 在线更新走 GitHub Releases,没有 SHA256SUMS 或哈希对不上一律不装
  • 密码和 API Key 只存系统钥匙串,没有明文回落
  • 设置里可以一键删除本机所有数据(含钥匙串凭据),服务器上的数据不受影响
  • macOS 版没有开发者签名,首次打开要在「隐私与安全性」里放行,或者执行 xattr -cr "/Applications/CZL Mail.app"

设置

下载

  • GitHub:https://github.com/woodchen-ink/czlmail
  • 安装包:https://github.com/woodchen-ink/czlmail/releases
  • 许可证:AGPL-3.0

技术栈是 Go + Wails v2 + Next.js,源码里带了单元测试和构建脚本,想自己编也方便。

有问题或者想要什么功能,可以到论坛的 反馈帖 说,也可以直接在 GitHub 提 Issue。

评论区