指南 · SMTP Python
产品团队应如何在 Python 中安全地实现 SMTP?
在 Python 中应从经过授权的后台 worker 实现 SMTP 发信,而不是直接在 Web 请求中发送。使用 EmailMessage 构建邮件;需要隐式 TLS 时使用 SMTP_SSL,当服务商当前的接口约定要求时则通过 STARTTLS 显式升级;使用服务端机密信息进行身份验证,并在有上限的超时设置下调用 send_message。在连接之前持久化任务,记录收件人级别的拒收证据,对不确定的断开连接进行对账,并区分 SMTP 受理与之后的投递和进入收件箱。
在 SMTP 之前完成授权并持久化发送任务
从一个合法的应用事件开始,例如收据、安全告警、用户请求的验证或账户通知。对调用方进行身份验证,并对租户、邮件类别、可见 From 身份、收件人和模板版本进行授权。在打开任何 SMTP 连接之前,写入一条带有稳定业务事件键的持久化出站任务。该键应防止两个 worker 各自创建同一封逻辑邮件。浏览器输入不得决定 SMTP 主机、端口、用户名、信封发件人、任意收件人、邮件头或 TLS 策略。这些值应保存在经过审核的服务端配置中。队列 worker 应认领一个任务,在发送时重新检查抑制列表和授权,记录每次尝试,并通过明确的状态释放或完成该任务。Python 的 SMTP 库只负责传输准备好的邮件;它不提供租户授权、用户同意、幂等性或抑制策略。
使用 EmailMessage 构建邮件
使用 email.message.EmailMessage,而不是拼接原始的邮件头和正文。按照应用经批准的模型设置 From、To、Subject、Date 和生成的 Message-ID,然后用 set_content 设置纯文本内容,需要时用 add_alternative 添加 HTML。校验地址对象,限制收件人和附件数量,拒绝值中的换行符注入,并根据输出上下文对模板数据进行转义。从同一个不可变的模板版本同时生成纯文本和 HTML。不要在主题、自定义邮件头、文件名、诊断字段和日志中放置机密信息和不必要的个人数据。有意识地将可见的 From 邮件头与 SMTP 信封发件人分开,因为身份验证和退信处理可能依赖不同的身份。保存内容版本或不含隐私信息的哈希用于审计,而不是在没有明确需要的情况下保留完整的邮件正文。
明确选择隐式 TLS 或 STARTTLS
Python 文档说明,SMTP_SSL 用于从一开始就加密的连接,SMTP.starttls 用于升级已建立的连接。请遵循服务商当前的主机名、端口、证书和提交接口约定,而不要根据通用的端口列表去猜测。创建一个经过验证的默认 SSL 上下文,不要禁用证书或主机名检查。对于 STARTTLS,先连接,按需发送 EHLO,使用该上下文调用 starttls,然后再次发送 EHLO,因为升级后服务器公布的扩展可能会发生变化。绝不要通过明文连接发送凭据或客户邮件内容。RFC 8314 推荐使用受 TLS 保护的提交方式,并弃用明文访问。将证书失败、主机名不匹配、缺少必需的 STARTTLS 或意外的能力变化视为需要调查的硬性失败,而不是悄悄回退。
将 SMTP 凭据限制在狭窄的机密边界内
在运行时从托管的服务端机密管理设施加载用户名和密码或令牌。不要将凭据放在源代码、客户端打包文件、环境变量转储、URL、异常堆栈、分析系统、notebook、截图、提示词或已提交的测试夹具中。将每个凭据的作用域限定在服务商支持的最小环境和工作负载内,并将开发环境与生产环境分开。只有在建立了所需的 TLS 状态之后才进行身份验证。使用受控收件人演练轮换:通过经批准的管理流程开通替换凭据,更新 worker,确认身份验证和完整的事件生命周期,然后吊销旧值。反复出现身份验证失败时,应暂停受影响的路由,而不是触发快速重试循环。Python 的 login 方法会在服务器公布的机制之间进行协商,但服务商实际使用的机制、账户策略、令牌权限和轮换行为需要最新的证据来确认。
使用有边界的 Python 发送函数
保持服务商适配器小巧,并将结构化的证据返回给任务状态机。典型流程如下:创建 SSL 上下文,对于隐式 TLS,以 smtp 为名打开 SMTP_SSL(host, port, timeout=10),调用 smtp.login(username, secret),然后调用 smtp.send_message(message, from_addr=envelope_from, to_addrs=recipients)。对于要求显式升级的服务商,使用带超时的 SMTP,依次调用 ehlo、starttls(context=context)、ehlo,然后再 login。不要把示例中的主机名或端口当作通用默认值。传入规范化后的收件人列表,而不要依赖对不可信邮件头的解析。在可用时记录异常类型、SMTP 回复码和有长度限制的诊断文本,但要脱敏地址、凭据和邮件内容。分别测量连接、TLS、身份验证、信封、数据和退出各阶段,以便运营故障始终可诊断。
准确解读 send_message 的收件人结果
Python 文档说明,当邮件至少被一个收件人接受时,sendmail 和 send_message 会正常返回,并返回一个包含被拒收件人的字典;空字典表示在该阶段没有收件人被拒绝。请保留这一收件人级别的结果,而不是把整个任务标记为已送达。如果所有收件人都被拒绝,该库会抛出 SMTPRecipientsRefused 异常。其他异常分别对应发件人被拒、DATA 被拒、身份验证、连接、协议及相关错误。将确切的证据映射到应用状态:已被提交服务器受理、被永久拒绝、被暂时拒绝或未知。正常返回只能证明该范围内的 SMTP 提交结果。它不能证明目标服务器已接收、邮件最终所在的邮箱位置、已被阅读或产生了互动。之后的投递状态通知或服务商事件必须另行关联。
只在重复风险受控时才重试
在安排下一次尝试之前,先对失败进行分类。地址、发件人、身份验证、策略或内容方面的永久性失败通常需要修正或加入抑制列表,而不是自动重复。暂时性的 4xx 回复可以使用指数退避、抖动、尝试次数上限、过期时间和按目标划分的预算进行重试。在发送邮件数据之后出现的连接重置或超时可能结果不确定:服务器可能已经接收了邮件,而客户端错过了最终回复。将这次尝试标记为未知,通过不含隐私信息的关联方式检查服务商活动或后续事件,并避免立即盲目重发。SMTP 没有通用的应用层幂等键。持久化的业务事件键可以防止应用层的并发尝试,但无法强制远程 SMTP 服务器对两次都被受理的提交进行去重。对反复出现的不确定结果进行升级处理,并保留用于决策的确切证据。
处理部分收件人和抑制列表
当一封邮件有多个收件人时,SMTP 可能接受其中一部分而拒绝另一部分。保存每个收件人的回复,只将被接受的那部分推进到下一个状态。不要仅仅因为某个地址遇到暂时性拒收,就把整个原始列表重新发送一遍。在每次尝试(包括重试)之前,应用永久性退信、投诉、退订、法律、租户和管理员层面的抑制规则。只能通过明确的成文策略来区分邮件类别;把邮件标记为事务性邮件,并不能抹去收件人安全要求或服务商的限制。当隐私和个性化状态值得付出成本时,敏感工作流优先使用单收件人任务。避免通过 To 或 Cc 暴露收件人列表,也绝不要把 Bcc 行为当作授权的替代品。限制并脱敏诊断文本,因为 SMTP 回复中可能包含收件人地址或收件方特有的细节。
使用受控系统测试失败路径
测试以下内容:邮件构建、Unicode、纯文本与 HTML 两种版本、附件、邮件头拒绝、收件人规范化、TLS 验证、缺少 STARTTLS、无效凭据、发件人被拒、单个及全部收件人被拒、DATA 被拒、可能已受理之前和之后的超时、断开连接、速率响应、重试过期、重复 worker、抑制状态变化以及机密信息轮换。使用受控的测试 SMTP 服务或本地模拟服务进行确定性的单元测试和集成测试;绝不要让较低环境的意外流量发往客户地址。在生产环境的金丝雀测试中,使用经过授权的收件人,并检查原始邮件头中的可见 From、信封路径、Message-ID、DKIM、SPF、DMARC 对齐以及服务商证据。确认日志和指标中没有泄露凭据或邮件正文。如果 worker 能绕过租户授权、降级 TLS、无上限地重试、忽略部分拒收,或者无法暂停发信路由,则不得上线。
SendHQ 的适用场景
SendHQ 是一个限定工作区作用域、用于符合预期的产品通信的邮件 API。其文档涵盖发送、已验证域名、投递事件和抑制记录。将 SendHQ 与 Python 集成时,请使用其已记录的 HTTP API。
常见问题
Python 应该使用 SMTP_SSL 还是 STARTTLS?
请使用服务商当前提交接口约定所要求的模式。SMTP_SSL 从连接建立起就进行加密;STARTTLS 则需要显式升级,并要求经过验证的 TLS 以及重新发送一次 EHLO。
send_message 正常返回能证明邮件已送达吗?
不能。它表示在该 SMTP 提交阶段至少有一个收件人被接受。目标服务器接收、邮箱中的位置和用户互动都需要之后各自范围内的证据。
send_message 返回的字典是什么意思?
它将被 SMTP 服务器拒绝的收件人映射到对应的回复证据。空字典表示在该阶段没有收件人被拒绝,而不是每封邮件都进入了收件箱。
超时后可以立即重试吗?
如果超时发生在可能已提交之后,立即重试并不安全。请将该次尝试保留为不确定状态,对照服务商或后续事件证据进行对账,并且只在有边界的重复风险策略下重新发送。
SMTP 密码应该存放在哪里?
使用托管的服务端机密管理设施,限定工作负载和环境的访问范围,审计读取行为,经过测试的轮换流程,并且不暴露给客户端、日志、提示词或测试夹具。
生产环境中可以禁用证书验证吗?
不可以。证书或主机名校验失败说明配置不安全或不正确。请停止该路由并进行诊断,而不是悄悄削弱 TLS 验证。
应如何处理部分收件人被拒?
保存每个收件人的结果,推进被接受的那部分,只重试符合条件的暂时性拒收。不要连同整个原始列表把已被接受的收件人再发送一遍。
本页能证明 SendHQ 支持 SMTP 吗?
不能。本指南泛泛介绍 Python SMTP;有关其邮件 API,请使用 SendHQ 的当前文档。
参考来源
- smtplib — SMTP 协议客户端 — Python Software Foundation
- email.message:表示电子邮件消息 — Python Software Foundation
- Python email 示例 — Python Software Foundation
- RFC 5321:简单邮件传输协议(SMTP) — RFC Editor
- RFC 4954:SMTP 身份验证服务扩展 — RFC Editor
- RFC 8314:明文已过时:将传输层安全性(TLS)用于邮件提交和访问 — RFC Editor