协议、服务器和用户视角下的电子邮件系统

电子邮件系统的一个模型

+------+                      +---+  +---+
|Sender|                      |MTA|->|MDA|
+------+                      +---+  +---+
        \           MX Lookup/            \IMAP,POP
         +------+  +--------+              +--------+
         |Mail  |  |Mail    |              |Receiver|
         |Submit|->|Transfer|              |Email   |
         |Agent |  |Agent   |              |Client  |
         +------+  +--------+              +--------+

首先在这里转述 SMTP (Simple Mail Transfter Protocol) 维基百科页面给出的电子邮件寄送全过程模型,它是一个很好的认知起点:

  1. 发件人(或称为 MUA, Mail User Agent,邮件用户代理)向邮件服务器通过 TCP 465 或 587 的端口发送发件请求。
  2. 发件人的邮件服务器的 MSA (mail submit agent,邮件提交代理) 负责接收这个发件请求,由 MTA (mail transfer agent,邮件传输代理) 负责发件:这里需要进行一个 DNS 查询,查找收件电子邮件的域名的 MX Record,从而确定邮件要寄送到哪个服务器。发件人电邮服务器的 MTA 和收件人电邮服务器的 MTA 之间完成寄送。MTA 和 MTA 之间的传输使用 TCP 25 端口。
  3. 收件人 MTA 收到邮件后,将邮件交给 MDA (Mail Delivery Agnet) 邮件递送代理。收件人可以主动访问 MDA(如登录网页邮箱、查看 MDA 服务器的磁盘等)阅读邮件;也可以在邮件客户端查看和管理自己的邮件。收件人客户端和 MDA 之间的通信协议主要包括 IMAP 和 POP。
    • POP (Post Office Protocol) 协议 / 邮局协议,最初的设计完全就是 “邮递员把信从邮局寄到个人信箱” 这个流程 – POP 客户端连接到 MDA 服务器,收取所有邮件,然后把邮件从 MDA 中删除。
    • IMAP (Internet Message Access Protocol) 也是支持本地客户端访问 MDA 上的收件箱的协议,它更好地支持多客户端的访问。

总结而言,SMTP 和 IMAP/POP 是一组相配合的经典邮件协议:SMTP 对邮件寄送全过程都有所描述,而并未规定最后一程的递送;IMAP 和 POP 主要负责处理邮件的 “最后一程”。SMTP 的客户端大多自身也是服务器,必须时刻在线准备接收传输,而 IMAP 和 POP 的客户端不必随时在线,只需发起请求,单线联系。

SMTP 协议流程

SMTP 主要包括三个命令:MAIL,RCPT 和 DATA,这些命令及对应的回复在两个主机的 SMTP client / server 对之间通过 TCP 进行。

  • MAIL 命令用来建立发件人地址。这个地址被称为 return-path(回退路径), reverse path(逆向路径)或 bounce address(当邮件 bounce,即寄送失败时回头通知的地址)
  • RCPT 命令用来建立收件人地址。可以执行多次 RCPT 命令来把信件寄送给多个收件人。MAIL 和 RCPT 两个命令的地址被 SMTP 协议视为 “信封” (envelope) 的一部分,就如同传统邮政服务中的信封包含了发件人地址和收件人地址一样。
  • DATA 命令,用来示意开始传输消息文本,信封里的内容。

SMTP 维基百科页面上有一个 Client / Server 通信序列样例,很值得参考,就不复制在此了。注意到在 DATA 命令中,消息文本内部另外有一层协议:消息文本由 header 和 body 组成,header 包括 From, To, Cc, Date, Subject 等我们电子邮件使用者所熟悉的语义。

SMTP 没有定义机制来保证 From 的地址就是 MAIL 命令使用的地址,To, Cc 的地址是 RCPT 使用的地址。例如,你可以在 header 中篡改 From,或者添加不在 RCPT 地址列表中的地址到 To 和 Cc 字段中。没有机制保证这两个层级上的 “发件身份” 和 “收件身份” 的一致性,就无法防范如前所述的欺骗行为 (Spoofing) 的发生。这种欺骗过于泛滥以至于有专有名词来描述它:电子邮件欺骗(Email Spoofing)。

防欺骗:发件人身份认证方法

在邮件欺骗中,篡改 From 字段假装成另一个发件人显然危害巨大;而篡改 To 字段,至多让收件人误以为某个其它人也收到了这封信(实际上没有)、没有其他人收到这封信(实际上有)、以及收到并不是寄给自己的信,造成的困扰有限——即使没有欺骗,邮件系统中的错误和丢包也会造成这些现象的发生。因此首先考虑对篡改 From 字段的防范,即对发件人进行身份认证。

