472 lines
13 KiB
Markdown
472 lines
13 KiB
Markdown
# 邮件服务设计 - 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 时效控制
|
||
- 邮箱验证 Token:24 小时有效
|
||
- 密码重置 Token:1 小时有效
|
||
|
||
### 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 服务默认开启
|