前置介绍
本实验参考知了传课的最新教程实现,利用fastapi-mail发送邮箱验证码,实验采用qq邮箱来进行邮箱的发送和接受
安装插件
1
|
pip install fastapi-mail
|
邮箱配置
首先打开QQ邮箱的首页 点击左上角 设置
然后点开左边的账号与安全
再进入安全设置
我们绑定完手机之后,去生成授权码

之后我们就拿到自己的授权码了

注意:授权码用于 SMTP 身份验证,不是邮箱登录密码,每次生成的都不一样,如果被别人弄走了,重新生成一个即可。
Fastapi-Mail配置
在自己的配置文件(我这里是settings),放置自己的配置,推荐使用pydantic_setting的BaseSettings:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# settings.py
from pydantic_settings import BaseSettings
class MailSettings(BaseSettings):
MAIL_USERNAME: str
MAIL_PASSWORD: str
MAIL_FROM: str
MAIL_PORT: int = 587
MAIL_SERVER: str = "smtp.qq.com"
MAIL_FROM_NAME: str = "FastAPI Mail Service"
MAIL_STARTTLS: bool = True
MAIL_SSL_TLS: bool = False
USE_CREDENTIALS: bool = True
VALIDATE_CERTS: bool = True
class Config:
env_file = ".env"
|
同时创建.env文件:
1
2
3
|
MAIL_USERNAME=your_email@qq.com
MAIL_PASSWORD=your_16_digit_auth_code
MAIL_FROM=your_email@qq.com
|
创建 FastMail 实例与依赖注入
创建工厂函数
先写一个FastMail实例的工厂函数,每次调用都返回一个新的实例,防止阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
from fastapi_mail import FastMail, ConnectionConfig
from pydantic import SecretStr, EmailStr
from settings import base_settings
def create_mail_instance() -> FastMail:
mail_config = ConnectionConfig(
MAIL_USERNAME=base_settings.MAIL_USERNAME,
MAIL_PASSWORD=base_settings.MAIL_PASSWORD,
MAIL_FROM=base_settings.MAIL_FROM,
MAIL_PORT=base_settings.MAIL_PORT,
MAIL_SERVER=base_settings.MAIL_SERVER,
MAIL_FROM_NAME=base_settings.MAIL_FROM_NAME,
MAIL_STARTTLS=base_settings.MAIL_STARTTLS,
MAIL_SSL_TLS=base_settings.MAIL_SSL_TLS,
USE_CREDENTIALS=True,
VALIDATE_CERTS=True,
)
return FastMail(mail_config)
|
创建依赖函数
根据工厂函数创建依赖
1
2
3
4
5
6
7
|
from fastapi_mail import FastMail
from models import AsyncSessionFactory
from core.mail import create_mail_instance
async def get_mail() -> FastMail:
return create_mail_instance()
|
mail发送测试接口
写一个api测试一下邮件发送:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
from fastapi import FastAPI, Depends
from fastapi_mail import FastMail, MessageSchema, MessageType
from aiosmtplib import SMTPResponseException
from dependencies import get_mail
app = FastAPI()
@app.get("/")
async def root():
return {"msg": "Hello World!"}
@app.get("/mail/test")
async def test_mail(email: str, mail: FastMail = Depends(get_mail)):
message = MessageSchema(
subject="测试邮件",
recipients=[email],
body=f"HELLO {email}",
subtype=MessageType.plain
)
await mail.send_message(message)
return {"msg": "发送成功"}
|
这里引入一个MessageSchema的pydatic模型用于数据校验,具体就是这四个东西:
- subject=“测试邮件”,
- recipients=[email],
- body=f"HELLO {email}",
- subtype=MessageType.plain
注意 QQ 邮箱 SMTP 限制:
- 使用端口 587(STARTTLS)或 465(SSL)。本文配置使用 587。
- 每日发送限额约为 500 封,超出后会被临时限制。
fastapi-mail 已知问题
在部分 SMTP 服务器(如 QQ 邮箱)关闭连接时,会返回非标准字节流,导致底层 aiosmtplib 抛出 code=-1 的异常。此为协议兼容性问题,邮件已成功投递,可安全忽略该特定异常。

优化错误处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@app.get("/mail/test")
async def test_mail(email: str, mail: FastMail = Depends(get_mail)):
message = MessageSchema(
subject="测试邮件",
recipients=[email],
body=f"HELLO {email}",
subtype=MessageType.plain
)
try:
await mail.send_message(message)
except SMTPResponseException as e:
if e.code == -1 and b"\\x00\\x00\\x00" in str(e).encode():
print("忽略QQ邮箱SMTP关闭阶段的非标准式响应,邮箱已经发送成功")
return {"msg": "发送成功"}
|
在获取到对应错误的时候,忽略一下即可
生产环境建议
- 敏感配置必须通过环境变量管理,禁止硬编码。
- 对验证码接口实施频率限制(如每 IP 每分钟 1 次)。
- 使用 HTML 邮件提升用户体验:
1
2
3
4
5
6
|
message = MessageSchema(
subject="验证码",
recipients=[email],
body="<h2>您的验证码是:<strong>123456</strong></h2>",
subtype=MessageType.html
)
|
- 记录邮件发送日志,便于问题追踪。
HTML 验证码邮件示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@app.get("/mail/verify")
async def send_verify_code(
email: str = Query(..., regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"),
code: str = Query(..., min_length=6, max_length=6),
mail: FastMail = Depends(get_mail)
):
message = MessageSchema(
subject="【验证码】您的登录验证码",
recipients=[email],
body=f"""
<div>
<h3>验证码:{code}</h3>
<p>5 分钟内有效,请勿泄露。</p>
</div>
""",
subtype=MessageType.html
)
await mail.send_message(message)
return {"message": "验证码已发送"}
|