Files
ystp/docs/email.md

472 lines
13 KiB
Markdown
Raw Permalink 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.
# 邮件服务设计 - ImageForge
目标:提供开箱即用的邮件服务,支持注册验证和密码重置,管理员只需配置邮箱地址和授权码即可使用。
---
## 1. 功能范围
### 1.1 首期必须
- **注册邮箱验证**:用户注册后发送验证邮件,点击链接完成激活
- **密码重置**:用户申请重置密码,发送重置链接邮件
### 1.2 后续可选V1+
- 订阅到期提醒
- 配额即将用尽提醒
- 支付成功/失败通知
- 安全告警异地登录、API Key 创建等)
---
## 2. 技术方案
### 2.1 SMTP 直连
使用标准 SMTP 协议,通过 `lettre` (Rust) 发送邮件,无需第三方 SaaS 依赖。
```toml
# Cargo.toml
[dependencies]
lettre = { version = "0.11", default-features = false, features = ["tokio1", "tokio1-rustls-tls", "builder", "smtp-transport"] }
```
### 2.2 预置服务商模板
管理员只需选择服务商并填写邮箱/授权码,系统自动填充 SMTP 配置:
| 服务商 | SMTP 地址 | 端口 | 加密 | 备注 |
|--------|-----------|------|------|------|
| QQ 邮箱 | `smtp.qq.com` | 465 | SSL | 需开启 SMTP 服务并获取授权码 |
| 163 邮箱 | `smtp.163.com` | 465 | SSL | 需开启 SMTP 服务并获取授权码 |
| 阿里企业邮箱 | `smtp.qiye.aliyun.com` | 465 | SSL | 使用邮箱密码 |
| 腾讯企业邮箱 | `smtp.exmail.qq.com` | 465 | SSL | 需获取授权码 |
| Gmail | `smtp.gmail.com` | 587 | STARTTLS | 需开启两步验证并生成应用专用密码 |
| Outlook/365 | `smtp.office365.com` | 587 | STARTTLS | 使用账号密码 |
| 自定义 | 用户填写 | 用户填写 | 用户选择 | 支持任意 SMTP 服务器 |
---
## 3. 配置设计
### 3.1 环境变量
```bash
# .env.example
# 邮件服务配置
MAIL_ENABLED=true
# 开发环境:当 MAIL_ENABLED=false 时,可打开该开关把验证/重置链接打印到日志(便于本地联调)
MAIL_LOG_LINKS_WHEN_DISABLED=true
# 预置服务商可选qq / 163 / aliyun_enterprise / tencent_enterprise / gmail / outlook / custom
MAIL_PROVIDER=qq
# 发件邮箱(必填)
MAIL_FROM=noreply@example.com
# 授权码/密码(必填)
MAIL_PASSWORD=your-smtp-authorization-code
# 发件人名称(可选,默认 "ImageForge"
MAIL_FROM_NAME=ImageForge
# === 以下仅 MAIL_PROVIDER=custom 时需要 ===
# MAIL_SMTP_HOST=smtp.example.com
# MAIL_SMTP_PORT=465
# MAIL_SMTP_ENCRYPTION=ssl # ssl / starttls / none
```
### 3.2 数据库配置(管理后台可改)
```sql
-- system_config 表
INSERT INTO system_config (key, value, description) VALUES
('mail', '{
"enabled": true,
"provider": "qq",
"from": "noreply@example.com",
"from_name": "ImageForge",
"password_encrypted": "...",
"custom_smtp": null
}', '邮件服务配置');
```
### 3.3 Rust 配置结构
```rust
#[derive(Debug, Clone, Deserialize)]
pub struct MailConfig {
pub enabled: bool,
pub provider: MailProvider,
pub from: String,
pub from_name: String,
pub password: String, // 运行时解密
pub custom_smtp: Option<CustomSmtpConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub enum MailProvider {
QQ,
NetEase163,
AliyunEnterprise,
TencentEnterprise,
Gmail,
Outlook,
Custom,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CustomSmtpConfig {
pub host: String,
pub port: u16,
pub encryption: SmtpEncryption,
}
#[derive(Debug, Clone, Deserialize)]
pub enum SmtpEncryption {
Ssl,
StartTls,
None,
}
impl MailProvider {
pub fn smtp_config(&self) -> (String, u16, SmtpEncryption) {
match self {
Self::QQ => ("smtp.qq.com".into(), 465, SmtpEncryption::Ssl),
Self::NetEase163 => ("smtp.163.com".into(), 465, SmtpEncryption::Ssl),
Self::AliyunEnterprise => ("smtp.qiye.aliyun.com".into(), 465, SmtpEncryption::Ssl),
Self::TencentEnterprise => ("smtp.exmail.qq.com".into(), 465, SmtpEncryption::Ssl),
Self::Gmail => ("smtp.gmail.com".into(), 587, SmtpEncryption::StartTls),
Self::Outlook => ("smtp.office365.com".into(), 587, SmtpEncryption::StartTls),
Self::Custom => panic!("Custom provider requires explicit config"),
}
}
}
```
---
## 4. 邮件模板
### 4.1 模板存储
建议使用内嵌模板(编译时包含),支持变量替换:
```
templates/
├── email_verification.html
├── email_verification.txt
├── password_reset.html
└── password_reset.txt
```
### 4.2 注册验证邮件
**主题**`验证您的 ImageForge 账号`
**HTML 模板** (`email_verification.html`)
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>验证您的邮箱</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 40px 20px; }
.header { text-align: center; margin-bottom: 30px; }
.logo { font-size: 24px; font-weight: bold; color: #4a90d9; }
.content { background: #f9fafb; border-radius: 8px; padding: 30px; margin-bottom: 30px; }
.button { display: inline-block; background: #4a90d9; color: #fff !important; text-decoration: none; padding: 12px 30px; border-radius: 6px; font-weight: 500; }
.button:hover { background: #357abd; }
.footer { text-align: center; font-size: 12px; color: #666; }
.link { word-break: break-all; color: #4a90d9; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">ImageForge</div>
</div>
<div class="content">
<h2>欢迎注册 ImageForge</h2>
<p>您好,{{username}}</p>
<p>感谢您注册 ImageForge。请点击下方按钮验证您的邮箱地址</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{verification_url}}" class="button">验证邮箱</a>
</p>
<p>或复制以下链接到浏览器打开:</p>
<p class="link">{{verification_url}}</p>
<p style="margin-top: 20px; font-size: 14px; color: #666;">
此链接将在 <strong>24 小时</strong>后失效。如果您没有注册 ImageForge 账号,请忽略此邮件。
</p>
</div>
<div class="footer">
<p>© {{year}} ImageForge. All rights reserved.</p>
<p>此邮件由系统自动发送,请勿直接回复。</p>
</div>
</div>
</body>
</html>
```
**纯文本模板** (`email_verification.txt`)
```text
欢迎注册 ImageForge
您好,{{username}}
感谢您注册 ImageForge。请点击以下链接验证您的邮箱地址
{{verification_url}}
此链接将在 24 小时后失效。如果您没有注册 ImageForge 账号,请忽略此邮件。
---
© {{year}} ImageForge
此邮件由系统自动发送,请勿直接回复。
```
### 4.3 密码重置邮件
**主题**`重置您的 ImageForge 密码`
**HTML 模板** (`password_reset.html`)
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>重置密码</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 40px 20px; }
.header { text-align: center; margin-bottom: 30px; }
.logo { font-size: 24px; font-weight: bold; color: #4a90d9; }
.content { background: #f9fafb; border-radius: 8px; padding: 30px; margin-bottom: 30px; }
.button { display: inline-block; background: #4a90d9; color: #fff !important; text-decoration: none; padding: 12px 30px; border-radius: 6px; font-weight: 500; }
.footer { text-align: center; font-size: 12px; color: #666; }
.link { word-break: break-all; color: #4a90d9; }
.warning { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 12px; margin: 20px 0; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">ImageForge</div>
</div>
<div class="content">
<h2>重置您的密码</h2>
<p>您好,{{username}}</p>
<p>我们收到了重置您 ImageForge 账号密码的请求。请点击下方按钮设置新密码:</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{reset_url}}" class="button">重置密码</a>
</p>
<p>或复制以下链接到浏览器打开:</p>
<p class="link">{{reset_url}}</p>
<div class="warning">
<strong>安全提示:</strong>此链接将在 <strong>1 小时</strong>后失效。如果您没有请求重置密码,请忽略此邮件,您的账号仍然安全。
</div>
</div>
<div class="footer">
<p>© {{year}} ImageForge. All rights reserved.</p>
<p>此邮件由系统自动发送,请勿直接回复。</p>
</div>
</div>
</body>
</html>
```
---
## 5. 数据库设计
### 5.1 邮箱验证 Token
```sql
CREATE TABLE email_verifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(64) NOT NULL, -- SHA256(token)
expires_at TIMESTAMPTZ NOT NULL,
verified_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_email_verifications_token ON email_verifications(token_hash);
CREATE INDEX idx_email_verifications_user_id ON email_verifications(user_id);
CREATE INDEX idx_email_verifications_expires_at ON email_verifications(expires_at);
```
### 5.2 密码重置 Token
```sql
CREATE TABLE password_resets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(64) NOT NULL, -- SHA256(token)
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_password_resets_token ON password_resets(token_hash);
CREATE INDEX idx_password_resets_user_id ON password_resets(user_id);
CREATE INDEX idx_password_resets_expires_at ON password_resets(expires_at);
```
### 5.3 用户表扩展
```sql
ALTER TABLE users ADD COLUMN email_verified_at TIMESTAMPTZ;
```
---
## 6. API 接口
### 6.1 发送验证邮件
```http
POST /auth/send-verification
Authorization: Bearer <token>
```
**响应**
```json
{ "success": true, "data": { "message": "验证邮件已发送,请查收" } }
```
**限流**:同一用户 1 分钟内最多发送 1 次
### 6.2 验证邮箱
```http
POST /auth/verify-email
Content-Type: application/json
```
**请求体**
```json
{ "token": "verification-token-from-email" }
```
**响应**
```json
{ "success": true, "data": { "message": "邮箱验证成功" } }
```
### 6.3 请求密码重置
```http
POST /auth/forgot-password
Content-Type: application/json
```
**请求体**
```json
{ "email": "user@example.com" }
```
**响应**(无论邮箱是否存在都返回成功,防止枚举):
```json
{ "success": true, "data": { "message": "如果该邮箱已注册,您将收到重置邮件" } }
```
**限流**:同一 IP 1 分钟内最多请求 3 次
### 6.4 重置密码
```http
POST /auth/reset-password
Content-Type: application/json
```
**请求体**
```json
{
"token": "reset-token-from-email",
"new_password": "new-secure-password"
}
```
**响应**
```json
{ "success": true, "data": { "message": "密码重置成功,请重新登录" } }
```
---
## 7. 安全考虑
### 7.1 Token 安全
- Token 使用 `crypto-secure random`32 字节 base64
- 数据库只存 `SHA256(token)`,不存明文
- Token 单次有效,使用后立即标记 `used_at`
### 7.2 时效控制
- 邮箱验证 Token24 小时有效
- 密码重置 Token1 小时有效
### 7.3 防滥用
- 发送邮件接口严格限流
- 密码重置不泄露"邮箱是否存在"
- 失败尝试记录审计日志
### 7.4 授权码加密存储
- SMTP 授权码在数据库中加密存储AES-256-GCM
- 密钥来自环境变量或密钥管理服务
---
## 8. 管理后台配置界面
管理后台提供邮件服务配置页面:
```
邮件服务配置
├── 启用状态:[开关]
├── 服务商选择:[下拉QQ邮箱 / 163邮箱 / 阿里企业邮 / 腾讯企业邮 / Gmail / Outlook / 自定义]
├── 发件邮箱:[输入框]
├── 授权码/密码:[密码输入框]
├── 发件人名称:[输入框,默认 ImageForge]
├── (自定义时显示)
│ ├── SMTP 服务器:[输入框]
│ ├── 端口:[输入框]
│ └── 加密方式:[下拉SSL / STARTTLS / 无]
└── [测试发送] [保存配置]
```
**测试发送**:向管理员邮箱发送测试邮件,验证配置是否正确。
---
## 9. 常见邮件服务商配置指南
### 9.1 QQ 邮箱
1. 登录 QQ 邮箱 → 设置 → 账户
2. 找到「POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务」
3. 开启「SMTP 服务」
4. 按提示发送短信获取授权码
5. 将授权码填入系统配置
### 9.2 163 邮箱
1. 登录 163 邮箱 → 设置 → POP3/SMTP/IMAP
2. 开启「SMTP 服务」
3. 设置客户端授权密码
4. 将授权密码填入系统配置
### 9.3 Gmail
1. 登录 Google 账号 → 安全性
2. 开启「两步验证」
3. 生成「应用专用密码」(选择"邮件"+"其他"
4. 将应用专用密码填入系统配置
### 9.4 阿里企业邮箱
1. 使用邮箱地址和登录密码即可
2. SMTP 服务默认开启