docs(smtp): 新建完整 README,覆盖协议、路由、配置与安全机制

- 3 个路由的完整请求/响应文档(send_email / verify_email / health)
- .env 配置项说明(4 种格式支持、必填/可选字段)
- 验证码生命周期:生成→冷却→消费→过期→作废→失败回滚
- 安全机制:发送冷却、失败次数上限防枚举、SMTP 失败回滚
- 集成关系:gateway 调用流程、core 守护、log_client 日志推送
- 旧版 smtp.py 的安全风险标注

fix(smtp): _set.sh 中 nuitka 编译目标 smtp.py → smtp_v2.py
This commit is contained in:
qing committed 2026-07-30 12:02:25 +08:00
1 parent 63a80694ae
commit 931e134796
2 files changed
+166 -1

No files matched your search

+165
View File
@@ -0,0 +1,165 @@
# smtp — 邮箱验证码服务
基于自研 [pymsg](../pymsg/) 协议(TCP 长度前缀帧 + JSON)的邮箱验证码服务,通过 SMTP_SSL 发送 6 位数字验证码,支持发送冷却和防暴力枚举。
服务由 `core`(PMC)的 `init.txt` 拉起,监听 `127.0.0.1:8008`(TCP),仅允许本机调用。`gateway` 在 `/register` 流程中经 pymsg 调用本服务完成邮箱验证。
## 架构
```
gateway :8443 (注册流程)
│ pymsg TCP
▼
smtp :8008 (MsgServer)
│ SMTP_SSL
▼
外部 SMTP 服务器 (163/QQ 等)
│
▼
用户邮箱(收验证码)
```
## 路由
服务基于 pymsg 协议,每条消息为完整 JSON,`_route` 字段指定路由。
| _route | 说明 |
|--------|------|
| `send_email` | 生成 6 位数字验证码,通过 SMTP_SSL 发送至指定邮箱 |
| `verify_email` | 校验用户输入的验证码,一次性消费 |
| `health` | 健康检查 |
响应格式:
```json
{"success": true, "data": {...}}
{"success": false, "error": "..."}
```
### 1. send_email — 发送验证码
**请求**:
```json
{"_route": "send_email", "email": "user@example.com"}
```
可选字段 `subject` / `content` 覆盖默认标题与正文,`content` 中 `{code}` 会被替换为验证码。
**响应**:
| 响应 data | 触发条件 |
|-----------|----------|
| `{"sent": true, "ttl": 600}` | 发送成功,TTL 为有效期(秒) |
| `{"sent": false, "error": "invalid email"}` | 邮箱格式无效 |
| `{"sent": false, "error": "SMTP 服务未配置"}` | .env 缺少 SMTP 配置 |
| `{"sent": false, "retry_after": N, "error": "发送过于频繁,请 N 秒后重试"}` | 同一邮箱在冷却期内 |
**安全机制**:
- 发送冷却:同一邮箱两次成功发送的最小间隔 `SEND_INTERVAL`(60 秒)。冷却期内请求直接拒绝,不覆盖已有验证码。
- 发送失败回滚:SMTP 发送失败时立即删除刚写入的验证码,避免"码已存但邮件没到"的不可用状态。
### 2. verify_email — 校验验证码
**请求**:
```json
{"_route": "verify_email", "email": "user@example.com", "code": "123456"}
```
兼容旧字段名 `input_captcha`。
**响应**(均为 `success: true`,业务结果在 `data` 中区分):
| 响应 data | 触发条件 |
|-----------|----------|
| `{"valid": true}` | 验证码正确(已消费) |
| `{"valid": false, "error": "email and code required"}` | 缺少字段 |
| `{"valid": false, "error": "invalid or expired code"}` | 验证码错误、过期或不存在 |
| `{"valid": false, "invalidated": true, "error": "验证码连续错误 5 次,已失效,请重新获取"}` | 连续校验失败达上限 |
> 验证码一经正确校验即从内存中删除(一次性消费),防重放。同一验证码连续校验失败达 `MAX_VERIFY_ATTEMPTS`(5 次)即作废,防止对 6 位数字码的在线枚举。
### 3. health — 健康检查
**请求**:`{"_route": "health"}`
**响应**:`{"success": true, "data": {"status": "ok", "service": "smtp"}}`
## 配置
通过 `.env` 文件配置 SMTP 连接信息,支持 4 种格式:
```bash
# KEY=VALUE(推荐)
SMTP_SERVER=smtp.163.com
SMTP_PORT=465
SMTP_USER=your@163.com
__PASSWORD=your_smtp_password
# 也支持 KEY: VALUE / KEY VALUE / export KEY=...
MAIL_FROM=dnfs@qsont.me # 可选,发件人身份;默认跟随 SMTP_USER
```
| 变量 | 必填 | 说明 |
|------|------|------|
| `SMTP_SERVER` | 是 | SMTP 服务器地址 |
| `SMTP_PORT` | 否 | SMTP 端口,默认 465 |
| `SMTP_USER` | 是 | 邮箱登录账号 |
| `__PASSWORD` | 是 | 邮箱密码/授权码(兼容别名 `SMTP_PASSWORD`) |
| `MAIL_FROM` | 否 | 发件人身份,默认同 SMTP_USER。多数服务商强制 From 头与登录账号一致 |
服务启动时检查必填配置项,缺失时打印警告日志但不阻止启动(`send_email` 路由会返回配置错误)。
## 数据模型与验证码生命周期
- **存储**:进程内存字典 `code_store`,键为 email,值为 `{code, expires_at, attempts}`。
- **冷却记录**:`send_history` 字典,键为 email,值为上次成功发送的时间戳。
- **生成**:`send_email` 成功时写入 `code_store`,记录 `expires_at = now + CODE_TTL`(600 秒)。
- **消费**:`verify_email` 校验通过时 `del code_store[email]`,不可再次使用。
- **过期**:超时后 `verify_email` 自动删除并返回 `invalid or expired code`。`send_email` 时惰性清理过期项。
- **作废**:连续校验失败达 5 次后删除验证码,返回 `invalidated: true`。
- **发送失败回滚**:SMTP 异常时立即 `pop` 刚写入的 `code_store` 条目。
## 运行
```bash
# 初始化(创建 venv + 安装依赖)
bash _set.sh
# 启动
bash _run.sh
# 或直接运行
python3 smtp_v2.py --addr 127.0.0.1 --port 8008
```
## 集成关系
- 由 `core` 通过 `init.txt` 拉起的 `bash _run.sh` 守护运行。
- `gateway` 在 `/register` 时经 pymsg 调 `send_email` 发送验证码,调 `verify_email` 校验。需在 gateway 的 `example.ini` 中启用邮箱验证功能。
- 日志通过 `log_client.py` 异步批量推送至 db:8005 的 Logs 表(缓冲 50 条 / 2 秒)。
## 项目结构
```
smtp/
├── smtp_v2.py # 服务主程序(pymsg 协议,当前有效版本)
├── smtp.py # 旧版 Flask HTTP 实现(已废弃,仅作参考)
├── log_client.py # 集中日志推送 SDK
├── _set.sh # 初始化脚本(创建 venv + 安装依赖)
├── _run.sh # 启动脚本(exec python3 smtp_v2.py)
├── requirements.txt # Python 依赖(pymsg、nuitka)
└── .gitignore # 忽略 __pycache__ / .env / venv
```
## 注意事项
- 验证码存储在进程内存中,服务重启后全部失效;不支持多实例水平扩展。
- 默认仅监听 `127.0.0.1`,仅供本机调用;浏览器侧不直接访问本服务。
- 旧版 `smtp.py`(Flask HTTP 实现)已废弃,`_run.sh` 当前启动的是 `smtp_v2.py`。旧版存在无冷却、无失败回滚、无防枚举等安全问题,且 `verify_email` 在 email 不存在时有 KeyError 风险,不应在生产环境使用。
- 多数邮箱服务商(163/QQ 等)强制 From 头与登录账号一致,`MAIL_FROM` 仅当服务商允许自定义发件身份时才起作用。
- 验证码为纯 6 位数字,有效期 600 秒。
## 许可证
MIT License