Files
vscode-workbench/.trae/documents/plan-fix-email-bounce-error.md
T

206 lines
7.5 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.
# 修复邮件发送异常处理:MX记录验证 + 后端日志记录
## 摘要
当用户输入不存在域名的邮箱(如 `sync4@example.com`)时,QQ SMTP 服务器会接受邮件但后续产生 bounce,退信被发回 FROM 地址(`cs10086086@qq.com`)。由于 FROM 地址就是用户自己的 QQ 邮箱,用户收到了退信而非验证码。需要:
1. 在发送邮件前验证邮箱域名有效性(MX 记录)
2. 将异常信息记录到后端 ErrorLog,而不是让用户收到退信
## 当前状态分析
### 邮件发送流程
```
前端 → SendUserEmailAPIView → 储存验证码到 Redis → 返回成功 → submit_task (Celery) → _send_html_email → email.send() (QQ SMTP)
↓
SMTP 返回退信到 cs10086086@qq.com
```
### 现有 MX 验证情况
| API 端点 | 文件位置 | MX 验证 |
|:---|:---|:---|
| `SendUserEmailAPIView` | `user/views/user.py:89` | **无** ❌ |
| `ForgotPasswordSendCodeAPIView` | `user/views/user.py:276` | **有** ✅ (第 287 行) |
| `SendChangeEmailCodeAPIView` | `user/views/email.py:37` | **无** ❌ |
### 现有错误处理
| 层级 | 文件 | 行为 |
|:---|:---|:---|
| View try/except | `user.py:132-137`, `email.py:96-102` | 返回 HTTP 500 + "邮件发送失败" |
| `safe_task.py:submit_task` | `safe_task.py:28-45` | 超时降级同步;失败只记录 warning |
| `_send_html_email` | `tasks.py:62-69` | **没有任何 try/except** - SMTP 异常直接穿透 |
| Django LOGGING | `settings.py:298-338` | ERROR 级别 → `DatabaseErrorHandler` → `ErrorLog` 表 |
### 根因
1. `SendUserEmailAPIView` 缺少 MX 记录验证 — `ForgotPasswordSendCodeAPIView` 已有但未复用到其他端点
2. QQ SMTP 对无效域名不会立即报错,而是在异步退信中通知 FROM 地址
3. FROM 地址 (`cs10086086@qq.com`) 就是用户自己的邮箱,退信直接发给了用户
4. `_send_html_email` 中 `email.send()` 没有任何异常捕获,即使 SMTP 抛异常也无法被妥善记录
## 修改方案
### 文件 1: `chunyu_project\user\tasks.py`
**改动**:在 `_send_html_email` 中增加 try/except 捕获 SMTP 异常并记录日志
```python
# 修改前 (第 41-69 行)
def _send_html_email(to_email, code, config_key):
config = EMAIL_CONFIG[config_key]
# ... 构建 email ...
email.send()
# 修改后
def _send_html_email(to_email, code, config_key):
import logging
logger = logging.getLogger(__name__)
config = EMAIL_CONFIG[config_key]
# ... 构建 email (不变) ...
try:
email.send()
except Exception as e:
logger.error(
f'[Email] SMTP send failed: to={to_email}, type={config_key}, error={str(e)}',
exc_info=True
)
raise # 重新抛出,让 safe_task 的降级逻辑感知失败
```
### 文件 2: `chunyu_project\user\views\user.py`
**改动**:在 `SendUserEmailAPIView` 中增加 MX 记录验证
```python
# 修改前 (第 89-137 行) - 直接发送,无 MX 验证
def post(self, request):
to_email = request.data.get('to_email', None)
if to_email is None or to_email == "":
return ...
try:
# 直接开始发送邮件
...
# 修改后 - 在发送前增加 MX 验证
def post(self, request):
to_email = request.data.get('to_email', None)
if to_email is None or to_email == "":
return create_standardized_error_response(
code=ResponseCode.EMAIL_EMPTY,
status_code=status.HTTP_400_BAD_REQUEST
)
# 新增:验证 MX 记录
if not validate_email_mx(to_email):
logger.warning(f'[Email] Domain MX check failed: email={to_email}')
return create_standardized_error_response(
code=ResponseCode.EMAIL_DOMAIN_INVALID,
status_code=status.HTTP_400_BAD_REQUEST
)
try:
# 原有发送逻辑不变
...
```
**改动**:统一 `ForgotPasswordSendCodeAPIView` 的 MX 验证位置,保持在发送前
此 API 已有 MX 验证(第 287-291 行),保持不动。无需修改。
### 文件 3: `chunyu_project\user\views\email.py`
**改动**:在 `SendChangeEmailCodeAPIView` 中增加 MX 记录验证
```python
# 修改前 (第 37-102 行) - 无 MX 验证
def post(self, request):
# captcha验证...
serializer = SendEmailCodeSerializer(...)
if not serializer.is_valid():
return ...
try:
email = serializer.save() # save() 中调用 submit_task 发邮件
...
# 修改后 - 在 serializer 验证后、save 发送前增加 MX 验证
def post(self, request):
# captcha验证 (不变)...
serializer = SendEmailCodeSerializer(data=request.data, context={'request': request})
if not serializer.is_valid():
return ...
# 新增:MX 验证
to_email = serializer.validated_data.get('email')
if to_email and not validate_email_mx(to_email):
logger.warning(f'[ChangeEmail] Domain MX check failed: email={to_email}')
record_failure(operation, identifier)
return create_standardized_error_response(
code=ResponseCode.EMAIL_DOMAIN_INVALID,
status_code=status.HTTP_400_BAD_REQUEST
)
try:
email = serializer.save()
...
```
注意:需要在 `email.py` 顶部导入 `validate_email_mx`:
```python
from ..views.user import validate_email_mx
# 或将该函数提取到独立模块 utils/email_utils.py
```
### 文件 4 (可选优化): `chunyu_project\utils\safe_task.py`
**改动**:增加 email-specific 的错误日志上下文
当前 `submit_task` 降级失败时只记录 `logger.error('submit_task eager fallback failed...')`,可以增加邮件相关的上下文(发送目标、类型)。此项为可选优化,本次不做强制要求。
### 文件 5 (推荐): 提取 `validate_email_mx` 到独立模块
**改动**:将 `validate_email_mx` 从 `user/views/user.py` 提取到 `utils/email_utils.py`
```python
# 新建文件: chunyu_project/utils/email_utils.py
import socket
import logging
logger = logging.getLogger(__name__)
def validate_email_mx(email):
"""验证邮箱域名是否有MX记录"""
try:
domain = email.split('@')[1]
socket.getaddrinfo(domain, None, socket.AF_INET, socket.SOCK_STREAM)
return True
except (socket.gaierror, IndexError, Exception):
logger.warning(f'MX record check failed for domain: {email}')
return False
```
然后在 `user/views/user.py` 和 `user/views/email.py` 中导入:
```python
from utils.email_utils import validate_email_mx
```
## 修改汇总
| 文件 | 改动类型 | 具体内容 |
|:---|:---|:---|
| `utils/email_utils.py` | **新建** | 提取 `validate_email_mx` 独立模块 |
| `user/views/user.py` | 修改 | 删除本地 `validate_email_mx`;`SendUserEmailAPIView` 增加 MX 验证 |
| `user/views/email.py` | 修改 | `SendChangeEmailCodeAPIView` 增加 MX 验证 |
| `user/tasks.py` | 修改 | `_send_html_email` 增加 SMTP 异常捕获和日志 |
## 验证步骤
1. 用 `sync4@example.com` 测试 `POST /user/send_user_email/` → 应返回 `20023 EMAIL_DOMAIN_INVALID` 而非 200/201
2. 用 `sync4@example.com` 测试 `POST /user/send_email_code/` → 应返回 `20023 EMAIL_DOMAIN_INVALID`
3. 确认后端 ErrorLog 表中记录了 `[WARNING] Domain MX check failed` 日志
4. 确认用户不再收到 SMTP 退信
5. 用正常邮箱(如 `testuser@qq.com`)测试 → 功能不受影响,正常发送验证码