SPF

一种方法是限制发件人邮件服务器(具体而言是 MTA)的 IP 地址段或者域名,被称为发件人策略框架(Sender Policy Framework, SPF)。收件服务器在 SMTP 接收信件的过程中,检查 From 地址的域名,对该域名进行一次 DNS 查询,寻找其中含有 SPF 信息的 TXT 字段,从而获知哪些 IP 地址段和域名被允许以该域名的名义发信。

DKIM

另一种方法由发件人生成非对称密钥,将公钥发布到 DNS record 中,用私钥对邮件 header 及 body 签名,保证邮件内容没有被篡改以及确实源自发件人。收件人可以依据发件人域名 DNS 查询获取到该域名的公钥,从而验证签名。这种策略被称为域名密钥识别邮件(DomainKeys Identified Mail, DKIM)。

更进一步的方法如 DMARC 就不在此讨论了。

Internet 上实际运行的电子邮件系统

前文所述的协议并不是一个全面的规定。对于真实世界中运行的电子邮件系统(即互联网上实际运行的邮件系统),需要敲定诸多重要细节,特别是关乎信任的认证和声誉。注:本文将 MX(Mail Exchange)和 MTA (Mail Transfer Agent) 模糊处理为同等的概念,并未做出区分。

邮件提交代理 MSA:接收谁的发件

SMTP 规定了 MSA 从发件人那里接收待发的电子邮件,但并没有规定谁可以利用 MSA 发件。现实中,必然有某种隔离或者身份认证过程,决定谁能向 MSA 递交信件,最典型的就是 SMTP AUTH,一个 SMTP 扩展(顺带一提,RFC 1869 定义了向 SMTP 添加扩展的方式)。实操中,电子邮件通常提供商会提供给你一个 SMTP 服务器地址和 “SMTP Password”;你在电邮客户端中填写 SMTP 服务器地址、你的邮件地址、以及 SMTP Password,从而发件并验证你的身份。你可以在 Python 中(或命令行内)模拟整个邮件提交过程,见附录 1。

MSA 通常会审查 message body 中的 From 域、RCPT FROM 和认证过程中发件人身份相一致,从源头阻止欺诈。

邮件传输代理 MTA:接受谁的连接请求

SMTP 并没有规定 MTA 要在 TCP 25 端口上接受哪些主机的连接请求。实际在运营中的 MTA 通常会基于声誉接受连接请求,在此基础上,通过 SPF + DKIM 进一步验证发送方身份,防止垃圾邮件。

作为 Client 发起 SMTP 连接的 MTA 必须有足够好的 “声誉”,连接才会被接受。对于新出现的 MTA 和有不良历史的 MTA Client,主流邮件服务器运营商(如 Gmail、Microsoft 等)的 MTA 会拒绝连接,或者假意执行协议,但暗中将邮件标记,并不实际寄送到用户收件箱(silent discard)。

架设邮件服务器的现实困难

假设你有域名,想要在 VPS 上或者利用 ISP 架设了一个为该域名服务的 MTA,实际效果怎样?

收件方面不该有问题:架设过程中,你理应将这个 MTA 主机地址写入你域名的 MX record 中,这样其它 MTA 在收到 RCPT TO 你域名内的邮箱地址时,会循着 MX record 找到你的 MTA 主机地址,发起连接并将信寄送到。

发件方面,向位于主流电邮运营商的目标地址发件要困难很多。主流电邮运营商通常要求 SPF + DKIM 认证,因此你需要将服务器地址按照 SPF 要求写入你域名的 TXT Record 中,并且按照 DKIM 要求,在服务器上安装私钥负责消息签名,将公钥 TXT Record 中。即使满足这些要求,MTA 主机的发信传输也会被对端拒绝或者丢弃。如果目标地址的邮件运营者并未实施这些基于声誉的限制,或者和你有协商的余地,则应该可以成功发件。

发件方面有一个额外且致命的限制:大多数 VPS 和 ISP 都默认封禁目标端口 TCP 25 的连接,且一般难以协商解禁。ISP 这样做是为了防止网络接入者发送大量垃圾邮件,损害 ISP 拥有的 IP 地址段的声誉。这样一来,简单自行架设的 MTA 无法发起 SMTP 连接请求,彻底无法发信了。

为域名提供可发件邮件服务器的选择

