用 Python 发送邮件:从 EmailMessage 到 SSL 服务器连接

2026-08-04 38 预计阅读时间: 1 分钟
来源: realpython.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

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/465SMTP + starttls()/587
  • 为网络连接设置超时,避免程序无限等待。
  • HTML 邮件同时提供纯文本版本。
  • 附件大小和邮件服务商的发送限制要提前确认。
  • 捕获 SMTP 异常,但不要在日志中泄露密码。
  • 将“服务器接受邮件”与“收件人最终收到邮件”视为两个不同结果。

掌握 EmailMessage 的消息构建方式,以及 smtplib 的 SSL、STARTTLS 和认证流程后,大多数 Python 邮件发送任务都可以从一个小脚本稳定扩展为应用中的通知模块。


相关推荐