Files
micro_servers/smtp/README.md
T
qing 931e134796 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
2026-07-30 12:02:25 +08:00

166 lines
6.3 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.
# 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