出于前一节所述的出(ISP 端口 25 封禁)、入(主流电邮提供商依据声誉的丢弃系统)限制,如果拥有域名,想要使用该域名的邮箱,可以寻求第三方服务提供商(如 Mailgun, etc)的帮助。第三方会提供未被端口屏蔽的网络连接和被主流邮件运营商认为声誉良好的 MTA。

依据前面的分析,在此过程中,你必然会被要求将第三方 MTA 的域名地址作为 SPF 发布到 DNS Record;第三方会生成非对称密钥,私钥留在第三方 MTA 上,而公钥由你作为 DKIM 发布到 DNS Record。

日常使用电子邮件的技巧

了解了电子邮件系统的组成之后,你会发现一些日常电子邮件的使用技巧。

区分 RCPT TO 和 Message Body to:

前文有所叙述,SMTP 协议中的 RCPT To 和 Message Body To 是两个协议层上的收件地址:前者就像传统邮政信封上的收件人地址,用于信件传递;后者就像信封里面、信纸上面的收件人,更多地传达语义信息(这封信写给谁)。Return-Path 和 from 同理。

很多邮件提供商允许你查看你的 MTA 接收此邮件时的通讯信息,从而让你获知更多关于该邮件的元数据。以当下(2025年10月)的 Gmail 为例,具体操作是邮件详情页中点击右上角的 “…”,选择 “Show Original” 查看原始消息。

  • Delivered-To 就是 RCPT TO,Return-Path 是 FROM。它们依次代表 “信封” 上的收件地址和发件地址,即这封邮件在 SMTP 邮递系统中源自那里、终到哪里。
  • 在 Message Body 中,From 和 To 等信息就代表 “信纸” 上的自某某、致某某,是人与人通信语义这个层次上的。

邮件列表(Mailing List)

邮件列表在传统意义上是小型的网络社区和讨论组,常见于学校、机构内部。一个 邮件列表是诸多邮件地址的集合,其自身也是一个地址 list@example.com。随便举例,ml-theory-seminar@seas.harvard.edulinux-kernel@vger.kernel.org 都是传统意义上的邮件列表。发送到 list@example.com 的邮件传统意义上会发送给所有成员。

以 Gmail 为例,在 groups.google.com 中可以查看和管理你加入的邮件列表:发送给邮件列表的邮件会以你指定的频率进入你的收件箱。

你会发现,来自邮件列表的邮件的 Message Body To 是邮件列表自身的地址 list@example.com 。而在原始消息中,Delivered-To 地址是你邮件的地址,这封邮件被 MTA 寄送到了你的信箱。

可以通过 Email Exploder 实现一个邮件列表:在收件的邮件服务器上,将所有 Message Body To 地址为 list@example.com 的地址原样转发(利用 SMTP,更改 RCPT TO)给所有成员。

CRM Mailing List

如今有很多邮件列表并非传统、社区意义上的邮件列表,更多地被当作 CRM (Customer Relationship Management System,客户关系管理系统) 被第三方使用,在此列举出来以和传统邮件列表区分。

在技术实现上,CRM Mailing List 与传统 Mailing List 完全不同:后者被一个独特的 list 邮件地址识别,成员情况存储在机构的邮件服务器中;而前者并没有识别方式,其成员情况存储在第三方、与 Email 网络不相干的数据库里。CRM Mailing List 的管理者通过数据库枚举客户 Email 地址,逐一单独发信,起到联络客户的作用。

你会发现,CRM Mailing List 的 Message Body To 通常就是你的邮件地址,而非一个 list(这个区分并不绝对)。通常,你需要点击 CRM Mailing List 发来的邮件中的 Unsubscribe 链接来告知这个第三方,将自己移除出这个邮件数据库,才能退出这个 “Mailing List”。

附录

1. 在 Python 中向 SMTP 服务器提交邮件

用 GPT5 生成了这段事实上工作的代码,个人认为它没有裸 HTTP 明文传密码之类的安全问题。如果要把风险降到最低的话,可以在如此测试之后修改密码。

import smtplib
from email.message import EmailMessage

smtp_host = <your host> # example: smtp.domain.com
smtp_port = 587
username = <your email address> # example: you@domain.com
password = <your SMTP password> # often same as email account password

msg = EmailMessage()
msg["From"] = <your email address>
msg["To"] = <recipient address>
msg["Subject"] = "Test email from Python"
msg.set_content("Hello — this is a test sent from smtplib. Testing SMTP AUTH.")

# STARTTLS example
with smtplib.SMTP(smtp_host, smtp_port) as s:
    s.ehlo()
    s.starttls()          # upgrade to TLS
    s.ehlo()
    s.login(username, password)
    s.send_message(msg)

Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *