MessageWeave/HANDOFF-push-401.md

214 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# HANDOFF — 邮件 Push 通知投递问题
> **日期**: 2026-10-13
> **状态**: 代码修复已完成,**未部署**
> **接手方优先级**: P0 — 用户阻塞中,邮件 push 收不到
---
## 1. 问题描述
**症状**: 用户配置了 Stalwart 邮件服务器 + Cloudflare Worker + Telegram Bot,通过 **cron 定时任务**(`/reconcile` 路径)可以正常收到邮件通知,但 **邮件 Push**(Stalwart → `/push/jmap` 路径)**完全收不到**。
**用户已确认的现象**:
- `/help` 等 Telegram 命令秒回
- cron 路径(`/reconcile` → JMAP `/changes` → worker → Telegram)工作正常
- 邮件 push 通知(Stalwart → `/push/jmap`)从未到达 Telegram
---
## 2. 根因分析(已完成,结论确认)
### 2.1 根因: 验证码 key 5 分钟后过期(旧代码 `TTL=300`)
Stalwart 按 RFC 8620 协议向我们注册 Push 订阅时:
1. **注册时**: Stalwart 发第一次带 `verificationCode` 的验证请求,我们的后端把验证码存进 Redis(key = `push:subscription:{id}`,value = 验证码)。**旧代码 TTL = 300 秒(5 分钟)**。
2. **事件推送时**: 每封新邮件到达后,Stalwart 发带 `emailId`/`changeType` 但**不带** `verificationCode` 的事件推送。我们的后端去 Redis 查 `push:subscription:{id}` 确认订阅已验证 — 但 key 已在 5 分钟后过期 → 返回 **401 unauthorized** → 邮件通知被丢弃。
### 2.2 时间线证据(来自 Stalwart 日志)
| 时间 (UTC) | 事件 |
|---|---|
| 02:52:30 | 用户执行 register,Stalwart 分配 `push_subscription_id: "b"`,向 `/push/jmap` 发第一次验证请求,后端存 key(TTL=300) |
| ~02:57:30 | Redis key 过期被自动删除 |
| 02:58:49 | 测试邮件到达 Stalwart,changeId=305,documentId=167 |
| 02:58:50 | Stalwart 向 `/push/jmap` 推送事件 → 后端查 key → **401 unauthorized** |
**注册到 401 恰好间隔 ~6 分钟,与 TTL=300(5 分钟)完全吻合。**
### 2.3 为什么 cron 能收到
cron 走 `/reconcile` → 主动调 JMAP `/changes` → 拉新邮件 → 发 Telegram。这条路径**完全不依赖验证码 key**,所以不受 TTL 影响。push 路径依赖 key 核对,key 过期就 401。
### 2.4 次要问题: 限流器 429
注册日志里出现 `429 push_verify_rate_limited` — Stalwart 在 30 秒内重复发了验证请求,第二个被限流器(`ratelimit:push-verify:{subscription}`,TTL=30s)拦住。但**第一个请求已成功存了 key**(只是 5 分钟后过期),所以 429 不是当前 401 的原因,只是鲁棒性问题。
---
## 3. 已完成的修复(3 个 commit,均已 push 双 remote)
| Commit | 修复内容 | 关键代码位置 |
|---|---|---|
| `d452f95` | `PUSH_SUBSCRIPTION_SECONDS` 300 → 604800(7 天) | `src/state.rs` ttl 模块 |
| `7331cb7` | `PUSH_SUBSCRIPTION_SECONDS` 604800 → **0(永不过期)** | `src/state.rs` ttl 模块 |
| `f9160c0` | ① 验证响应回 `{"verificationCode":"..."}`(原返回空 204,违反 RFC 8620 §7.2);② `verify_push_subscription` JMAP API 改为 best-effort(失败不阻塞) | `src/notify.rs` `jmap_push` 验证分支 |
### 3.1 关键代码变化
**`src/state.rs`** — TTL 常量:
```rust
pub const PUSH_SUBSCRIPTION_SECONDS: u64 = 0; // 永不过期
```
`remember_push_subscription` 处理 `ttl_seconds == 0` 时用 `SET key value`(不带 `EX`),MemoryState 用 `expiry = i64::MAX`。
**`src/notify.rs`** — 验证流程(当前代码,已修但未部署):
```
if 带 verificationCode(验证请求):
1. rate limiter claim_dedup → 429 拦截(❗这在 remember 之前,有问题)
2. verify_push_subscription JMAP API → best-effort(warn 不阻塞)
3. remember_push_subscription → 存 key(TTL=0 永不过期)
4. set_push_subscription_status → "verified"
else(事件推送):
查 push:subscription:{id} 是否存在 → 不存在 401
```
**RFC 8620 §7.2 响应**(`notify.rs` 的 `jmap_push` handler,验证请求分支末尾):
```rust
if let Some(code) = push.verification_code.as_deref() {
return axum::Json(serde_json::json!({
"verificationCode": code
})).into_response();
}
return StatusCode::NO_CONTENT.into_response();
```
---
## 4. 未完成的工作(接手方要做)
### 4.1 【P0】部署最新代码到服务器
**当前服务器跑的是旧代码(TTL=300),三个修复 commit 都在 git 里但没部署。**
部署方式(Docker):
```bash
# 在服务器上
git pull origin main # 拉到 f9160c0 或更新
docker build -t messageweave .
docker stop <容器名>
docker run --rm --env-file .env -p 8080:8080 messageweave
```
或者如果不用 Docker:
```bash
git pull origin main
cargo build --locked --release
# 重启运行中的 message-weave 进程
```
### 4.2 【P0】部署后重新注册 Push 订阅
部署新代码后,旧的 key 可能还残留(或已过期),需要让 Stalwart 重新走一次验证流程:
```bash
# 变量
BACKEND="https://messageweave.motofans.club"
TOKEN="你的 worker_token(业务配置里的)"
# 1. 禁用旧订阅
curl -X POST "$BACKEND/api/push/disable" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"callback_url":"https://messageweave.motofans.club/push/jmap"}'
# 2. 重新注册(Stalwart 会发新验证请求,新代码存 key 且永不过期)
curl -X POST "$BACKEND/api/push/register" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"callback_url":"https://messageweave.motofans.club/push/jmap"}'
# 3. 发测试邮件,观察 Stalwart 日志确认不再 401
```
### 4.3 【P1】修复限流器顺序(代码改动)
**问题**: `notify.rs` `jmap_push` 的验证分支中,rate limiter(`claim_dedup("ratelimit:push-verify:{subscription}")`)在 `remember_push_subscription` **之前**。如果 Stalwart 在 30 秒内重发验证请求,第二个请求被 429 拦住 → `remember_push_subscription` 不会被调用 → 那次尝试的 key 没存。虽然第一个请求通常已存了 key,但这不鲁棒。
**修法**: 把 `remember_push_subscription` 移到 rate limiter **之前**,rate limiter 只控制**出站** `verify_push_subscription` JMAP API 调用(防止频繁调外部 API),**不拦验证请求本身**。
修改后流程应该是:
```
if 带 verificationCode:
1. remember_push_subscription(先存 key,TTL=0) ← 移到最前
2. set_push_subscription_status → "verified"
3. rate limiter claim_dedup → 只拦 JMAP API 调用,不拦整个请求
4. verify_push_subscription JMAP API → best-effort
```
**需要同步更新的文档**(改代码后必须一起改,否则 GATE-DOCS 会挂):
- `docs/reference.md` / `docs/reference.zh-CN.md` — `ratelimit:push-verify:{subscription}` 键说明行 + 429 `push_verify_rate_limited` 错误码表行
- `docs/opengaps.md` / `docs/opengaps.zh-CN.md` — TTL 表 `PUSH_VERIFY_LIMIT_SECONDS` 行
- `docs/design.md` / `docs/design.zh-CN.md` — "Push verification code writing current limiting" 段落
### 4.4 【P2】验证完整链路
部署 + 重新注册后:
1. 发测试邮件
2. 看 Stalwart 日志: 验证请求应该返回 200 + `{"verificationCode":"..."}`,事件推送应该返回 2xx(不再 401)
3. 看 Telegram 是否收到通知
4. 等 10+ 分钟再发第二封(确认 key 不再过期)
---
## 5. 两条投递路径说明
| 路径 | 触发方式 | 依赖验证码 key | 当前状态 |
|---|---|---|---|
| **Cron** | 定时调 `/reconcile` → JMAP `/changes` → 拉新邮件 → worker → Telegram | ❌ 不依赖 | ✅ 正常工作 |
| **Push** | Stalwart → `/push/jmap`(事件推送)→ 查 key → worker → Telegram | ✅ 依赖(401 的原因) | ❌ 旧代码 key 过期 → 401 |
---
## 6. 环境信息
- **Stalwart**: `0.16.26`,Docker 运行在 `mail.motofans.club`
- **Push 订阅 ID**: `"b"`(Stalwart 分配的单字符 ID)
- **回调 URL**: `https://messageweave.motofans.club/push/jmap`
- **后端仓库**: `/home/okabe/Repo/messageweave`
- **双 remote**: `origin`(motofans.club)+ `upstream`(GitHub fork)
- **测试基线**: Rust 106 passed / 0 failed / 4 ignored,GATE-DOCS 6/6 PASS
- **最新 commit**: `f9160c0`(已 push 双 remote)
---
## 7. 已知的文档残留(非阻塞)
- `docs/opengaps.md` 声称"当前无未完成项",但限流器顺序问题(§4.3)还没修 — 接手方修完后应更新
- `docs/reference.md` / `docs/reference.zh-CN.md` 的 `ratelimit:push-verify` 与 429 错误码表行,在限流器顺序修完后需要改
- `docs/roadmap.md` 不存在(之前有任务提到要创建,没落地)
- `audit_anchor_semantics.py` 有 7 个候选(历史遗留,非本轮引入)
- `docs/reference.zh-CN.md` 机翻质量问题(历史遗留)
---
## 8. 快速诊断命令
```bash
# 查看部署版本
curl -s https://messageweave.motofans.club/api/status | jq .
# 查看 push 注册状态(需要 worker_token)
TOKEN="你的token"
curl -s https://messageweave.motofans.club/api/push/status \
-H "Authorization: Bearer $TOKEN" | jq .
# 查看 Redis 中验证码 key 是否存在(需要 Redis 访问)
# key 格式: push:subscription:{subscription_id}
redis-cli GET "push:subscription:b"
# 查看 Stalwart 日志(在 mail.motofans.club 上)
docker logs <stalwart容器名> --since 10m 2>&1 | grep -i push
```