20 KiB
MessageWeave(中文)
English documentation: README.md · 架构与设计见 docs/design.md,部署与运维见 docs/deployment.md
一个把 Stalwart 邮箱接入 Telegram 的个人邮件助手:新邮件来了在 Telegram 提醒你,还能按需查看/总结邮件内容。
⚠️ 当前仍属早期阶段,请先按本文完成小范围联调。 真实邮箱和 Telegram 仍需要由部署者自行验证;JMAP Push 注册、验证回写已接入,但不会自动猜测公网 callback URL,必须由管理员显式调用注册接口。
这个项目是什么
- 你在自己的服务器上运行 Stalwart 邮箱。
- 用本工具把邮箱接到 Telegram 机器人:收到邮件时推送通知(只含发件人/主题/时间,不含正文)。
- 你可以在 Telegram 里要求查看某封邮件的原文、或让 AI 帮忙总结/翻译。
隐私原则
- 查看原文不会经过任何 AI,直接读邮箱。
- 只有在你明确要求并确认后,才把正文发给 AI 分析。
- 分析结果不会被保存。
- AI 可以完全不配置、完全关闭。
部署前,你需要准备这几样东西
- 一个 Stalwart 邮箱账户,并为其生成一个 应用专用密码(App Password),而不是账户主密码。
- 一个 Telegram 机器人(通过 @BotFather 创建),拿到它的 Bot Token。
- 一个 Redis 数据库(你托管,建议开启 AOF 持久化)。机器人所有状态都放在这里。TLS Redis 使用
rediss://,普通 Redis 使用redis://;Upstash 等服务的密码如果包含@、:、/或#,必须先做 URL 编码。 - 一个公网 HTTPS 地址。平台会把这个地址指向机器人容器(HTTPS 由平台自动提供)。
- 一个定时任务(外部 Cron / 调度器):每隔 5–10 分钟访问一次
/reconcile地址,让机器人定期和邮箱对账。机器人自己不会设置定时器。
需要登记的地址
- 告诉 Telegram:把 webhook 指向
你的公网地址/webhook/tg
如果你使用「多实例 + Cloudflare Worker 负载均衡」(可选,见
docs/deployment.md§10),则把上面的 Telegram Webhook 和 Reconcile 地址指向 Worker 的稳定域名(https://<your-worker>.workers.dev/webhook/tg等),不再指向各后端的平台地址。
在 Docker 管理器中创建容器
创建一个新容器,填好镜像、端口、内存等常规项后,只需注入 Redis 连接凭据:
| 环境变量 | 必须? | 填什么 |
|---|---|---|
REDIS_URL |
✔ | Redis 连接串(含密码),例如 redis://:密码@redis.example.com:6379/0 |
CONFIG_ENCRYPTION_KEY |
✔ | 32 字节十六进制密钥;可用 openssl rand -hex 32 生成 |
首次启动后,通过 SPA 的受保护配置页面提交 Telegram/JMAP/LLM 和业务鉴权配置。配置密钥加密写入 Redis,API 永不回显;管理员 PUT 成功后会校验并原子热重建客户端,后续请求即时生效,失败保留旧实例。
AI 摘要默认关闭。Telegram 用户需先发送受支持的授权语,再发送 /summary <email_id>:/ai on(或 /ai yes,1 小时);含 临时 或 一次 的任意消息,如 临时一次(一次性,1 小时);今天(24 小时);7天(7 天);直到我撤销(或 长期,最长 365 天)。这些触发词按原文字匹配,需按上文的中文原文输入,不支持英文别名。撤销:/ai off(或 撤销授权)。授权到期不会自动续期。未授权时仅返回邮件元数据;未配置 AI 或调用失败则回退为本地截断摘要。
缺少
REDIS_URL或CONFIG_ENCRYPTION_KEY时服务进入 configuration-setup 模式,仍提供 SPA、/api/status和探针;SPA 隐藏管理会话授权区,只显示缺少的环境变量与密钥不保存、不回显说明。配置恢复后/api/statusready=true 才显示授权入口。
HostStack 原生部署
HostStack 使用根目录 hoststack.yaml 构建并运行 Rust 服务:执行 cargo build --release --locked,运行 ./target/release/message-weave,并以 /healthz 进行 30 秒间隔、5 秒超时的健康检查。请在 HostStack Secret 中注入 REDIS_URL 与 CONFIG_ENCRYPTION_KEY;密钥不写入 YAML、镜像或仓库文件。
管理会话内的“启用业务处理”开关持久化在 Redis config:enabled;缺失或读取失败均按关闭处理。关闭时 Webhook、Reconcile 和 Worker 返回 HTTP 503,不确认或丢弃上游事件;SPA、状态和管理配置 API 仍可用。
启动后怎么看它是否正常
- 浏览器或命令行访问
你的公网地址/healthz:能返回正常状态,说明机器人进程活着。 /ready返回 JSON 就绪状态并检查配置完整性与 Redis 可访问性;200 表示基础依赖就绪,503 表示不应接收业务流量。它不会调用 JMAP、Telegram 或触发任何业务副作用。- 运行监控建议使用 Uptime Kuma:为
/healthz建立存活 HTTP 检查,为/ready建立就绪 HTTP 检查(期望 200,间隔 30 秒、超时 5 秒)。项目不引入 Prometheus 或 exporter。 /reconcile执行鉴权、Redis 单飞锁与有界 JMAPEmail/changes对账;事件先幂等入 Redis Streams,全部成功后才推进 RedissinceState,依赖暂时失败返回503以便 Cron 重试。Push 事件仍按鉴权、去重、入队路径处理,不能将其宣传为完整实时同步。- 冷启动基线也有页数、邮件数和时间预算;超出预算时保存断点,下一次
/reconcile从断点继续,避免大邮箱反复从头扫描。
配置管理
打开服务根地址 你的公网地址/,输入 REDIS_URL 中 Redis ACL 用户的密码,创建有效期 15 分钟的管理会话。会话与表单中的密钥只保存在当前页面内存;刷新或关闭页面后需要重新授权。页面不使用浏览器存储或 Cookie 保存会话、密钥和配置。
页面分开管理两类配置:运行参数(JMAP、Telegram、LLM 请求超时与最大重试次数)可通过 API 读取和保存;业务配置(JMAP、Telegram、LLM、chat allowlist 与各鉴权 token)由 PUT /api/business-config 完整替换。为避免密钥回显,服务端没有业务配置读取接口,因此每次完整替换都必须重新输入必填项。保存收到 HTTP 204 后页面会提示新配置已热加载,并清空密钥输入框。
Redis 尚未连接或初始化时,管理会话或配置 API 会返回 503;页面会说明不可用状态,并在运行参数读取成功前禁用其保存。经 Cloudflare Worker 访问时使用 Worker 根地址;/api/config、/api/business-config、/api/admin/session、/api/admin/session/revoke 等管理 API 路径已纳入受限路由白名单(POST /api/push/register 不在其中,需按 docs/deployment.md §10.5 直连某个后端实例)。
首次联调完整流程
下面是一条从空部署到 Telegram 收到第一条消息的可重复流程。所有示例中的 <...> 都是占位符,不要把尖括号原样复制;真实密钥不要发到聊天、工单或仓库。
1. 先确认部署和 Redis
访问:
curl -fsS https://<message-weave-domain>/api/status
curl -fsS https://<message-weave-domain>/healthz
当 REDIS_URL 与 CONFIG_ENCRYPTION_KEY 正确时,/api/status 应包含:
{"ready":true,"mode":"configured","missing":[]}
如果缺少变量,容器不会退出,而是进入 configuration-setup;SPA 只显示缺少的变量,不会显示管理会话授权区。Redis URL 必须包含正确协议:rediss:// 表示 TLS,redis:// 表示明文连接。TLS Redis 不能只在 redis-cli 命令中加 --tls,应用本身的 REDIS_URL 也必须使用 rediss://。
2. 创建 Telegram Bot 和测试群
在 Telegram 中打开 @BotFather,发送 /newbot,按提示创建机器人并保存 Bot Token。然后新建“群组”(不是频道),例如 MessageWeave,把机器人加入群并设置为管理员。
在群里发送 /start,然后临时使用 Bot API 的 getUpdates 查看 Chat ID:
curl "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/getUpdates"
返回中的:
"chat":{"id":-5260770881,"title":"MessageWeave","type":"group"}
其中 -5260770881 就是 Chat ID。群 ID 通常是负数,负号不能删除。getUpdates 不是群组列表接口;只有机器人实际收到消息后才会返回该群。若返回空数组,先在群里发送 /start。若已经设置 Webhook,先删除它,否则消息会被 Webhook 消费:
curl -X POST "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/deleteWebhook"
如果机器人看不到普通消息,在 @BotFather 执行 /setprivacy,选择机器人并选择 Disable;或者确保机器人是管理员。调试结束后再设置 Webhook。
3. 生成应用侧鉴权密钥
以下三个值必须分别生成,不能使用 Redis 密码、Bot Token 或 SPA 管理会话:
openssl rand -hex 32 # Telegram Webhook Secret
openssl rand -hex 32 # Reconcile Token
openssl rand -hex 32 # Worker Token
- Telegram Webhook Secret:由你生成,填写 SPA,并在 Telegram
setWebhook时作为secret_token。MessageWeave 会校验请求头X-Telegram-Bot-Api-Secret-Token。 - Reconcile Token:由你生成,供外部 Cron 调用
/reconcile。 - Worker Token:由你生成,供
/worker和兼容管理 API 使用。
4. 创建 SPA 管理会话
打开 https://<message-weave-domain>/,输入部署时 REDIS_URL 中 Redis ACL 用户的密码,点击创建管理会话。会话有效期 900 秒,只保存在当前页面内存,刷新页面后需要重新授权。
5. 填写 Telegram 业务配置
示例:
目标 Chat ID: -5260770881
Chat allowlist: -5260770881
Telegram Webhook Secret: <第一组 openssl 随机值>
Reconcile Token: <第二组 openssl 随机值>
Worker Token: <第三组 openssl 随机值>
目标 Chat ID 和 Chat allowlist 不代表同一个字段:前者是默认目标,后者是允许触发机器人的白名单;测试单个群时可以填写相同的 ID。allowlist 支持每行一个或逗号分隔,重复项会被拒绝。
6. 填写 JMAP 业务配置
JMAP Session URL可填https://mail.example.com或https://mail.example.com/.well-known/jmap;两者会被归一化。- 只能使用 HTTPS;URL 中不要放用户名、密码、query 或 fragment。
JMAP Username填邮箱账号。JMAP Password填 Stalwart 应用专用密码。Account ID初次联调可留空,使用 JMAP Session 的主账户。- 不需要在 Stalwart WebUI 手工创建 PushSubscription。通常内置
user角色已经包含 PushSubscription 的读取、创建、修改和删除权限;只有实际注册返回forbidden时,管理员才需要在/admin → Management → Directory → Accounts/Roles检查角色。Stalwart 的 Push 全局参数位于Settings → Network → JMAP → Push,它不是具体订阅配置。 - LLM 初次联调建议关闭,避免把邮件问题和 AI 网络配置混在一起。
7. 注册 JMAP PushSubscription
MessageWeave 不要求你在 Stalwart WebUI 中手工创建 PushSubscription。保存 JMAP 业务配置后,使用当前 SPA 管理会话或 WORKER_TOKEN 调用注册接口,并明确提供公网 HTTPS callback URL:
curl -X POST "https://<messageweave-domain>/api/push/register" \
-H "Authorization: Bearer <admin-session-or-worker-token>" \
-H "Content-Type: application/json" \
-d '{"callback_url":"https://<messageweave-domain>/push/jmap"}'
成功返回 push_subscription_id。URL 必须是 HTTPS,不能包含用户名、密码或其他内嵌凭据。相同 callback URL 的重复注册请求会复用已有订阅,避免重复创建。Stalwart 随后向 /push/jmap 发送它生成的 PushVerification;MessageWeave 自动通过 JMAP PushSubscription/set 回写验证码,验证成功后才接收正式 Push。注册接口不会猜测平台域名,也不会接受用户填写验证码。
普通 user 角色通常已经具备 PushSubscription 的读取、创建、修改和删除权限,无需手工配置;只有实际返回 forbidden 时,管理员才检查 /admin → Management → Directory → Accounts/Roles。Stalwart 的 Settings → Network → JMAP → Push 是全局 Push 参数,不是具体订阅创建页面。
依据:Stalwart Push notifications、Stalwart Permissions、Stalwart Roles、RFC 8620 §7.2 PushSubscription。
保存业务配置时服务端会真实建立 JMAP 客户端;建立失败返回 503,不会写入坏配置,可以直接修正后再次保存。业务配置没有 GET 接口,保存成功后密钥不会回显;完整替换时必须重新输入必填项。
8. 设置 Telegram Webhook 并打开业务开关
业务配置保存成功后执行:
curl -X POST "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook" \
--data-urlencode "url=https://<message-weave-domain>/webhook/tg" \
--data-urlencode "secret_token=<TELEGRAM_WEBHOOK_SECRET>"
curl "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/getWebhookInfo"
确认返回的 url 正确,并且没有持续增长的 last_error_message。然后回到 SPA 打开“启用业务处理”。全局开关关闭时,Webhook、Reconcile 和 Worker 都返回 503 service disabled,不会入队,也不会 ACK 上游消息。
最后在白名单群里发送测试消息。收到消息后观察 Telegram getWebhookInfo、平台日志和机器人回复;如果没有回复,优先检查 Chat ID 的负号、allowlist、Webhook Secret、全局开关和机器人管理员权限。
9. Reconcile 鉴权测试
curl -i -X POST "https://<message-weave-domain>/reconcile" \
-H "Authorization: Bearer <RECONCILE_TOKEN>"
错误 Token 必须返回 401。外部 Cron 建议每 5–10 分钟调用一次,用于 Push 丢失时的邮件对账兜底。
当前邮件同步方式
当前版本不要把 PushSubscription 当作唯一可靠来源;Push 回调仍需鉴权后入队,邮件同步请同时使用外部 Cron 定时调用 /reconcile。对账使用 Redis 持久 sinceState,并在入队失败时返回 503,不会提前推进游标。
如果部署平台会让服务休眠,Cron 的 HTTP 请求通常可以唤醒容器,但首次请求会承担冷启动延迟,可能超出 Cron 或平台的请求超时时间。因此:
- 将 Cron 超时时间设置得足够长,并允许失败重试;
- 频率不要高于平台允许的唤醒/请求限制;
- 不要把
/healthz当作邮件同步,它只检查进程存活; - 如果平台休眠期间无法唤醒服务,Reconcile 也无法执行,此时需要关闭休眠或改用常驻实例。
对账锁会在长任务运行期间由持有者续租;续租失败不会推进游标。Push 注册和对账都使用 Redis owner-token 单飞锁,避免并发请求重复创建订阅或并行推进同一个 JMAP 游标。
常见踩坑与排查
Redis 报 can't connect with TLS, the feature is not enabled
通常是镜像构建时 Redis TLS feature 未启用,或 redis:// / rediss:// 写错。使用包含 TLS 支持的最新镜像,并确保 REDIS_URL=rediss://...。Upstash 密码中的特殊字符必须 URL 编码,例如 @ 写成 %40。
Redis 报 Multiplexed connection driver unexpectedly terminated
先用同一个 URL 在外部验证网络和凭据;不要只验证 redis-cli --tls 而应用仍使用 redis://。检查 HostStack/平台是否允许出站 6379/TLS,检查 URL 中 ACL 用户、密码和端口。
Rustls 报 provider panic
这是旧镜像同时加载多个 TLS provider 的问题。必须使用包含显式 rustls ring provider 安装逻辑的最新镜像,不要继续使用旧镜像标签缓存。
getUpdates 返回空数组
机器人没有收到新消息,或消息已经被 Webhook 消费。删除 Webhook 后在群里重新发送 /start;确认机器人已加入群、是管理员,并在需要时通过 BotFather 关闭隐私模式。
Telegram 提示无权读取消息
确认创建的是群组而不是频道;把机器人设为管理员;重新发送 /start。隐私模式开启时,机器人通常只能看到命令、回复和提及它的消息。
SPA 保存业务配置返回 503
这通常表示 JMAP URL 无法从部署平台访问、证书/DNS 有问题、应用密码错误,或 LLM 配置不符合 HTTPS 校验。保存失败会保留旧 worker 和旧配置,不会锁死初始化槽位;修正后可重试。
SPA 显示 configuration-setup
检查 HostStack Secret 或容器环境变量中是否同时存在:
REDIS_URL
CONFIG_ENCRYPTION_KEY
CONFIG_ENCRYPTION_KEY 必须是 32 字节十六进制值,例如:
openssl rand -hex 32
它不能写在公开的 hoststack.yaml、Dockerfile、README 或镜像中。
隐私与运维红线(对用户透明)
- 无数据库、无本地文件:机器人所有状态都放在你的 Redis 里;容器自身不写任何本地文件、不挂本地卷,重启后由 Redis + 邮箱对账恢复。
- 日志只走标准输出:容器只往 stdout/stderr 打印日志,由你的平台/采集器负责收集与落盘;机器人自己不写日志文件。
- 日志里不会写敏感内容:邮箱正文、密钥、AI 请求/响应、附件内容都不会出现在日志或 Redis 里,只记录结构化事件、计数、时间戳、脱敏摘要。
- 详见
docs/deployment.md§0 与 §8.2 的检查清单。
目前能做到什么 / 还差什么
已经有了
- 入口框架、三处安全鉴权、配置加载。
- 配置管理 SPA:使用短期 Redis admin session;可编辑完整业务配置并热加载,也可读取和保存
/api/config运行参数。 - 读取 Stalwart 邮箱的基础代码(列文件夹、列邮件、读原文)。
- Telegram Webhook、Chat ID 白名单、Reconcile/Worker Bearer Token 和 Redis 持久化全局启用开关。
- HostStack 原生 Rust 部署配置与 Docker 构建配置。
- 可选:一个免费 Cloudflare Worker 前置负载均衡(多实例 HA),见
docs/deployment.md§10 与cloudflare-worker/子目录。
还差 / 尚未验证
- ⚠️ 真实邮箱联调需要部署者验证:JMAP 客户端代码和配置校验已提供,但不同 Stalwart 网络、权限和应用密码仍可能不同。
- JMAP Push 注册已接入:通过
POST /api/push/register显式提交 HTTPS callback URL 后,后端调用PushSubscription/set create,并由/push/jmap自动完成验证码回写。当前仍需真实 Stalwart 环境验证;外部 Cron 调用/reconcile继续作为可靠补偿通道。 - 生产级多实例协调和发送邮件能力仍未完成;AI 总结属于可选能力,需另行配置并验证。
Docker 镜像和 HostStack 原生 Rust 部署均已提供,但仍建议先按本文完成小范围联调,再考虑生产使用。
对开发者的门禁
任何改动发布前须全绿(在 Debian rust:1-slim-bookworm 容器内执行,见 AGENTS.md §5 与 docs/deployment.md §8):
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
- 真机验证用例以
#[ignore]标记、由环境变量驱动(cargo test -- --ignored jmap::),CI 默认不跑;未实际运行前不得声称“真机通过”。 - 涉及安全边界的改动需同步
docs/design.md、docs/deployment.md、AGENTS.md三份文档。
给开发/技术人员
架构、设计、部署细节见:
许可证
(待定)