Python 发送邮件的核心并不复杂:用 EmailMessage 组织邮件内容,再通过 smtplib 连接 SMTP 服务器并完成认证。真正容易出错的地方,通常集中在消息格式、SSL/TLS 连接方式、端口选择和凭据管理上。
这篇文章围绕这些关键点,整理一套可以直接改造的发送流程,也适合用来检查自己是否掌握了 Python 邮件发送的基础。
EmailMessage 负责构建邮件
不要把整封邮件拼成一段手写字符串。EmailMessage 可以明确表达发件人、收件人、主题和正文,并且能够继续扩展到 HTML、附件等内容。
from email.message import EmailMessage
message = EmailMessage()
message["From"] = "sender@example.com"
message["To"] = "recipient@example.com"
message["Subject"] = "Python 邮件测试"
message.set_content("这是一封由 Python 发送的纯文本邮件。")
print(message.as_string())
From 是邮件头中的发件人地址,不一定等同于 SMTP 登录用户名;To 是收件人地址;Subject 是主题。set_content() 会设置纯文本正文,并自动生成相应的 MIME 信息。
如果需要同时发送 HTML 和纯文本版本,可以使用 add_alternative():
message.set_content("请使用支持 HTML 的邮件客户端查看此邮件。")
message.add_alternative(
"<html><body><h1>构建完成</h1><p>这是 HTML 邮件正文。</p></body></html>",
subtype="html",
)
保留纯文本版本很有价值,因为部分客户端、自动化系统或安全策略可能不会渲染 HTML。
用 SSL 连接真实 SMTP 服务器
发送邮件时,SMTP 服务器通常需要认证。使用 smtplib.SMTP_SSL 时,连接从一开始就通过 SSL 加密,常见端口是 465。另一种方式是先连接普通 SMTP 端口,再调用 starttls() 升级到 TLS;两者不能混用。
下面的示例使用环境变量保存服务器信息和密码,避免把凭据直接写进源代码:
import os
import smtplib
from email.message import EmailMessage
smtp_host = os.environ["SMTP_HOST"]
smtp_port = int(os.environ.get("SMTP_PORT", "465"))
smtp_user = os.environ["SMTP_USER"]
smtp_password = os.environ["SMTP_PASSWORD"]
message = EmailMessage()
message["From"] = smtp_user
message["To"] = os.environ["MAIL_TO"]
message["Subject"] = "来自 Python 的测试邮件"
message.set_content("SMTP 连接和邮件发送成功。")
with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=30) as smtp:
smtp.login(smtp_user, smtp_password)
smtp.send_message(message)
print("邮件已提交给 SMTP 服务器")
运行前设置环境变量,下面的命令适用于 macOS、Linux 以及支持类似语法的终端:
export SMTP_HOST="smtp.example.com"
export SMTP_PORT="465"
export SMTP_USER="sender@example.com"
export SMTP_PASSWORD="应用专用密码"
export MAIL_TO="recipient@example.com"
python send_mail.py
实际使用时,应根据邮件服务商提供的 SMTP 主机、端口和认证要求进行替换。很多服务要求使用应用专用密码,而不是账户的网页登录密码。
465 和 587 端口如何选择
端口选择必须和连接 API 匹配:
| 场景 | Python API | 常见端口 |
|---|---|---|
| 从连接开始使用 SSL | smtplib.SMTP_SSL |
465 |
| 先连接,再通过 STARTTLS 加密 | smtplib.SMTP + starttls() |
587 |
使用 STARTTLS 时,代码可以这样写:
with smtplib.SMTP("smtp.example.com", 587, timeout=30) as smtp:
smtp.ehlo()
smtp.starttls()
smtp.ehlo()
smtp.login(smtp_user, smtp_password)
smtp.send_message(message)
连接失败时,先检查这几项:主机名是否正确、端口是否开放、服务器是否要求 SSL 或 STARTTLS、账户是否允许 SMTP 登录,以及密码是否应替换为应用专用密码。不要仅仅反复更换端口,因为加密模式和端口通常是成对配置的。
发送附件和处理失败
EmailMessage 也可以添加文件附件。示例假设当前目录存在 report.pdf:
from pathlib import Path
attachment = Path("report.pdf")
message.add_attachment(
attachment.read_bytes(),
maintype="application",
subtype="pdf",
filename=attachment.name,
)
生产代码还应考虑异常处理和日志记录,但日志中不要打印 SMTP 密码或完整的认证信息:
try:
with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=30) as smtp:
smtp.login(smtp_user, smtp_password)
smtp.send_message(message)
except (OSError, smtplib.SMTPException) as exc:
print(f"邮件发送失败:{exc}")
raise
send_message() 成功返回,只表示 SMTP 服务器接受了这次提交,不代表邮件一定已经送达收件箱。后续仍可能受到收件方策略、垃圾邮件过滤、域名信誉和 SPF、DKIM、DMARC 配置的影响。
一份实用检查清单
- 使用
EmailMessage构建邮件,而不是手工拼接 MIME 字符串。 - 从环境变量或密钥管理系统读取 SMTP 凭据。
- 明确选择
SMTP_SSL/465或SMTP+starttls()/587。 - 为网络连接设置超时,避免程序无限等待。
- HTML 邮件同时提供纯文本版本。
- 附件大小和邮件服务商的发送限制要提前确认。
- 捕获 SMTP 异常,但不要在日志中泄露密码。
- 将“服务器接受邮件”与“收件人最终收到邮件”视为两个不同结果。
掌握 EmailMessage 的消息构建方式,以及 smtplib 的 SSL、STARTTLS 和认证流程后,大多数 Python 邮件发送任务都可以从一个小脚本稳定扩展为应用中的通知模块。