Python 标准库已经提供了完整的邮件发送能力:用 email.message.EmailMessage 构造消息,通过 smtplib 连接 SMTP 服务器。真正需要处理好的,不只是“把邮件发出去”,还包括纯文本降级、HTML 内容、附件类型、连接加密、批量个性化以及凭据管理。
SMTP 发送链路是怎样工作的
一封邮件通常要经过三个步骤:
- 程序构造发件人、收件人、主题和正文等消息内容。
smtplib连接邮件服务商提供的 SMTP 服务器,并完成身份认证。- SMTP 服务器接收消息,再将它投递到目标邮箱所属的邮件系统。
常见的加密连接方式有两种:
- 隐式 TLS:通常使用 465 端口,通过
smtplib.SMTP_SSL建立加密连接。 - STARTTLS:通常使用 587 端口,先连接服务器,再调用
starttls()升级为加密连接。
端口和认证方式必须以邮件服务商的配置为准。许多服务商不允许直接使用账户登录密码,而要求创建应用专用密码或 SMTP 凭据。
一个可直接改造的完整脚本
下面的脚本只使用 Python 标准库,支持纯文本与 HTML 双版本正文,并可选择添加附件。运行前需要把 SMTP 参数写入环境变量。
#!/usr/bin/env python3
import mimetypes
import os
import ssl
from email.message import EmailMessage
from pathlib import Path
from smtplib import SMTP_SSL
def build_message(
sender: str,
recipient: str,
recipient_name: str,
attachment: Path | None = None,
) -> EmailMessage:
message = EmailMessage()
message["From"] = sender
message["To"] = recipient
message["Subject"] = f"{recipient_name},这是你的月度报告"
text_body = f"""你好,{recipient_name}:
你的月度报告已经生成,请登录系统查看详细数据。
此致
数据团队
"""
message.set_content(text_body)
html_body = f"""\
<!doctype html>
<html lang="zh-CN">
<body>
<p>你好,<strong>{recipient_name}</strong>:</p>
<p>你的月度报告已经生成,请登录系统查看详细数据。</p>
<p>此致<br>数据团队</p>
</body>
</html>
"""
message.add_alternative(html_body, subtype="html")
if attachment is not None:
mime_type, _ = mimetypes.guess_type(attachment.name)
if mime_type is None:
main_type, sub_type = "application", "octet-stream"
else:
main_type, sub_type = mime_type.split("/", 1)
with attachment.open("rb") as file:
message.add_attachment(
file.read(),
maintype=main_type,
subtype=sub_type,
filename=attachment.name,
)
return message
def main() -> None:
host = os.environ["SMTP_HOST"]
port = int(os.environ.get("SMTP_PORT", "465"))
username = os.environ["SMTP_USERNAME"]
password = os.environ["SMTP_PASSWORD"]
sender = os.environ.get("SMTP_FROM", username)
attachment_path = os.environ.get("MAIL_ATTACHMENT")
attachment = Path(attachment_path) if attachment_path else None
if attachment is not None and not attachment.is_file():
raise FileNotFoundError(f"附件不存在:{attachment}")
message = build_message(
sender=sender,
recipient="alice@example.com",
recipient_name="Alice",
attachment=attachment,
)
tls_context = ssl.create_default_context()
with SMTP_SSL(host, port, context=tls_context, timeout=30) as smtp:
smtp.login(username, password)
smtp.send_message(message)
print("邮件发送完成")
if __name__ == "__main__":
main()
保存为 send_email.py 后,可以这样配置并运行:
export SMTP_HOST="smtp.example.com"
export SMTP_PORT="465"
export SMTP_USERNAME="mailer@example.com"
export SMTP_PASSWORD="replace-with-an-app-password"
export SMTP_FROM="Reports <mailer@example.com>"
export MAIL_ATTACHMENT="./monthly-report.pdf"
python3 send_email.py
需要修改的关键位置包括 SMTP 主机、账户、应用密码、收件人地址和附件路径。不要把真实密码直接写进脚本或提交到 Git 仓库。
如果服务商要求 STARTTLS,可以把连接部分替换为:
import smtplib
import ssl
context = ssl.create_default_context()
with smtplib.SMTP(host, 587, timeout=30) as smtp:
smtp.ehlo()
smtp.starttls(context=context)
smtp.ehlo()
smtp.login(username, password)
smtp.send_message(message)
HTML 与附件为什么要这样组织
set_content() 设置纯文本正文,add_alternative(..., subtype="html") 添加 HTML 版本。支持 HTML 的客户端会优先展示 HTML,不支持或禁用 HTML 的客户端仍然可以读取纯文本内容。
邮件 HTML 与普通网页不同。很多客户端会过滤 JavaScript、外部资源和复杂 CSS,因此应尽量使用简单结构,并为重要信息保留纯文本版本。用户姓名等动态内容如果来自外部输入,还应通过 html.escape() 转义后再插入 HTML,避免生成损坏或带有意外标签的正文。
附件不能只读取字节后随意塞进邮件。mimetypes.guess_type() 会根据文件名推断 application/pdf、image/png 等媒体类型;无法识别时,则回退到通用的 application/octet-stream。同时要限制附件大小,因为邮件服务商通常设置单封邮件的体积上限。
批量个性化:一位收件人一封邮件
批量发送时,不要把所有地址放进 To 字段,否则收件人可能看到彼此的邮箱地址。更稳妥的方式是复用一个 SMTP 连接,但为每位收件人单独构造并发送消息:
recipients = [
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"},
]
with SMTP_SSL(host, port, context=tls_context, timeout=30) as smtp:
smtp.login(username, password)
for recipient in recipients:
message = build_message(
sender=sender,
recipient=recipient["email"],
recipient_name=recipient["name"],
)
smtp.send_message(message)
这段方式适合规模较小、发送频率有限的通知任务。进入生产环境后,还需要补充速率限制、失败重试、发送日志和退订机制。不要在捕获异常后无限重试:认证失败、地址永久无效等错误不会因为重试而消失,而临时网络错误可以采用带上限的指数退避。
上线前检查清单
- 使用环境变量、密钥管理服务或部署平台的 Secret 保存 SMTP 凭据。
- 确认服务商要求的是 465 + SSL,还是 587 + STARTTLS。
- 同时提供纯文本和 HTML 正文,并在多个邮件客户端中测试显示效果。
- 对姓名等动态 HTML 数据进行转义,不允许用户输入进入邮件头字段。
- 为附件设置大小限制,并校验文件是否存在、类型是否符合业务预期。
- 批量邮件逐个发送,记录每个收件人的成功或失败状态。
- 配置 SPF、DKIM 和 DMARC,减少合法邮件进入垃圾箱的概率。
- 营销或订阅类邮件应提供退订入口,并遵守适用的隐私与反垃圾邮件规则。
smtplib 很适合内部工具、定时报告和低到中等规模的事务通知。当发送量扩大,或者系统需要投递状态、退信处理、模板管理和统计分析时,应考虑使用专业邮件服务商的 API,并把发送工作放入任务队列,避免邮件服务器延迟阻塞业务请求。