电子邮件基础:投递协议与邮件结构

Alice 要把一份报告交给 Bob 处理。她把 Bob 填在「收件人」里,抄送 Carol 让她知道这件事,再把审计邮箱填进「密送」,留一份归档。Bob 收到后,可以看到 Alice 和 Carol 的地址,也能打开报告附件。审计邮箱同样收到一份,但在 Bob 看到的收件列表里没有显示。

Alice 还给这封信设了一个客服地址。Bob 点「回复」时,客户端会把回复发给客服。如果审计邮箱投递失败,发件系统又会用另一个地址接收失败通知。要解释这些地址各自是怎么用的,先看写好的邮件里有什么,再看服务器传递它时还需要哪些信息。

邮件消息与 SMTP 信封

写信窗口里的作者、收件人和主题会被写进邮件头,正文和附件放在邮件体。这两部分合在一起就是这封邮件的消息内容,收件客户端从这里读取主题、显示发件人和打开附件。Alice 这封信的部分头字段是这样写的。

From: Alice <[email protected]>
To: Bob <[email protected]>
Cc: Carol <[email protected]>
Reply-To: [email protected]

From 是作者字段。上面的 Alice 是显示名,[email protected] 是邮箱地址。客户端可能在列表里只显示名字,展开详情才显示邮箱。工程上常称这个字段为 Header From,方便和稍后要用的信封发件人区分。To 和 Cc 是这份副本里公开的收件人,Reply-To 给出了建议的回复目标。

发件系统还得告诉接收系统把这份内容送到哪几个邮箱、出错时把通知发回哪里。传送邮件用的协议叫 SMTP(Simple Mail Transfer Protocol)。它把投递地址放在单独的信封(envelope)里。SMTP 的 MAIL FROM 指定反向路径(reverse-path),也常称 Envelope From,这里用发件平台的退信地址。RCPT TO 每发一次,就声明一个信封收件地址,规范称为正向路径(forward-path)。Bob、Carol 和审计邮箱都在本次信封里,接收服务器按这三个地址处理投递。

在哪里读取 本例的值 用于哪一步
信封的 MAIL FROM [email protected] 发送方用来接收投递失败通知。
信封的 RCPT TO [email protected]、[email protected]、[email protected] 服务器要投递的三个邮箱。
消息的 From: Alice <[email protected]> Bob 阅读邮件时看到的作者信息。
消息的 To: 和 Cc: Bob、Carol Bob 看到的公开收件列表。这个副本里没有审计邮箱的地址。
消息的 Reply-To: [email protected] Bob 点「回复」时,客户端用它选择回复目标。

最后负责投递的 SMTP 服务器会把当时看到的反向路径写到 Return-Path:。这一路沿用 [email protected] 时,Bob 收到的邮件就带有 Return-Path: <[email protected]>。转投服务可以改写反向路径,每一跳收到的 MAIL FROM 和 RCPT TO 都可以从该次 SMTP 会话记录中查到。

邮件从发出到读到

邮件系统的角色

Alice 使用的客户端叫 MUA(Message User Agent,邮件用户代理)。它可以是桌面或手机应用,也可以由 Webmail 的浏览器界面和后端共同完成。客户端把邮件交给 MSA(Message Submission Agent,邮件提交代理),MSA 在提交阶段检查用户是否有权发信,并将接受的邮件交给发件服务器。

服务器之间传递邮件的是 MTA(Message Transfer Agent,邮件传输代理)。发件 MTA 为信封收件地址选择下一跳,收件方的边界网关可能再把邮件交给内部 MTA。到达负责最终投递的系统后,由 MDA(Mail Delivery Agent,邮件投递代理)把邮件放进目标用户的邮件存储。Bob 打开客户端时,再通过邮箱访问协议或服务的接口取信。同一套软件可以承担多个角色,例如入口网关同时接收外部邮件和处理本地用户提交。

SMTP 邮件提交、跨域传递、本地投递和 IMAP / POP3 邮箱访问

SMTP 采用存储转发的方式。下一跳暂时不可达时,负责当前投递的服务器可以先排队,再重试。一次用户提交也可能被拆成多次投递,例如同时发给两个域,或同一封信中只有部分收件人在本次连接中被接受。

DNS 与收件服务器

假设本次跨域投递的信封收件人为 [email protected]。发件 MTA 用 receiver.example 查 DNS 的 MX 记录,从中取得接收邮件的主机名,再解析该主机名的 A 或 AAAA 记录并连接。下面是示意记录,地址使用文档保留网段。

receiver.example.      IN MX 10 mx1.receiver.example.
receiver.example.      IN MX 20 mx2.receiver.example.
mx1.receiver.example.  IN A     192.0.2.25
mx2.receiver.example.  IN A     192.0.2.26

这里的 10 和 20 是 MX 的优先值,越小越优先。发件服务器先选 mx1.receiver.example,将它解析为 192.0.2.25,连接该地址的 TCP 25。如果连接不上,可以试 mx2.receiver.example 的 192.0.2.26。要是两个目标都暂时不可达,邮件可以留在发件服务器的队列里,之后再试。

如果 receiver.example 存在,但根本没有 MX 记录,SMTP 按隐式 MX 规则查 receiver.example 自己的 A 或 AAAA。已经发布 MX 的域,按那些 MX 给出的主机尝试。域还可以用 MX 0 . 明确表示不接收邮件,这叫 Null MX。使用出站网关的单位通常先把信交给指定网关,再由网关查目标域的 MX。

PTR 反向解析与正向回查

邮件连接建立后,收件服务器已经看到了来信的对端 IP 198.51.100.27。如果想查这个 IP 在 DNS 中对应的主机名,可以做反向 DNS 查询(reverse DNS,rDNS)。IPv4 的查询名把四段地址倒过来,再接上 in-addr.arpa.,查记录类型 PTR。这里假设 DNS 中配置了下面两条记录。

27.100.51.198.in-addr.arpa.  IN PTR  smtp.sender.example.
smtp.sender.example.        IN A    198.51.100.27

先查第一行,会得到 smtp.sender.example。再用这个主机名查第二行的 A 记录,发现返回的地址中包含原来的 198.51.100.27,两次查询便对上了。这种检查叫正向确认的反向 DNS(Forward-Confirmed Reverse DNS,FCrDNS)。一个名字可以有多个地址,回查结果包含最初的 IP 即可。IPv6 则把地址展开为 32 个十六进制位,将每一位倒序并用点分隔,接上 ip6.arpa. 查询 PTR,再通过 AAAA 回查。

这两条记录分别在正向和反向的 DNS 区域中管理。A 或 AAAA 通常由发件方的域名管理员配置。PTR 的管理权跟着公网 IP 地址段走。假如 Alice 的团队租了一台云主机,团队可以在 sender.example 的 DNS 中加 A 记录,PTR 则通常在云服务商的控制台设置,或者由服务商把相应的反向 DNS 区委托给团队管理。

发件服务器查 MX 找到收件服务器。收件服务器查来信 IP 的 PTR,可以把查到的主机名写进接收日志。发件服务器还会通过 EHLO smtp.sender.example 声明自己的名字。这三项在本例中分别是收件 MX mx1.receiver.example、来信 IP 的反查结果 smtp.sender.example,以及会话中的 EHLO 名称。收件服务器可以把 PTR 缺失或回查地址不匹配用作接收策略的判断条件。

SMTP、IMAP、POP3 与 Webmail

SMTP(Simple Mail Transfer Protocol)负责提交和转交邮件。IMAP(Internet Message Access Protocol)与 POP3(Post Office Protocol Version 3)负责访问已经投递的邮件。提交、转交和取信分别由不同的角色发起连接。

连接用途 协议与常用 TCP 端口 连接的两端
邮件提交 SMTP Submission,587 常用 STARTTLS,465 使用隐式 TLS 发件客户端 -> 提交服务器,常用 SMTP AUTH 确认提交身份
服务器间传递 SMTP,25,可通过 STARTTLS 升级 发件 MTA -> 下一跳 MTA。收件方通常按接收域与接收策略接受外来邮件
访问并同步邮箱 IMAP,143 可用 STARTTLS,993 使用隐式 TLS 收件客户端 -> 邮箱服务器,可获取邮件、同步标记及操作文件夹
取回邮件 POP3,110 可用 STLS,995 使用隐式 TLS 收件客户端 -> 邮箱服务器,主要从服务器的 maildrop 取回消息
网页邮箱 HTTPS,通常 443 浏览器 -> 邮件服务的 Web 界面。后端负责提交、存储及访问

例如 Bob 用网页邮箱发信时,浏览器通过 HTTPS 把操作交给邮件服务的后端。后端负责提交,向其他域投递时仍可以走 SMTP。Bob 在手机上读取同一个账号时,可以走 IMAP、POP3 或该平台的专用接口。信进了邮箱之后,客户端怎样跟上服务器上的变化,见后面的邮箱同步一节。

SMTP 会话与连接加密

连接、信封与消息内容

前面的三个收件地址都属于 receiver.example。假设发件 MTA 已选中 mx1.receiver.example,就可以在一条 SMTP 连接里依次声明三个收件人,再发送一次消息内容。SMTP 用 DATA 命令请求进入发送消息内容的阶段。服务器答 354 后,接下来发送的才是邮件头、空行和邮件体,直到发送端用单独一行 . 表示结束。From:、To:、Cc: 都在这个阶段作为消息内容送过去。

下面用 C: 标出发件 MTA 发出的行,S: 标出接收服务器的应答。应答中的 <CRLF> 是 SMTP 使用的行尾,由回车和换行两个字符组成。DATA 之后列出几行头字段,其余部分在后面「带附件的原始邮件」中完整展开。

S: 220 mx1.receiver.example ESMTP
C: EHLO smtp.sender.example
S: 250-mx1.receiver.example
S: 250-SIZE 26214400
S: 250 ENHANCEDSTATUSCODES
C: MAIL FROM:<[email protected]>
S: 250 2.1.0 Ok
C: RCPT TO:<[email protected]>
S: 250 2.1.5 Ok
C: RCPT TO:<[email protected]>
S: 250 2.1.5 Ok
C: RCPT TO:<[email protected]>
S: 250 2.1.5 Ok
C: DATA
S: 354 End data with <CRLF>.<CRLF>
C: From: Alice <[email protected]>
C: To: Bob <[email protected]>
C: Cc: Carol <[email protected]>
C: [其余邮件头、空行和邮件体]
C: .
S: 250 2.0.0 Accepted for delivery
C: QUIT
S: 221 2.0.0 Bye

EHLO 声明客户端名称,并让服务端列出本次连接支持的 SMTP 扩展,这套扩展机制也叫 ESMTP。SIZE 26214400 表示该节点公布的邮件大小上限为 25 MiB,计算时包含头字段、编码后的正文和换行。

消息里也可能原本就有一行只写了一个 .,例如纯文本里列出的符号。为了区分正文和结束标志,发送端会给每个以 . 开头的内容行再加一个点。原文的 . 在传输中变成 ..,接收端读到后去掉多出来的第一个点。这个规则叫点填充(dot-stuffing)。结束用的点和为了传输而加的点都不会留在保存下来的邮件里,也不计入 SIZE。

RCPT TO 后的 250 只接受当前这个收件地址。结束 DATA 后的 250 则表示接收服务器已经为这次接受的地址接下投递或转交责任。它还可能排队处理,然后才把副本放进各自的邮箱。

从一条 MAIL FROM 到整份消息的最终应答,构成一次邮件事务(mail transaction)。如果发件服务器还要交给同一个 MX 另一封信,可以在现有连接里重新发一条 MAIL FROM,从新信封开始。RSET 可以放弃当前事务并清空本次的地址和内容状态,连接保持打开。示例中的 QUIT 则结束整个连接。

传输加密

同样是连接提交服务器,587 和 465 的开头顺序不一样。Alice 用 587 提交时,通常先读到服务器的 SMTP 220 问候,再用 EHLO 查看是否支持 STARTTLS。她的客户端发出 STARTTLS 并收到允许升级的 220 后,才开始 TLS 握手。成功后重新发 EHLO,再根据加密会话中公布的能力登录并提交。改用 465 的隐式 TLS 时,连接建立后第一步就是 TLS 握手,SMTP 的 220 问候在加密通道里才会出现。

TLS 保护的是某一条连接。邮件从发件客户端到提交服务器、再从一台 MTA 到另一台 MTA,可以经历多次独立的 TLS 握手。跨域的 25 端口上常见机会式 TLS。如果发送方的策略允许回退,无法建立 TLS 时仍可能使用明文传递。收件域可以用 MTA-STS 或 DANE 为发件 MTA 提供更明确的连接策略。

以 MTA-STS 为例。receiver.example 在 _mta-sts.receiver.example 发布 v=STSv1; id=2026111001 的 TXT,id 用来提示策略版本变化。实际策略通过 https://mta-sts.receiver.example/.well-known/mta-sts.txt 提供,包含 mode、允许的 mx 名称或模式和 max_age 缓存时间。若策略为 enforce,支持 MTA-STS 的发件 MTA 会核对候选 MX 是否在策略范围内,并验证 TLS 证书。当前候选无法满足时可以尝试其他符合条件的 MX,找不到可用目标就不能把邮件改走明文交付。

DANE for SMTP 使用 DNSSEC 验证的 TLSA 记录提供服务器认证依据。若实际连接目标是 mx1.receiver.example 的 TCP 25,查询对应 _25._tcp.mx1.receiver.example。记录绑定的是该 SMTP 服务端的证书或公钥材料,具体比较方式由 TLSA 字段指定。这里验证的是邮件经过的服务器连接,消息内写出的作者域还要经过独立的发信域检查。

收件地址、抄送与密送

Alice 的报告交给 Bob 处理,抄送 Carol,再给审计邮箱留一份。发件端从「收件人」「抄送」「密送」三个栏目收集投递地址。给 Bob 和 Carol 的副本会显示公开的收件人和抄送人。

写信时填写的位置 本例的地址 其他收件人看到的内容
收件人 To: [email protected] 通常随消息公开,表示这封信主要写给 Bob。
抄送 Cc:(carbon copy) [email protected] 通常也随消息公开。Carol 收到同样的正文和附件,Cc 主要表达这份副本供她知悉。
密送 Bcc:(blind carbon copy) [email protected] 审计邮箱也收到副本。给 Bob 和 Carol 的副本不会列出这次填写的密送地址。

如果只是把 Carol 从「抄送」移到「收件人」,她通常仍收到相同内容。变化的是邮件头中她归在哪一栏,以及读者对她角色的理解。Alice 可以把两个人都填进 To:,也可以只填 Carol 的 Cc:,让 To: 留空。服务器给 To: 或 Cc: 的地址投递时,用的都是 RCPT TO,To: 本身没有传输优先级。把 Carol 移到「密送」,才会改变公开副本里能否看到她的地址。

一封消息可以投递给多个邮箱

只发给 Bob 时,信封只有一条 RCPT TO。上面的完整会话加了 Carol 和审计邮箱,便有三条 RCPT TO。接收服务器同意接收后,发件端只发送一次消息内容。三个地址各有自己的投递结果,消息头中的 To: 和 Cc: 则留在这次交过去的消息里。

一次点击「发送」,可以产生不止一次 SMTP 事务。比如 Carol 改用 [email protected],发件服务器若直接向两个域投递,通常会分别查找 receiver.example 和 partner.example 的接收服务器,再为各自地址发起事务。如果三个地址先交给同一个出站中继,可以先合在一笔事务里,由中继再分流。即使收件人都在同一域,逐人生成密送副本、按人定制正文或分批投递时,也可能分别发送。下一笔事务要重新发 MAIL FROM 和 RCPT TO,但可以复用尚未关闭的 SMTP 连接。区分这些情况时,一次事务的 RCPT TO 列表回答「这次交给谁」,这次传送的消息头回答「这些人会看到什么」。

密送地址如何从副本里隐藏

上例中,审计邮箱能收到报告,是因为它出现在信封里。发件端还要决定在每份副本里保留哪些头字段。Internet Message Format 描述了三种密送副本形式。

  1. 删掉所有副本的 Bcc:。 Bob、Carol 和审计邮箱都收到不含该字段的副本。仅看其中任意一份,通常读不出密送地址。
  2. 为密送对象另做副本。 Bob 和 Carol 的副本仍去掉 Bcc:,审计邮箱的副本可以保留 Bcc: [email protected]。同时密送给多个人时,发件端也可以为每个人生成仅列出其自身地址的副本。若把完整密送名单留在共用的密送副本中,拿到那份副本的密送人便能看到名单中的其他地址。
  3. 留下一个空的 Bcc:。 邮件头有 Bcc: 这一行,冒号后没有地址。读者可以知道发生了密送,具体地址仍由信封传递。

只填密送栏也能发信。客户端可以不写 To:,也可以写 To: undisclosed-recipients:;,用一个空地址组给界面显示。真正收到邮件的邮箱仍在 RCPT TO 中。密送控制的是公开副本的可见性。发件端、负责投递的服务器以及保留了相应信封日志的系统,仍可能知道哪些地址收到了交接。

回复与全部回复会重新确定收件人

Bob 点「回复」时,客户端一般把回复写给 Alice 在 Reply-To: 指定的 [email protected]。原信没有 Reply-To: 时,通常取 From: 的作者地址。点「全部回复」时,客户端通常还会从原信可见的 To: 和 Cc: 取出其他收件人。发送前,Bob 可以在写信窗口确认或修改这份名单。

例如一个客户端可能把 Bob 的「全部回复」预填为 To: [email protected]、Cc: [email protected]。这会产生一封新邮件,新信封里是客服邮箱和 Carol,Bob 成了新邮件的作者。Bob 还可以在发送前删掉 Carol,改成只回复客服。

如果 Bob 的副本没有密送地址,「全部回复」也没有原来的信封可供恢复,审计邮箱不会因此自动收到回复。反过来,如果审计邮箱自己点「全部回复」,Bob 和 Carol 可能收到一封以审计邮箱为 From: 的新邮件,从而知道它看到了原信。审计邮箱点普通「回复」时,回复通常只给 Reply-To: 或原作者。如果原副本中确实有 Bcc:,客户端选择保留这些密送地址时,仍应把它们放在回复的 Bcc:,而不能移进公开的 To: 或 Cc:。

投递失败、重试和退信

Alice 点了一次发送,三个人却可能在不同时间收到报告。发件服务器要为每个信封收件地址记录进度。判断某个地址要不要重试,先看出问题时邮件传到了哪一步,再看当时是哪台服务器负责继续投递。

一个地址被拒绝,其他地址继续投递

假设发件服务器把 Bob、Carol 和审计邮箱的三个地址一起交给 mx1.receiver.example。Bob 的邮箱正常,Carol 的邮箱暂时不可用,审计地址在这台服务器上不存在。这是同一次 SMTP 事务的几行应答,C 代表发送端,S 代表接收端。

C: MAIL FROM:<[email protected]>
S: 250 2.1.0 Sender accepted
C: RCPT TO:<[email protected]>
S: 250 2.1.5 Recipient accepted
C: RCPT TO:<[email protected]>
S: 450 4.2.0 Mailbox temporarily unavailable
C: RCPT TO:<[email protected]>
S: 550 5.1.1 Mailbox unavailable
C: DATA
S: 354 End data with <CRLF>.<CRLF>
C: [发送原邮件内容]
C: .
S: 250 2.0.0 Queued as MX403

SMTP 交接、重试与退信。Bob 在 DATA 结束后的 250 应答处交接给收件服务器,Carol 由发件服务器重试。永久失败发生在交接前或交接后,由当时负责投递的服务器处理退信。

应答的第一位给出处理方向。2xx 表示当前请求成功。3xx 表示命令已被接受,服务器还在等后续数据,例如 DATA 后的 354。4xx 表示暂时失败,发送端通常保留待投递任务。5xx 表示当前请求永久失败。服务端声明支持 ENHANCEDSTATUSCODES 时,还会用 4.2.0、5.1.1 这样的增强状态码说明失败类别,后者在这里表示目标邮箱地址无效。

这里两个地址的失败都发生在 RCPT TO,因此 Bob 仍能进入后续的 DATA。接收端在这次事务里接下的收件人只有 Bob,消息内容仍可以保留 Cc: Carol <[email protected]>。假设发件服务器半小时后再试,它只需向接收端提交 Carol 的地址和同一份消息。Bob 已交给下一跳,审计地址按永久失败处理。重试间隔和队列保留时间由发送系统的策略决定。

RCPT TO 得到 250,说明服务器愿意把这个地址加入当前事务。等整份消息发完并用点号行结束,接收端才给出是否接下邮件的最终应答。把例子稍微改一下。如果 Bob 和 Carol 的 RCPT TO 都通过,但服务端读完消息后返回 451,这次交接仍未成功,发件服务器应为两个地址保留重试。如果结束时返回 554,就按对这批收件人的永久拒绝处理。普通 SMTP 在这里给当前事务一个最终结果,范围是此前接受的地址。上例最后返回 250,则由接收端为 Bob 接下后续投递或转交的责任。

同一套收件系统里,入口服务器有时还要把信交给另一个本地投递服务。这一步可以使用 LMTP(Local Mail Transfer Protocol)。它沿用许多 SMTP 命令,但收完整份消息后,会为此前接受的每个地址各返回一条最终结果。例如 Bob 的邮箱服务答 250,Carol 的邮箱暂时不可用答 450,内部投递进程只需为 Carol 保留重试任务。公网 SMTP 会话仍按它自己已经给出的结果分配投递责任。

接收后才发生的失败

另一种情况是 mx1.receiver.example 把三个地址都接受了,收完消息也返回了 250。Bob 和 Carol 的副本随后进入邮箱,审计地址却在下一步内部投递时被发现已经停用。外部 SMTP 会话已经结束,接收端会另外生成一封投递状态通知(Delivery Status Notification,DSN),送往原信的反向路径 [email protected]。若永久失败在上一段的 SMTP 交接时就已收到,则由当时仍负责投递的发件服务器处理。如果它之前已接下 Alice 的邮件,通常由它向原反向路径发送失败通知。两条路径产生退信的节点不同,故障仍按地址分别记录。

退信自己通过 SMTP 发送时使用 MAIL FROM:<>,也就是空反向路径。如果这封通知又投递失败,邮件系统就不会再为它发送投递失败通知,避免两台服务器反复互发退信。临时失败可以先发延迟通知,负责投递的服务器会继续重试,直到投递成功或超过重试期限。对普通待投递邮件,SMTP 规范建议一般至少尝试 4 到 5 天再放弃,邮件系统可以配置队列的重试期限。

DSN 常用 multipart/report; report-type=delivery-status 封装。第一部分是可直接读的说明,第二部分 message/delivery-status 是供程序读取的状态记录,还可以附上原邮件或它的头部。状态记录中,最前面是一组关于整份报告的字段,空行之后是关于一个收件人的字段。要报告多个收件人,就再用空行加下一组。

Reporting-MTA: dns; mx1.receiver.example

Final-Recipient: rfc822; [email protected]
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550 5.1.1 Mailbox unavailable

Reporting-MTA 是生成报告的服务器。Final-Recipient 是这组状态对应的信封收件地址,rfc822 表示这里写的是 Internet 邮箱地址。Action: failed 表示这个地址的投递已失败,Status: 5.1.1 给出标准化的状态,Diagnostic-Code 保留 SMTP 诊断细节。发件平台读到这个块后,可以把审计邮箱记为失败,Bob 和 Carol 的投递状态按各自记录处理。

主动请求投递通知

服务器在 EHLO 应答中公布 DSN 扩展时,发送端可以把通知要求写进 SMTP 命令。例如 Alice 的发件平台希望跟踪审计副本,要求失败或延迟时通知,并请求失败通知只附原邮件的头部。

MAIL FROM:<[email protected]> ENVID=report-20261109 RET=HDRS
RCPT TO:<[email protected]> NOTIFY=FAILURE,DELAY ORCPT=rfc822;[email protected]

ENVID 是发件端给这次信封传输设置的标识,返回的 DSN 可以用 Original-Envelope-ID 带回这个值。RET=HDRS 和 RET=FULL 分别请求在失败通知中返回原消息的头部或整份消息。NOTIFY 的作用范围是这一条 RCPT TO,可以请求 SUCCESS、FAILURE、DELAY,也可以单独用 NEVER 表示无需通知。ORCPT 记录原始收件地址,后续经过地址变换时,DSN 可以同时报告 Original-Recipient 和 Final-Recipient。没有这项扩展也可能产生普通退信。扩展提供的是请求与关联信息。

Bob 打开邮件时可能产生另一类回执。消息处置通知(Message Disposition Notification,MDN)报告的是客户端对消息做了什么,例如 displayed 表示消息被显示。发件方可在消息头用 Disposition-Notification-To 请求 MDN,收件客户端可以按用户设置决定是否发送。DSN 跟着服务器的投递结果走,MDN 跟着收件端对消息的处置走。

别名、自动转投与邮件列表

前面的信封地址直接写到了个人邮箱。实际投递也可以先经过一个共享地址。假设 Alice 把同一份报告发给 [email protected]。接收系统查到 team 是 Bob 和 Carol 的别名,就为 [email protected] 和 [email protected] 投递。两人看到的原邮件头仍可以是 To: [email protected]。在同一个系统内完成展开时,这一步甚至无需再走公网 SMTP。

Bob 也可以在邮箱里配置自动转投,把收到的信交给 [email protected]。如果规则要向外部域发送,receiver.example 的服务器会发起下一笔 SMTP 事务。它发送的 RCPT TO 是 [email protected],消息里的 To: 仍可能写原地址。原反向路径可以沿用,也可以由转投服务按自身策略改写。这样,归档邮箱看到的 From: 还是 Alice,但连接到它的服务器已变成 Bob 的转投服务。这正是后面重新做 SPF 时,输入可能发生变化的原因。

邮件列表会把发到列表地址的消息重新分发给订阅者。比如 [email protected] 收到报告后,对每位订阅者生成投递任务,并用列表自己的地址接收退信。列表软件还可能在主题加前缀、在正文末尾附退订说明。这些改动会改变随信传递的消息内容,因而也可能影响原来的 DKIM 签名。

如果是 Bob 在客户端里手动点「转发」,通常会创建一封以 Bob 为作者的新信,把原信放在引用文字或附件里。拿到两份邮件时,可以看外层作者、信封收件人和被引用的原消息,判断发生的是自动转投、列表分发,还是重新写了一封信。

邮箱同步与邮件存储

Bob 的电脑还在休眠,Alice 的报告已经进了服务器邮箱。Bob 在手机上用 IMAP 打开报告,手机把已读状态写回服务器。电脑上线后从同一个邮箱同步到这个标记。断网时,设备用本地缓存打开已经下载的正文或附件。使用 POP3 时,客户端把邮件下载到本机,已读状态也留在本机。

IMAP 两台设备通过服务器同步已读状态,POP3 从服务器下载本地副本

IMAP 同步的是服务器邮箱

Bob 的手机与电脑分别登录同一个 IMAP 账号。每个客户端先选中服务器上的邮箱文件夹,例如 SELECT INBOX,再按需获取邮件列表、头字段、正文和标记。可以先把发件人、主题、日期下载下来,点开时再取正文和附件。以 BODY.PEEK[HEADER] 取头部时,IMAP 不会因为这次读取而设置已读标记。

服务器给这封报告在 INBOX 里分配一个 UID,例如 42。客户端按账户、邮箱文件夹、UIDVALIDITY 和 UID来关联自己的缓存。UIDVALIDITY 是服务器为该文件夹给出的 UID 有效性标识。它变了,客户端就要重新建立映射,避免把旧缓存对应到别的信。协议中的消息序号会随其他邮件移除而变化,在同一个文件夹里查这封报告时,可以用 UID 稳定地定位到它。

  1. 手机把报告标为已读时,可以发 UID STORE 42 +FLAGS (\Seen)。服务器把 \Seen 加到 INBOX 中 UID 为 42 的条目上。
  2. 电脑在线并在选中的邮箱上使用 IDLE 时,服务器可以通知它有新邮件或状态变化,客户端再按需读取。假如电脑还在休眠,它联网后重新选中收件箱,向服务器补取邮件及标记,就能看到报告已经读过。
  3. 如果 Bob 又把报告移到服务器上的归档文件夹,电脑下一次同步时会看到收件箱里的条目消失,归档文件夹里有一封报告。UID 在单个文件夹内有效,归档文件夹里的那封信会使用目标文件夹的 UID。

客户端和服务器如果都支持 CONDSTORE,可以用修改序号(MODSEQ)记录同步进度,通过 CHANGEDSINCE 查询上次同步后发生变化的标记。支持 QRESYNC 时,还可以在重新打开邮箱时得知哪些 UID 已经移除。比如电脑上次同步后离线两天,这两项扩展可以减少重新扫描整个收件箱的工作量。IDLE 解决的是在线时的通知,重连时仍要补齐离线期间的变化。

「已发送」和「草稿」也可以是服务器上的文件夹。发件客户端用 SMTP 提交邮件后,可以通过 IMAP APPEND 把副本存入「已发送」,有些邮件服务则会在发送后代为保存。草稿存到服务器上,另一台设备就能打开它继续编辑。如果 Bob 把草稿放在电脑的本地文件夹里,手机登录同一个账号也看不到那份草稿。

POP3 下载邮件与服务器副本

POP3 客户端从服务器的邮件存放区(maildrop)下载消息。支持 UIDL 时,客户端可以保存每封邮件的唯一标识。下次取信时先对比这些标识,已经下载过的就可以跳过。比如电脑看到报告对应的标识已在本地记录,就无需再执行一次 RETR。另一台设备有自己的下载记录。服务器仍保留报告时,它也可以取到这封信。

RETR 负责下载。如果客户端设置为取信后删除,通常会在下载后发 DELE,给该条邮件加上待删除标记。客户端在取信阶段发出 QUIT 时,服务器进入 UPDATE 状态并尝试落实删除。假如发了 DELE 就意外断线,这次会话不会提交待删除标记。若客户端设置为保留服务器副本,就可以只下载而不发 DELE,服务器还可能另有自动清理策略。

Bob 在 POP3 客户端上读完报告、把它移进电脑本地归档,这些动作都发生在电脑的本地副本上。POP3 没有用于跨设备同步已读、文件夹和归档位置的操作。要是服务器副本已经被删除,而报告只剩这台电脑的下载副本,另一台设备就无法再从服务器取到它。

服务器和客户端各自怎样存储

IMAP 向客户端提供邮箱文件夹、邮件内容和标记。以 Dovecot 的 Maildir 为例,一个邮箱目录里有 tmp/、new/ 和 cur/。投递进程先在 tmp/ 写完一封信,再将它放进 new/,避免客户端看见还没写完的文件。邮件之后可以移到 cur/,文件名中的 S 标记表示已读。Dovecot 另用 dovecot-uidlist 维护 IMAP UID 和邮件文件名的对应关系。mbox 则把多封邮件顺序存进一个邮箱文件。其他系统也可将内容和索引放在各自的数据库或存储服务里。

报告的附件作为 MIME 部分随消息投递。服务器把原始消息保存成一份文件时,附件的编码内容也在里面。客户端可能先缓存头和正文,到 Bob 点开 报告.txt 时才拉取附件。若 Bob 再点「另存为」,磁盘上还会有一份解码后的 报告.txt。这份独立文件和邮箱里的邮件可以各自保留。

有些客户端还可以限定离线缓存的时间范围。比如 Windows 版经典 Outlook 用缓存 Exchange 模式访问 Exchange 邮箱时,会将下载的项目存入本地 .ost 文件。如果 Bob 将离线保留范围设成三个月,电脑断网时只看到最近三个月的邮件。更早的报告只要还在服务器邮箱里,联网后就能继续查阅。

邮件会保存多久

投递队列在重试期限内暂存待发邮件。投递完成后,服务器邮箱和设备副本各按自己的保留方式存放。

位置 什么决定保存期限 以报告邮件为例
发件服务器的投递队列 发送系统的重试和队列过期策略 收件服务器暂时拒绝时先排队。后续成功交接就移出该待投递任务,超过重试期限则按投递失败处理。
服务器上的收件箱、归档或垃圾箱 服务商或单位的邮箱保留设置,以及用户操作 移到服务器的归档文件夹后,两台设备都可以继续访问。若垃圾箱设置了自动清理,到了期限就会从该文件夹移除。
服务端可恢复区、合规保留或备份 恢复期限、保留策略及备份周期 以 Exchange Online 为例,从「已删除邮件」再删除的项目会进入可恢复区,默认保存 14 天。管理员可以把这段恢复期限设到最多 30 天,合规留置可以让项目在服务端保留更久。
客户端离线缓存或本地归档 下载范围、缓存清理和本地文件管理方式 IMAP 客户端可能只留近期邮件。POP3 下载后的本地副本或单独导出的文件,可以在服务器副本消失后继续留在设备上。

Bob 在手机的 IMAP 邮箱点「删除」时,客户端可能把邮件移入服务器垃圾箱,也可能先设 \Deleted 标记,再用 EXPUNGE 从该邮箱文件夹移除。电脑会同步服务器上的变化。清理手机缓存后,手机可以联网重新下载服务器上保留的邮件。Bob 从电脑的 POP3 本地归档删除报告时,移除的是这台电脑上的副本。

邮件头与邮件体

Bob 如果打开「查看原文」,看到的会是一行行的头字段和正文。先用一封短邮件看清格式。消息顶部是邮件头(header section),第一个空行下面是可选的邮件体(body)。这种 Internet 邮件消息格式简称 IMF(Internet Message Format)。

From: Alice <[email protected]>
Date: Mon, 9 Nov 2026 09:00:00 +0800
Subject: Report review
 for Monday

From: This line is part of the message body.

Subject: 下一行以空格开头,所以是主题的续行,展开后主题为 Report review for Monday。空行后的 From: 已在正文中,按普通文字处理。读取一个字段时,冒号前是字段名,后面是字段体。字段名不区分大小写,例如 Message-Id 和 Message-ID 是同一个字段名。

线上每行用 CRLF(回车加换行)结尾。展开折行时,移除续行前的 CRLF,保留续行开头的空白。基本消息的每行上限为 998 个字符,建议不超过 78 个字符。允许直接使用 UTF-8 的国际化邮件按 998 个八位字节限制行长,均不计算行尾 CRLF。

邮件头字段的类别与出现次数

一行 From: 描述谁写了信,一行 Received: 记录哪台服务器接过信。两者都是头字段(header field),但承担不同工作。Internet Message Format 把字段按用途划为起草日期、来源、目的地、标识、一般信息、再发送、追踪和可选字段。其他规范还定义了 MIME、邮件列表、认证和回执字段。IANA 的 Message Headers 登记表列出这些公开字段的用途、适用协议及标准状态。

出现规则 代表字段 如何解读
必须各有一项 Date、From 标准消息的最低要求。同一个 From: 可以按规范容纳多个作者邮箱,字段仍然只有一项。
至多一项 Sender、Reply-To、To、Cc、Bcc、Subject、Message-ID、In-Reply-To、References Sender 在 From 含多个邮箱时必需。Message-ID 建议填写。标识字段只有一项,References 的字段体仍可引用多个 ID。
可以按次重复 Received、成组的 Resent-*、DKIM-Signature 分别对应多跳传递、多次重新发送及多条签名。需要保留原顺序和每一次出现的内容。
按各自规范解释 Comments、Keywords 及其他扩展字段 前两项在 RFC 5322 中可重复出现。其他字段的次数限制由定义它的规范决定。

有多位作者时,在同一个 From: 字段里列出各自的地址,并用 Sender: 指明负责发送的邮箱。一封邮件出现两条 From: 属于字段重复。SMTP 每经过一跳就加一条 Received:,四条记录可以沿着各节点逐条读。最终投递环节写入 Return-Path。

作者、收件人和回复地址

以 From: Alice <[email protected]> 为例,Alice 是显示名,[email protected] 是邮箱地址。地址中 @ 左侧是本地部分,右侧是域名。客户端可以把显示名和邮箱地址一起展示,也可能在列表里只展示显示名。程序要先解析 From: 的地址结构,再分别取出显示名、邮箱地址和域名。显示名可以用引号或 RFC 2047 编码表示,其中的 @ 不会因此变成地址分隔符。

字段 含义与来源
From 作者的邮箱,可以附显示名。比如值班人员撰写通知,作者地址可以是代表团队的业务邮箱。
Sender 负责实际发出这份消息的邮箱。若同一 From: 列出多个作者邮箱,标准要求用 Sender: 指明一个负责发送的邮箱。它仍是邮件头字段。
Reply-To 作者建议的回复目标,可以是客服邮箱或工单地址。省略时,客户端一般回复到 From:。
To、Cc、Bcc 消息中的主要、抄送和密送收件人。是否在收到的副本里出现 Bcc:,由发送端生成副本的方式决定。实际 SMTP 投递地址由各跳的 RCPT TO 给出。

例如助理代两位作者发送联合通知,From: 可以列出两位作者,Sender: 填助理邮箱,Reply-To: 指向共用工单地址。RFC 6854 还扩展了 From 和 Sender 的地址组语法。像 From: Automated reports:; 这样的空地址组可以显示组名,但没有可供提取的邮箱或作者域。普通邮件仍应优先使用邮箱地址语法。

重新发送与 Resent 字段组

假设 Bob 要把 Alice 的原消息原样再发送给新加入的复核邮箱 [email protected]。这次 SMTP 信封的 RCPT TO 用复核地址。消息会在原字段之前加一组 Resent-*,记录 Bob 的再发送,较新的组排在较老的组前。每组都要写 Resent-Date 和 Resent-From,建议带 Resent-Message-ID,也可以写入 Resent-Sender、Resent-To、Resent-Cc 或 Resent-Bcc。

Resent-Date: Mon, 9 Nov 2026 10:10:00 +0800
Resent-From: Bob <[email protected]>
Resent-To: [email protected]
Resent-Message-ID: <[email protected]>
Date: Mon, 9 Nov 2026 10:00:00 +0800
From: Alice <[email protected]>
Reply-To: [email protected]
To: Bob <[email protected]>
Cc: Carol <[email protected]>
Message-ID: <[email protected]>

这组新字段记录 Bob 重新发出原消息这件事。正常回复仍使用原有的 From、Reply-To 和 Message-ID 等字段。客户端里的常规转发通常生成一封新消息,引用原正文或附加原邮件。邮件服务器的自动转投则可能只改变信封路由。读原文时可以从有无新消息、原字段是否保留和实际信封地址三个角度区分这些操作。

主题、起草时间与会话标识

Subject 是客户端显示的主题。Date 一般由作者或其客户端设置,表示消息已写完、准备进入邮件系统的时间。离线撰写、定时发送和中间排队都会拉开 Date 与投递时间的差距。比较某两台服务器的接收时刻时,先看各自的 Received 时间和时区,再结合日志确认时钟差异。RFC 5322 的基本消息需要 Date 和 From,建议提供 Message-ID,主题和 To 均为可选项。

Message-ID 标识这份消息,例如 <[email protected]>。回复时,In-Reply-To 可以引用父消息的 ID,References 可以保留此前的引用链,客户端据此组织会话。SMTP 各节点也会分配本地队列 ID。它适合在同一节点关联接收、排队和投递日志,邮件跨到另一台服务器后通常会分配新 ID。

列表标识、管理字段和一键退订

List-Id 用于把邮件归到一个稳定的列表身份,例如 Reports <reports.lists.sender.example>。尖括号内是列表标识。List-Post 提供发帖入口,只读列表可以写 NO。List-Help、List-Owner 和 List-Archive 分别指向帮助、联系列表负责人和历史归档。List-Subscribe 与 List-Unsubscribe 指向订阅或退订入口。管理字段常用尖括号包住 URI,值中可以按优先顺序列出逗号分隔的多个 URI,例如网页入口和 mailto: 邮件命令。

RFC 8058 的一键退订使用成对的 List-Unsubscribe 和 List-Unsubscribe-Post。前者必须提供一个 HTTPS URI,并可再给一个 mailto: 等非 HTTP(S) 入口。后者声明固定的表单键值。示例中 tk_31qB8 表示服务端用来识别收件人和列表的不透明令牌。

List-Unsubscribe: <https://lists.sender.example/unsubscribe/tk_31qB8>,
 <mailto:[email protected]?subject=unsubscribe>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

得到用户同意后,收件端才向该 HTTPS URI 发送 POST。表单只需提交 List-Unsubscribe=One-Click,编码可用 multipart/form-data,也可用 application/x-www-form-urlencoded,不附带 Cookie 或 HTTP 认证信息。收件端以 POST 明确表达退订操作,这样预读链接的 GET 可以和退订请求区分。发件方须提供至少一条有效 DKIM 签名,其中 h= 同时覆盖这两个退订字段。验签让接收端知道它读到的退订入口仍是签名方声明的那个地址。

自动提交与消息处置通知

Auto-Submitted 最多出现一次。定时任务等独立生成的邮件可标 auto-generated,休假通知等由收到某封信触发的自动回复使用 auto-replied,no 表示不是自动提交。自动回复程序通常跳过带有非 no 值的来信,减少两边互相回信形成的循环。这类头字段表达消息的生成方式,仍需结合写入来源理解。

Disposition-Notification-To 是消息处置通知(MDN)的请求字段,提供回执地址。客户端可以忽略请求,也可以根据用户选择回送一封通知,其中 Disposition 记录 displayed、processed 等状态。displayed 只说明客户端展示了这封信,无法证明人已经读懂。SMTP 投递状态通知(DSN)则由负责传递或投递的邮件系统报告投递结果。

Received 和 Return-Path

每台 SMTP 服务器接收消息时,会把自己这一跳的 Received 加在旧记录上面。这里沿用 Alice 的邮件和信封地址,演示启用了 TLS 的投递路径。提交服务器与发件中继由同一节点承担,最终投递后的头部可以包含下面这些字段。

Return-Path: <[email protected]>
Received: from smtp.sender.example (smtp.sender.example [198.51.100.27])
 by mx1.receiver.example with ESMTPS id MX402
 for <[email protected]>; Mon, 9 Nov 2026 10:00:07 +0800
Received: from client.sender.example (unknown [192.0.2.40])
 by smtp.sender.example with ESMTPSA id OUT091;
 Mon, 9 Nov 2026 10:00:02 +0800

从下往上读,第一段是客户端向发件系统提交,第二段是发件系统向收件 MX 交接。from 描述连接来源,可能同时包括 SMTP 客户端声明的名称和接收端观察到的 IP。by 指写下这条记录的接收节点。with 标记传输类型。按 RFC 3848,ESMTP 代表使用扩展 SMTP,ESMTPA 表示 SMTP AUTH 成功,ESMTPS 表示通过 STARTTLS 成功建立了 TLS,ESMTPSA 则表示 STARTTLS 和 SMTP AUTH 都成功。id 可以用来查该节点的本地队列日志。可选的 for 记下一个收件路径,无法从中恢复一份完整的密送或批量投递列表。分号后的日期是该接收节点记录的时间。

在这份示例里,from 后第一个 smtp.sender.example 是发件服务器在 EHLO 中声明的名称,括号里的同名主机是收件 MX 对 198.51.100.27 做反向解析得到的结果。两处恰好写成同一个名字。括号的内容和格式随服务器实现而异,例如下方那一跳用 unknown 表示未取得主机名。如果能在本方 MX 上找到 MX402,可以用对应的日志确认入站连接的对端 IP。更早的 Received 则由上游服务器写入。

Return-Path 由最终 SMTP 投递环节写入,记录该次投递所见的 MAIL FROM。这次可以刚好等于入口会话的反向路径,转发或邮件列表改写后则可能不同。要确认某一跳具体用了哪个信封地址,仍要回到该跳的 SMTP 记录。

认证结果与网关自定义字段

安全相关字段也有不同的写入方。DKIM-Signature 来自签名方,包含签名和验签所需参数。Authentication-Results 来自执行验证的服务,authserv-id 标识该服务,后面才是 spf、dkim、dmarc 等检查结果。Received-SPF 由执行 SPF 检查的接收端记录,可能给出被查身份和连接 IP。ARC 的 ARC-Authentication-Results、ARC-Message-Signature、ARC-Seal 按实例编号成组写入,以签名串联转发链上的声明。IANA 将 ARC 字段登记为 experimental。读 Authentication-Results 时应先确认生成方属于已知信任边界,外部同名头里的 pass 只能代表该文本声称的结果。

网关可以在邮件头加入诊断和追踪字段。例如某个网关把入口会话的 MAIL FROM 写进 X-Gateway-Envelope-From。产品文档说明字段的写入规则,同次会话的日志可以核对这个地址。X- 是历史命名习惯,字段是否有标准定义可查 IANA 的邮件头字段登记。

RFC 8601 要求入口清理冒充本方认证服务的 Authentication-Results。如果导出副本省略了认证结果,可以到执行验证的节点查对应的会话记录。

导出的邮件文件

一个 .eml 文件通常保存一封邮件的头和体,包括导出时已经存在的追踪字段。一次 SMTP 连接的命令、响应及全部信封地址属于会话记录。.msg 是 Outlook 用来保存单个项目及其属性的文件格式,邮件只是可存储的一类项目。要把多封邮件放在同一个归档文件中,也可以用前面提到的 mbox。

MIME 与邮件能装下的内容

如果 Alice 只给 Bob 留一句「报告已完成」,邮件体可以只放一段文本。加上附件后,Bob 的客户端得知道哪些字节属于正文,哪些字节应该还原成文件。MIME(Multipurpose Internet Mail Extensions)提供了表示这些内容的格式。它可以给每部分标出媒体类型和编码方式,也能把多部分嵌套起来。下面是只有一句纯文本的邮件所需的 MIME 头和正文片段。

MIME-Version: 1.0
Content-Type: text/plain; charset=US-ASCII

The report is complete.

MIME-Version: 1.0 声明邮件使用 MIME。客户端读到 text/plain 后按纯文本显示,charset=US-ASCII 说明怎样把正文的字节还原为字符。有附件时,顶层 Content-Type 可以改成一个多部分容器,再在容器内部各放一段文本和一个文件。

内容类型和呈现方式

MIME-Version 声明 MIME 格式版本,Content-Type 给出类型和 charset、boundary 等参数,Content-Transfer-Encoding 说明如何还原字节,Content-Disposition 给出呈现方式和建议文件名。Content-ID 标识一个 MIME 部分,供 HTML 中的 cid: 引用。Content-Description 可放一段供人阅读的内容说明,Content-Language 可标注内容使用的语言。例如一封邮件的文本部分可以同时有 charset=UTF-8 和 Content-Language: zh。前者告诉客户端怎样把字节解成字符,后者说明字符组成的文本使用什么语言。顶层头字段描述整份邮件,从多部分容器拆出来的子部分也可以有自己的内容字段。

Content-Type 表示内容的媒体类型。例如 text/plain 是纯文本,text/html 是 HTML,image/png 是 PNG,application/pdf 是 PDF。charset 是文本的字符集参数,例如 text/plain; charset=UTF-8。application/octet-stream 可以表示通用二进制内容。

Content-Disposition 描述期望的呈现方式。inline 表示随消息呈现,attachment 表示作为独立附件呈现,filename 或 filename* 可以提供建议文件名。媒体类型、文件名和呈现方式各有用途。比如同样一段 PNG 数据,可以作为独立附件,也可以通过内容标识被 HTML 正文引用。客户端最终如何显示,还受其渲染能力和设置影响。

形式 常见表示 实际含义
纯文本或 HTML 正文 text/plain、text/html 纯文本按字符呈现,HTML 可包含格式、表格、超链接和图片引用。客户端按自身规则渲染 HTML。
文档、图片、音视频、压缩包等 application/pdf、image/png 等媒体类型 文件字节可以作为独立 MIME 部分传输,二进制附件常用 Base64。
正文内嵌图片 multipart/related 中的 HTML 与带 Content-ID 的图片 HTML 的 cid: 引用指向消息内部的 MIME 部分,图片字节随邮件一起投递。
远程图片、网页和共享文件 正文中的 HTTPS URL 消息携带地址,图片加载或链接打开时另行发起网络访问。
作为附件转发的邮件 message/rfc822 子部分可以包含一份完整邮件,里面还有自己的邮件头和 MIME 结构。普通转发也可能把原信复制成新邮件正文里的引用文字。
日历邀请 text/calendar 以 iCalendar 数据承载会议邀请、更新或取消。支持的客户端可以将其显示为可响应的日程。

MIME 实体、子部分与边界行

一份 MIME 实体(MIME entity)由描述内容的头字段和实体体组成。整封邮件也可以看作一个顶层实体,顶层 Content-Type 决定其正文是一段纯文本、一张图片,还是一个容纳多个子部分的容器。以 multipart/mixed 为例,读到顶层邮件头结束的空行后,解析器再用该实体自己的 boundary 参数查找各个子部分。每个子部分也有自己的头部、空行和实体体。子部分如果还是 multipart/*,解析器继续进入下一层。

Content-Type: multipart/mixed; boundary="mix-20261109" 中的 boundary 参数值是 mix-20261109。每个子部分从一行 --mix-20261109 开始,整层用 --mix-20261109-- 结束。这些行在原始消息中位于行首。每层应选用不同的边界,且边界分隔符不能出现在封装内容的行首。边界参数至多 70 个字符,参数长度不计分隔行中额外添加的 --。在结束子部分内容时,紧接边界分隔符之前的 CRLF 在语法上属于分隔符。举例来说,若要让文本子部分自身也以 CRLF 结束,线上就会在最后一行正文与边界之间再留一个空行。解析器识别内层边界的同时仍要能识别外层边界,这样截断的内层实体也不会把外层后续附件吞进去。

某个多部分实体中,第一条边界之前的字节叫 preamble,闭合边界之后的字节叫 epilogue。它们都在该实体的封装范围内,解析 MIME 树时无需作为有类型的子部分呈现。发件程序有时把兼容性提示写在 preamble 里,让只能读原始文本的客户端也能显示一行提示。嵌套的 multipart 也可有自己的 preamble 和 epilogue,解析时按容器层级处理。

容器类型决定子部分怎样组合

邮件需要把报告正文和一个文件一起交给收件人时,外层可用 multipart/mixed。其中正文又可以是一个 multipart/alternative,同时包含 text/plain 与 HTML 版本。alternative 的每个直接子部分都是同一内容的一个候选版本,发送方一般把低保真、易兼容的版本放前面,把更完整的版本放后面。支持后者的客户端可以优先选后面的候选。若 HTML 版本依赖一张内嵌图片,可以把这个候选做成 multipart/related。客户端选中该候选后,再把根文档和它引用的资源组合起来渲染。

类型 直接子部分的含义 例子
multipart/mixed 按顺序封装多个可独立解释的部分 正文后带一个报告文件。
multipart/alternative 同一内容的多种候选表达 纯文本和带内嵌图的 HTML 版本各是一项候选。
multipart/related 组合成一个对象的多个部分。必需的 type 参数标明根部分的媒体类型,start 可按 Content-ID 指定根部分。省略 start 时,取第一个直接子部分为根。 HTML 是根部分,PNG 是被引用资源。
multipart/digest 由多封消息组成的文摘。直接子部分省略 Content-Type 时按 message/rfc822 解释。 把邮件列表的一批来信装进一封每日摘要。
multipart/report 第一部分便于人阅读,第二部分是可供程序读取的报告,第三部分可选,可携带原始消息或其一部分。 退信的 report-type=delivery-status 对应机器可读的 message/delivery-status,可报告各收件人的投递动作、状态码与诊断信息。
multipart/signed 第一部分为受签名保护的实体,第二部分为分离签名,protocol 和 micalg 说明签名部分的类型及完整性算法。 OpenPGP/MIME 可用 application/pgp-signature 携带签名。验签以签名协议规定的原始实体表示为准,涉及该部分自己的 MIME 头和内容。
multipart/encrypted 第一部分是由 protocol 指定的加密控制信息,第二部分是封装后的加密数据。 OpenPGP/MIME 的控制部分可含版本信息,解密数据部分后再解析内部 MIME 结构。

message/rfc822 本身也是一种实体类型,实体体是一份嵌套的互联网邮件,内有自己的邮件头和正文。「将原邮件作为附件转发」可以用这种类型实现,邮件客户端解析到这里会进入另一份完整消息。若所封装的邮件使用 RFC 6532 的原生 UTF-8 头字段,可以用 message/global 表示。message/rfc822 的传输编码限于 7bit、8bit 或 binary。message/global 则允许为这一整段嵌套消息使用 Base64 等传输编码,再在解码后解析内部邮件。

假设一封报告邮件附有 PDF,HTML 里还引用了一张内嵌图,结构可以画成下面这样。alternative 的两个候选是一段纯文本和一个 HTML 资源组合,PDF 则是外层 mixed 的独立子部分。

multipart/mixed
├─ multipart/alternative
│  ├─ text/plain
│  └─ multipart/related; type="text/html"
│     ├─ text/html            <img src="cid:[email protected]">
│     └─ image/png            Content-ID: <[email protected]>
└─ application/pdf           Content-Disposition: attachment

字符集、传输编码与国际化

拿到一个 MIME 部分时,可以按两个步骤还原内容。先看 Content-Transfer-Encoding,把传输表示恢复为原字节。如果媒体类型是文本,再按 charset 把字节解释成字符。UTF-8 解决文字到字节的映射,base64 和 quoted-printable 解决字节怎样表示在邮件里。

  • Base64 用可打印字符表示字节,常用于二进制附件。长度为 n 字节的数据经 Base64 编码后有 4 × ceil(n / 3) 个字符,含末尾填充。再算上换行和 MIME 头字段,整封邮件占用的传输空间会更大。
  • Quoted-Printable 保留大部分可直接表示的 ASCII 字符,把需要编码的字节写成 =HH。行尾单独的 = 表示软换行,解码时和紧随的换行一并移除。
  • 7bit、8bit、binary 描述字节数据的性质,不做 Base64 或 QP 那样的转换。SMTP 8BITMIME 允许相应的 8 位 MIME 内容按行传输。任意二进制数据的直接传输需要协商 BINARYMIME 与 CHUNKING,使用带长度的 BDAT 代替以点号结束的 DATA。

接收端解开传输编码后得到原始字节。对于文本,再按 charset 还原字符。对于 PNG 或 PDF,解码结果就交给相应格式的解析器。

头字段中的中文另有规则。传统 ASCII 兼容邮件可以把主题写成 =?UTF-8?B?5rWL6K+V6YKu5Lu2?=,读出来是「测试邮件」。这是 RFC 2047 的 encoded-word,可用于主题或地址中的显示名等允许的位置。附件文件名是 MIME 参数,适用 RFC 2231 的 filename* 表示,例如 filename*=utf-8''%E6%8A%A5%E5%91%8A.txt,解码后是「报告.txt」。

encoded-word 里的 B 用 Base64 表示字节,Q 则用 =HH 表示需编码的字节,并可用 _ 表示空格。例如「报告」可以写成 =?UTF-8?B?5oql5ZGK?=,也可以写成 =?UTF-8?Q?=E6=8A=A5=E5=91=8A?=。这种 _ 的含义来自头字段的 Q 编码,正文里的 Quoted-Printable 下划线仍按普通字符读取。每个 encoded-word 连同分隔符最多 75 个字符。长字段可以拆成多个完整的 encoded-word,展示时忽略相邻编码词之间用于分隔的线性空白。邮箱的实际 addr-spec 和 MIME 参数各有自己的语法。

如果邮件地址的本地部分需要非 ASCII 字符,或者原始头字段直接使用 UTF-8,则使用国际化邮件格式与 SMTP SMTPUTF8 扩展。SMTPUTF8 由下一跳 SMTP 服务在 EHLO 中公布,和 8BITMIME 所解决的问题不同。域名还可以通过 IDNA 的 ASCII 形式参与 DNS 查询,域名的表示方式也和 @ 前面的邮箱本地部分分开处理。

带附件的原始邮件

下面是前面 SMTP 示例中,发件服务器用 DATA 传过去的消息内容。Bob 写在 To:,Carol 写在 Cc:。Alice 还密送了审计邮箱。发件服务器在发送内容前发出 RCPT TO:<[email protected]>,收件服务器回复 250,把审计邮箱列入本次投递名单。发件端生成这份消息时去掉了 Bcc:,所以你在下面的原文里找不到审计地址。收件服务器仍会按 RCPT TO 为它安排投递。

原文省略了服务器添加的追踪字段。outer-report 把正文和文件分开,body-report 把同一正文的纯文本版和 HTML 版分开。

From: Alice <[email protected]>
To: Bob <[email protected]>
Cc: Carol <[email protected]>
Reply-To: [email protected]
Date: Mon, 09 Nov 2026 10:00:00 +0800
Subject: =?UTF-8?B?5rWL6K+V6YKu5Lu2?=
Message-ID: <[email protected]>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="outer-report"

--outer-report
Content-Type: multipart/alternative; boundary="body-report"

--body-report
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

=E4=BD=A0=E5=A5=BD=EF=BC=8C=E6=8A=A5=E5=91=8A=E8=A7=81=E9=99=84=E4=BB=B6=E3=
=80=82
--body-report
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

<p>=E4=BD=A0=E5=A5=BD=EF=BC=8C<strong>=E6=8A=A5=E5=91=8A</strong>=E8=A7=81=
=E9=99=84=E4=BB=B6=E3=80=82</p>
--body-report--
--outer-report
Content-Type: text/plain; charset=US-ASCII
Content-Disposition: attachment;
 filename*=utf-8''%E6%8A%A5%E5%91%8A.txt
Content-Transfer-Encoding: base64

UmVwb3J0IHN1bW1hcnkNCg==
--outer-report--

解码后的主题是「测试邮件」。纯文本正文为「你好,报告见附件。」,HTML 版只给「报告」加了粗体。最后一个 text/plain 部分带有 attachment,文件名为 报告.txt,字节解码后是 Report summary 和一个 CRLF。三部分都随消息投递,客户端按 MIME 结构选正文,再把独立的附件展示出来。

HTML 正文与内嵌图片

现在另发一封报告邮件,在 HTML 正文里放一张蓝色状态图。这封邮件也有一个纯文本版本和一份独立附件「报告-2026.txt」。支持 HTML 的客户端要先找到作为正文的 HTML,再顺着其中的图片引用找到 PNG。下图用简短的标识展示这两次查找。

multipart/related 用 start 找到根 HTML,HTML 的 cid 引用匹配图片的 Content-ID

下面的原文把图片排在 HTML 前面,可以看出 start 怎样选中根部分。图像的 Base64 解码后是一个完整的 1×1 PNG。HTML 的属性等号使用 Quoted-Printable 编码。整个示例可以按 ASCII 保存,实际 SMTP 消息使用 CRLF 换行。

From: Alice <[email protected]>
To: Bob <[email protected]>
Subject: Report complete
Date: Mon, 9 Nov 2026 10:01:00 +0800
Message-ID: <[email protected]>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="mix-20261109"

This is a MIME message with a plain-text version and an attachment.
--mix-20261109
Content-Type: multipart/alternative; boundary="alt-20261109"

--alt-20261109
Content-Type: text/plain; charset=us-ascii

Report status: complete. The report is attached.
--alt-20261109
Content-Type: multipart/related; boundary="rel-20261109";
 type="text/html"; start="<[email protected]>"

--rel-20261109
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-ID: <[email protected]>
Content-Disposition: inline; filename="status.png"

iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNQTV0MAAHg
AS6gvpjoAAAAAElFTkSuQmCC
--rel-20261109
Content-Type: text/html; charset=us-ascii
Content-Transfer-Encoding: quoted-printable
Content-ID: <[email protected]>

<html><body>
<p><img src=3D"cid:[email protected]" alt=3D"">Report
status: complete. The report is attached.</p>
</body></html>
--rel-20261109--
--alt-20261109--
--mix-20261109
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
 filename*0*=utf-8''%E6%8A%A5%E5%91%8A-;
 filename*1*=2026.txt

UmVwb3J0IHN0YXR1czogY29tcGxldGUuDQo=
--mix-20261109--
End of MIME example.

按边界拆开后,树结构如下。根 HTML 仍然是 related 的直接子部分,只是线上的位置排在 PNG 后面。

multipart/mixed [mix-20261109]
├─ multipart/alternative [alt-20261109]
│  ├─ text/plain
│  └─ multipart/related [rel-20261109, type=text/html]
│     ├─ image/png [[email protected]]
│     └─ text/html [根, [email protected]]
└─ text/plain [attachment, 报告-2026.txt]

支持 HTML 的客户端进入 related,先用 start 找到 Content-ID: <[email protected]>,并核对 type="text/html" 与根部分的 Content-Type 一致。解码根部分的 Quoted-Printable 后,HTML 的 src=3D"cid:…" 还原为 src="cid:…"。客户端根据 cid: URL 找到同一组合中的 <[email protected]>,再把对应实体体的 Base64 还原成 PNG 字节。Content-ID 字段值写成带尖括号的消息标识符语法,cid: URL 中则使用不带尖括号的标识符,必要时还要处理 URL 的百分号编码。标识符里的 @sender.example 是标识符的一部分,此处没有 DNS 查询或 HTTP 下载。纯文本客户端可选第一种候选,从外层 mixed 仍可取得独立的报告附件。

Content-Disposition: inline 和 attachment 是给客户端的呈现建议,filename 是建议文件名。按 RFC 2387,支持 related 的客户端按组合关系处理内嵌资源:HTML 用 Content-ID 找到状态图,保存时可以采用建议文件名 status.png。独立附件「报告-2026.txt」位于外层 mixed 中,用自己的 Content-Disposition 声明用途。

Content-Location、相对地址与远程图片

把上一封邮件的图片引用改为 <img src="https://www.sender.example/reports/images/status.png">,客户端可以先在邮件里查找 Content-Location 与该 URI 一致的图片子部分。匹配到子部分时,直接用邮件里的图片字节渲染。

MHTML 还可以用 Content-Location 标记资源的 URI。假设 HTML 根部分带有 Content-Location: https://www.sender.example/reports/index.html,HTML 中引用 <img src="images/status.png">。若 HTML 自己没有指定更优先的基地址,便可以将相对引用解析成 https://www.sender.example/reports/images/status.png。当同一 multipart/related 中的图像子部分具有相同的 Content-Location 时,处理器可取用附带的图像。URI 长得像 HTTPS 地址,取用的仍然是消息里已经收到的字节。

RFC 2557 对基地址有查找顺序。先看文档内的基地址声明,例如 HTML 的 <base href="…">,然后看当前部分的绝对 Content-Location,再往封装它的外层实体查找。邮件内缺少可用绝对基地址时,multipart/related 内部还可使用 thismessage:/ 作为解析相对地址的基地址。处理器解析 HTML 引用及各子部分的位置标记后,在适用的 related 范围内比较得到的 URI。若使用的是 cid:,则按 Content-ID 匹配。

当邮件里找不到与 https: 图片地址匹配的 MIME 部分,加载这张图就需要一次外部请求。客户端可以拦截这次加载、等用户允许后再加载,或通过服务商的代理请求。读取 HTML 原文和 MIME 头即可列出引用及其对应的内嵌资源。

缺省类型、传输编码和跨行参数

普通 MIME 实体省略 Content-Type 时,默认按 text/plain; charset=us-ascii 解释。上面提到的 multipart/digest 有一个明确例外,它的直接子部分没有 Content-Type 时按 message/rfc822 解释。省略 Content-Transfer-Encoding 时默认是 7bit,本例的纯文本部分就是这样。7bit、8bit、binary 描述可以直接承载的字节范围和行结构,不发生 Base64 一类的还原操作。传统 SMTP DATA 可以经 8BITMIME 扩展承载 8 位行数据,任意二进制则涉及 BINARYMIME 与 BDAT。在 MIME 树中,multipart/* 的边界需要直接可见,所以不能将整个容器的体声明为 Base64 或 Quoted-Printable。对子部分分别使用编码,就能同时传递可见边界和编码后的图像、附件。

媒体类型和参数分开解析。像 boundary="rel-20261109" 或 start="<[email protected]>" 里的双引号属于参数语法。参数可用折行写到下一行,续行开头要有空白。更长的文件名还可以按 RFC 2231 把一个参数拆成带序号的几段。上面附件的 filename*0* 和 filename*1* 是从零开始连续编号的扩展参数片段。第一段的 utf-8'' 指明字符集与空的语言标签,随后的 %HH 表示文件名字节。后续段继承这套字符集与语言信息。接收端按序收集片段,解百分号编码,再用 UTF-8 解出「报告-2026.txt」。正文里的 UmVwb3J0IHN0YXR1czogY29tcGxldGUuDQo= 是另一层编码,解码结果是 Report status: complete. 加 CRLF。

日历邀请与更新

如果 Alice 还要约 Bob 看报告,可以在邮件中加入一个 text/calendar 部分。iCalendar 定义数据怎么写,iTIP 定义邀请、更新、取消和回复各自的含义,iMIP 则规定怎样通过 Internet 邮件交换这些数据。下面是一段 ASCII 编码的邀请部分。客户端识别 text/calendar 后,既可以把它展示成一张会议卡片,也可以交给日历应用处理。

Content-Type: text/calendar; charset=UTF-8; method=REQUEST
Content-Transfer-Encoding: 7bit

BEGIN:VCALENDAR
PRODID:-//Sender Example//Mail Demo//EN
VERSION:2.0
METHOD:REQUEST
BEGIN:VEVENT
UID:[email protected]
DTSTAMP:20261110T020000Z
DTSTART:20261112T020000Z
DTEND:20261112T023000Z
SEQUENCE:0
ORGANIZER:mailto:[email protected]
ATTENDEE;RSVP=TRUE:mailto:[email protected]
SUMMARY:Report review
END:VEVENT
END:VCALENDAR

Content-Type 的 method=REQUEST 与内部 METHOD:REQUEST 一致。UID 标识同一场会议,后续调整时间可以继续用这个 UID 发出 REQUEST,并提高 SEQUENCE。取消使用 CANCEL,与会者的接受或拒绝使用 REPLY。这里的 ORGANIZER、ATTENDEE 属于日历数据。SMTP 仍按信封地址投递邮件,日历客户端再根据日历字段处理会议。

发件域认证

提交账号和跨域身份

Bob 看到正文、图片和附件时,客户端已经把 MIME 解开了。收件系统做发信域验证时,使用的是更早拿到的数据,包括 SMTP 会话里的对端 IP 和信封地址,以及客户端渲染前的消息。

SMTP AUTH 是客户端向当前 SMTP 服务证明身份的机制,常用于邮件提交。提交服务器可以用认证账号决定是否允许发信,以及账号能用哪些发件地址。例如 Alice 登录提交服务的账号可以是 [email protected],管理员另行授权她用 [email protected] 发信。认证账号、信封反向路径和邮件头作者在这次提交里各有一个值。

到了跨域这一步,mx1.receiver.example 面对的是一条来自外部 MTA 的 SMTP 连接。它按自己负责的收件域接收邮件,通常无需外部 MTA 登录成域内邮箱用户。发件方的提交账号和授权规则留在发件系统。收件方先看见的是对端 IP、SMTP 命令里的信封地址,以及 DATA 中的消息。这三样数据的来源不同。连接 IP 来自本次连接,另外两项由发送端交给收件方。单靠 MAIL FROM 和 From: 中写出的域名,还无法判断发送端能否使用该域。

收件 MX 可以先用对端 IP 验证信封域的发信授权。这是 SPF 能解决的问题,它在 DATA 前就可以检查。邮件继续转发时,对端 IP 可能变化,但原消息可以原样保留。DKIM 把域名签名放进邮件本身,收件方拿到消息后就能核对签名覆盖的头字段和正文。两项检查各自成功时,得到的可能是退信域和代发平台的签名域。要把其中一个结果用来验证 Bob 所见的作者域,还需要 DMARC 的标识对齐。

SPF 与 SMTP 对端授权

SPF(Sender Policy Framework)让域名管理者通过 DNS 发布发信授权规则。收件服务器可以在 SMTP 会话中拿本次连接的对端 IP 来检查这些规则。

回到 Alice 的报告邮件。前面查 MX 得到的 192.0.2.25 是收件服务器 mx1.receiver.example 的地址,发件服务器用它来建立连接。连接进来后,mx1 看到对端的 IP 是 198.51.100.27,又收到 MAIL FROM:<[email protected]>。于是它取 @ 后面的 bounces.sender.example 查询 TXT 记录,检查该域是否授权 198.51.100.27 发信。邮件头里的 From: Alice <[email protected]> 会在之后随消息内容送来,其中的 sender.example 是后面做 DMARC 对齐时要用的作者域。

SPF 检查使用的 SMTP 身份

SPF 定义了 MAIL FROM 和 HELO 两种身份。用 MAIL FROM 检查时,输入包括当前连接的 IP、信封地址及它的域名。若另外检查 EHLO smtp.sender.example 声明的主机身份,就用同一个连接 IP 检查 smtp.sender.example 的授权记录。两种检查应各自记录,稍后做 DMARC 对齐的是 SPF 验证的 MAIL FROM 身份。退信使用 MAIL FROM:<> 时,SPF 以 postmaster@<HELO 域> 构造该次 MAIL FROM 身份。例如 EHLO smtp.sender.example 对应 [email protected]。

从一条 TXT 记录算出 SPF 结果

以下记录是假设的 DNS 配置,域名和 IP 均用于文档示例。

bounces.sender.example.  IN TXT "v=spf1 ip4:198.51.100.27 include:_spf.vendor.example -all"
_spf.vendor.example.     IN TXT "v=spf1 ip4:203.0.113.0/28 -all"

接收端筛出版本标记为 v=spf1 的 TXT,然后从左往右检查规则。匹配后,用该规则前面的限定符给出结果。ip4:198.51.100.27 匹配本次对端 IP,省略限定符视为 +,所以得到 pass,检查到这里就结束了。

如果对端换成 203.0.113.7,第一项的 IP 就对不上。下一项 include:_spf.vendor.example 让接收端查询供应商的 SPF 记录,并用同一个对端 IP 检查那条记录。203.0.113.0/28 覆盖 203.0.113.0 到 203.0.113.15,.7 落在其中。供应商记录返回 pass,于是原记录里的 include 也匹配,并按默认的 + 给出 pass。这个结果表示 203.0.113.7 获准使用 [email protected] 作为信封发件人。

写法 匹配或处理方式
ip4、ip6 用指定 IP 或 CIDR 网段匹配对端地址。
a、mx a 比较目标域名的 A/AAAA 地址。mx 先查目标域名的 MX,再比较各 MX 主机的地址。未显式指定目标时,两者使用正在求值的 SPF 域。
include 对目标域递归求值,子结果为 pass 时命中这一项。子结果为 fail、softfail 或 neutral 时继续外层求值。子结果为 none 时返回 permerror,子结果为 temperror 或 permerror 时向外传播。
exists、ptr exists 检查目标名称是否有 A 记录,目标名称可以用宏构造。ptr 把正向回查确认过的主机名用于 SPF 域名匹配。规范不建议在 SPF 策略里使用这项机制。
all 总是匹配,通常放在记录尾部,例如 -all、~all。
redirect=、exp= 二者是修饰符。前者在本记录没有机制匹配时改用目标域规则,记录中出现 all 时忽略 redirect。后者可为 fail 提供解释。

一次 SPF 检查的 DNS 查找上限

一次 SPF 检查最多可以处理 10 个需要查 DNS 的规则项,包括 include、a、mx、ptr、exists 和 redirect。处理到 include 时,原记录中的这条 include 计 1 项,接下来处理被引用的记录时,遇到的同类规则继续累加。比如前面的 include:_spf.vendor.example 计 1 项,如果供应商记录还引用了另一个域,处理到第二条 include 时累计就是 2 项。处理到第 11 项时,SPF 返回 permerror。

这里计的是检查过程中实际处理到的规则项。比如 198.51.100.27 在第一项 ip4 就匹配,后面的 include 根本没有执行。ip4、ip6 和 all 本身无需查 DNS,也不计入。反过来,一项 mx 可能要先查邮件服务器名,再查这些主机的地址,因此实际发出的 DNS 请求数可能多于规则项数。SPF 另外限制 mx 的地址查找次数,并建议把域名不存在或查询成功却无记录的空查询限制为两次。同一个域若返回多条可选的 v=spf1 TXT,也会得到 permerror。

SPF 的七种结果

结果 含义
pass 匹配 + 规则,域策略授权当前 IP 使用所检查的身份。
fail 匹配 - 规则,域策略明确不授权当前 IP。
softfail 匹配 ~ 规则,发布方认为该 IP 大概未获授权。
neutral 匹配 ? 规则,或有记录但求值落到缺省结果,发布方未对这个 IP 作出授权断言。
none 没有可用的 SPF 记录,或缺少可用于查询的有效域名。
temperror 临时错误,例如 DNS 服务暂时不可用。
permerror 策略无法正常解释,例如语法出错、发布了多条 SPF 记录或求值超限。

邮件转投后的 SPF 结果

Alice 发信时,她的发件服务器以 198.51.100.27 连接 Bob 的收件服务器,SPF 通过。现在 Bob 的邮箱启用了自动转投,将报告再送给 [email protected]。这次由 Bob 的邮件系统发起 SMTP 连接,归档服务器看到的对端 IP 是 203.0.113.77。假设转投时保留了原来的 MAIL FROM:<[email protected]>,归档服务器会查同一条 bounces.sender.example 的 SPF 记录,只是换成新的 IP 来检查。

  1. 先比 ip4:198.51.100.27。这条只授权 198.51.100.27,和现在的 203.0.113.77 对不上。
  2. 再看 include:_spf.vendor.example。归档服务器查到供应商授权的范围是 203.0.113.0 到 203.0.113.15。.77 在这个范围之外,供应商记录返回 fail,原记录便继续检查下一项。
  3. 原记录最后的 -all 适用于前面没有匹配的 IP,给出 fail。在这次连接里,bounces.sender.example 的 SPF 记录没有授权 203.0.113.77 使用该信封发件人。

归档服务器用转投服务器的 IP 求得 SPF fail。转投服务沿用原邮件内容,归档邮箱打开报告时仍显示 From: Alice <[email protected]>。

部分转投服务用 SRS(Sender Rewriting Scheme)处理这种情况。它把 MAIL FROM 换成自己控制的域,例如 forwarder.receiver.example,并在地址里保留映射所需的信息。转投方需要在该域授权自己的发信 IP。归档服务若得出 SPF pass,得到的是重写后信封域的授权结果。之后真有退信,转投服务再把它映射回原发件方。Alice 的 From: 仍属于 sender.example,DMARC 会另外判断能否对齐。

DKIM 与邮件数字签名

还是刚才那封转投到归档邮箱的报告。SPF 用的是转投服务的 IP,而 Alice 的发件系统可以把签名随原信一起发送。只要转投没有改变签名覆盖的内容,归档服务器仍可核对原签名。DKIM(DomainKeys Identified Mail)让签名域对所覆盖的消息内容承担责任,验签所需的公钥从签名域的 DNS 取得。

在本例中,发件系统可以使用 sender.example 域的私钥签名,选择器设为 report2026。收件端读到 d=sender.example 与 s=report2026,便查 report2026._domainkey.sender.example 的 TXT。选择器让同一个域并行发布不同公钥,也便于换钥时让新旧公钥分别对应新旧邮件。

签名头与 DNS 公钥

下面的签名头对应「带附件的原始邮件」,它的 Message-ID 是 <[email protected]>。bh= 用该示例的正文实际计算,b= 省略了依赖私钥的签名值。

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=sender.example; s=report2026;
 h=from:to:cc:reply-to:date:subject:message-id:mime-version:content-type;
 bh=friDRiMQoIjyudDH5LFJxGShUyoBSthWzDe+910CkNw=;
 b=<省略实际签名>

对应的 DNS 记录写成下面这种形式,p= 中填 Base64 公钥。私钥留在签名系统里。

report2026._domainkey.sender.example. IN TXT "v=DKIM1; k=rsa; p=<Base64 公钥>"
签名标签 作用
v=、a= DKIM 版本和签名算法。rsa-sha256 使用 RSA 与 SHA-256。RSA 签名密钥至少 1024 位,规范建议至少 2048 位,rsa-sha1 已禁用。另有 ed25519-sha256。
d=、s= 签名域和选择器,共同决定 DNS 查询名。d= 是 DKIM 给 DMARC 提供的域身份。
h= 按顺序列出纳入签名的头字段,必须包含 From。
c=、l= c= 指定头/正文各自的规范化方法,默认 simple/simple。l= 可把正文覆盖范围限制为规范化后的前若干字节,省略时覆盖整个规范化正文。
bh=、b= bh= 是正文哈希的 Base64 表示。b= 是实际数字签名。
i=、t=、x= 可选身份标识、签名时间及失效时间。i= 的域要等于或隶属 d=,域名对齐仍使用 d=。

签名头和公钥记录各有自己的标签。比如 DNS 中公钥记录的 p= 是公钥内容,若明确留空则表示该密钥已撤销。签名头中的 t= 是签名时间,而公钥记录中的 t= 是一组标志。RFC 6376 定义这些字段,算法与密钥要求要同时看后续更新。

正文哈希和签名输入

  1. 取邮件头之后的整段正文,按 c= 里的正文规则规范化,然后算哈希写进 bh=。对上面的 multipart/mixed 样例来说,参与计算的是原始的 MIME 分界线、各子部分的头、传输编码后的正文和附件字节表示。客户端渲染出来的 HTML 或解码后的附件文件属于另一个层次。
  2. 按 h= 指定的顺序取头字段,按 c= 的头部规则规范化。把本条 DKIM-Signature 也接到签名输入里,计算时把 b= 的值视为空,保留 bh= 等其他标签。这里加入的是当前这条签名头,且不加入它自己的末尾 CRLF。
  3. 用私钥对这些输入签名,将签名结果作 Base64 编码写回 b=。接收端取得公钥后重算正文哈希,并对签名头和 h= 所选字段验签。正文哈希与签名都核对成功,这一条 DKIM 签名才成立。

DKIM 签名与验证。正文哈希写入 bh=,随本条签名头和 h= 选中的头字段参与 b= 的计算。收件端分别比对正文哈希并用 DNS 公钥验签。

将「带附件的原始邮件」中的 eml 代码块原样保存为 report.eml,从 From: 那行开始,到 --outer-report-- 结束。把下面的脚本保存为 body_hash.py。它读取文件的原始字节,按 CRLF 生成哈希输入。邮件库重新序列化消息,或者先解码 MIME 再拼回正文,都可能改变这些字节。

import base64
import hashlib
import re
import sys
from pathlib import Path

raw = Path(sys.argv[1]).read_bytes().replace(b"\r\n", b"\n")
_, body = raw.split(b"\n\n", 1)
lines = [re.sub(rb"[ \t]+", b" ", line.rstrip(b" \t"))
         for line in body.split(b"\n")]
while lines and lines[-1] == b"":
    lines.pop()
canonical = b"\r\n".join(lines) + b"\r\n" if lines else b""
print(base64.b64encode(hashlib.sha256(canonical).digest()).decode())

执行 python body_hash.py report.eml,会得到 friDRiMQoIjyudDH5LFJxGShUyoBSthWzDe+910CkNw=。把主题从「测试邮件」改为「报告完成」时,这个正文哈希仍一样。如果原信已按示例的 h= 签过 Subject:,修改主题后的签名验证就会失败。如果给正文加一句说明或者换掉附件,重新计算的 bh= 就会变。

这段代码演示 relaxed 正文的 SHA-256,且本例没有 l=。实际验签还需要核对公钥、选取正确的原始头字段、执行签名算法。

规范化、重复字段与多重签名

规范化是把特定的排版差异映射成相同签名输入。simple 头部按原样计算。relaxed 头部会将字段名转为小写,展开折行,压缩连续的空格或 Tab,并去掉冒号周围及字段值末尾的这类空白。simple 正文去掉末尾的额外空行,空正文按一个 CRLF 计算。relaxed 正文还会去除每行行尾的空格或 Tab,并把行内连续的空格或 Tab 压为一个空格,完全空的正文按零字节计算。因此,把 Subject: 折行或改一处多余空白有机会在 relaxed 模式下保持有效,改主题里的文字、在正文中插入警告条或重新做 MIME 编码通常会改变已签内容。规范化只用于计算,原邮件仍按自己的字节传输。

邮件有同名字段时,DKIM 会从消息中最后一个仍未被选中的同名字段开始取。如果 h=received:received:from,就先取最靠近正文的那条 Received,再取倒数第二条,最后取 From。对于按规范只能出现一次的字段,签名方还可以在 h= 中多列一次,例如原信只有一个 From,签名列出 from:from。第二个位置当时没有对应字段。如果后来有人插入另一条 From,接收端的签名输入就会变化,原签名无法通过。这种做法通常叫 oversigning。

同一封邮件可以带多条 DKIM-Signature。作者域的出站服务器可以签一次,代发平台或邮件列表还可以用自己的域再签一次。接收方分别验证,每条签名各自有 d=、s= 与覆盖范围。转发只追加未纳入签名的追踪字段时,原签名可以继续有效。若邮件列表改了已签主题或正文,原签名可能失效,列表域的新签名也只证明列表域签过修改后的内容。后续做 DMARC 时,只要有一条通过且签名域对齐的 DKIM 路径即可,另一个域的有效签名不能替代这次对齐。

DMARC 作者域对齐与收件策略

假设第三方平台代发 Alice 的邮件。SPF 通过意味着连接 IP 获准使用信封域,DKIM 通过意味着某个签名域的签名有效。此时 Bob 在 From: 中读到的作者地址仍是 [email protected]。收件方还要确定,已经认证的信封域或签名域,有没有一项能用于验证这个作者域。DMARC(Domain-based Message Authentication, Reporting, and Conformance)就是在做这次比较。仍以 Alice 的邮件为例,三个域可以分别来自下面的位置。

从哪里取值 示例 在 DMARC 中的作用
邮件头 From: 中作者地址的域名 Alice <[email protected]> → sender.example 作者域(Author Domain)。接收方从这个域开始查策略,也是后续对齐的基准。
当前 SMTP 信封的 MAIL FROM 域 [email protected] → bounces.sender.example 这条 MAIL FROM 身份通过 SPF 后,其域才是可以用来对齐的 SPF 认证域。
每条有效 DKIM 签名的 d= d=vendor.example → vendor.example 每条通过验证的签名分别提供一个 DKIM 认证域。一封邮件可以有多条签名。

接收方从结构化的 From: 邮箱地址提取域名。比如 From: "运维中心 <[email protected]>" <[email protected]> 解析出的显示名是 运维中心 <[email protected]>,作者地址是 [email protected],对齐基准就是 vendor.example。即使这封信通过了 vendor.example 的 DMARC,结论也只覆盖这个作者域。常规评估要求能从 From: 提取出一个明确的作者域。缺失、重复或出现多个不同域的异常头,需要单独处理。

两条认证路径

假设这次实际用的是 [email protected],平台用 d=vendor.example 签名,邮件头仍写 Alice 的 [email protected]。两个代发域的检查都通过,收件网关却可以记录下面这组结果。

Authentication-Results: mx1.receiver.example;
 spf=pass [email protected];
 dkim=pass header.d=vendor.example;
 dmarc=fail header.from=sender.example

先看每项结果后面的域。SPF 通过的是 mailer.example,DKIM 通过的是 vendor.example。DMARC 要验证的是 sender.example。在本例的组织域关系下,两项 pass 都没有为 sender.example 提供一个对齐的域,所以最后一项是 fail。

找到适用的 DMARC 记录、可以完成评估时,接收方按下面的关系判定。SPF 路径用的是 MAIL FROM 身份的 SPF 结果,空反向路径按 SPF 的构造规则处理。单独的 HELO 检查即使通过,也只说明 HELO 身份的检查结果。

SPF 路径  = MAIL FROM 的 SPF 通过 AND 该认证域与作者域对齐
DKIM 路径 = 存在至少一条 DKIM 验证通过 AND 其 d= 与作者域对齐的签名

DMARC 通过 = SPF 路径成立 OR DKIM 路径成立

SPF 与 DKIM 分别验证后,与 From 作者域对齐的两条 DMARC 路径

下面固定邮件头的作者域为 sender.example,假设该域发布了适用策略。每行代表一种可能的投递条件。

SPF 结果与认证域 DKIM 结果与签名域 DMARC 结果
pass,mailer.example,不对齐 pass,vendor.example,不对齐 fail,两项单独认证都成功,但两条对齐路径都不成立。
fail,没有可用的 SPF 认证域 pass,sender.example,对齐 pass,DKIM 路径成立。
pass,bounces.sender.example pass,vendor.example,不对齐 若 SPF 使用宽松对齐且两域的组织域都是 sender.example,则为 pass。若 SPF 改用严格对齐,则为 fail。
pass,sender.example,对齐 多条签名都失败 pass,SPF 路径成立。

域所有者可以让第三方使用自己的退信子域,或授权第三方用对齐的域名签名。举例来说,平台原本使用 vendor.example 签名,随后为 sender.example 增加一条有效签名,两条签名可以并存。接收方仍对每条签名分别验签,任何一条通过且对齐即可让 DKIM 路径成立。

DNS Tree Walk 与组织域

严格对齐要求域名相同。宽松对齐比较各自的组织域(Organizational Domain)。adkim 和 aspf 分别指定 DKIM 与 SPF 的对齐模式,r 为宽松且是默认值,s 为严格。现行规范 RFC 9989 使用 DNS Tree Walk 确定组织域,取代了旧版基于公共后缀列表(PSL)的做法。子域的归属由查询结果决定,域名有相同后缀也可能分属不同的组织域。

假设接收方只查到以下 DMARC 记录。bounces.sender.example 是已存在的退信子域。

_dmarc.sender.example.  IN TXT "v=DMARC1; p=none; psd=n; aspf=r; adkim=s; rua=mailto:[email protected]"
_dmarc.vendor.example.  IN TXT "v=DMARC1; p=none; psd=n"
  1. 作者域是 sender.example。接收方查询 _dmarc.sender.example,直接取得适用策略。记录的 psd=n 指明该域是自己和相应子域的组织域。
  2. SPF 若通过,认证域是 bounces.sender.example。宽松对齐时从 _dmarc.bounces.sender.example 开始查。该处没有策略记录,向上查到 _dmarc.sender.example 的 psd=n,组织域也是 sender.example,因此对齐。
  3. DKIM 若以 d=vendor.example 通过,adkim=s 直接比较 vendor.example 与 sender.example,得出不对齐。假如改用宽松模式,两域分别落在自己的 psd=n 记录,仍不对齐。
  4. 此时 SPF 路径成立,DMARC 通过。将作者域记录里的 aspf 改为 s 后,bounces.sender.example 和 sender.example 也不对齐,两条路径都不成立。

接收方优先采用作者域自己的有效 DMARC 记录。做宽松对齐时,接收方比较参与对齐的域各自归属哪个组织域,必要时继续向上查询。Tree Walk 沿父域查找 _dmarc.<域名>,命中有效的 psd=n 或 psd=y 记录可停止继续向上查。作为祖先边界的 psd=y 表示公共后缀,向起始域方向下一层作为组织域。若没有这样的标记,通常从找到有效记录的域中取标签最少的一个。对齐查找无法据此确定组织域时,以初始目标域为组织域。域名标签过多时规范还规定跳过部分层级,限制查询开销。

DNS 策略、认证结果和最终动作

DMARC 记录发布在 _dmarc.<域名> 的 TXT 记录里,首个标签是 v=DMARC1。p 描述域所有者对认证失败邮件的处置偏好,取值为 none、quarantine 或 reject。例如给 _dmarc.sender.example 发布下面的记录。

_dmarc.sender.example.  IN TXT "v=DMARC1; p=none; sp=quarantine; np=reject; psd=n; t=y; rua=mailto:[email protected]"
标签 作用
p 当前域的处置偏好,也可作为继承策略的默认值。none 表示不表达特殊处置偏好,quarantine 表示将失败邮件视为可疑,reject 表示域所有者认为失败体现了域名的无效使用。
sp / np 继承策略时,分别为已存在与不存在的子域指定偏好。子域若有自己的适用记录,则直接取该记录的 p。域名存在但没有自己的 DMARC TXT 记录,也可以继承策略。
adkim / aspf 分别控制 DKIM、SPF 的对齐方式。
t 测试开关。t=y 请求将失败邮件的策略降一级,reject 降为 quarantine,quarantine 降为 none。认证计算和报告不受此开关影响。默认 t=n。
rua / ruf 分别指定聚合报告和逐封失败报告的接收 URI。常见值为 mailto: 地址。

以上面的记录为例,From: 使用 sender.example 时采用 p=none。若作者地址使用存在的 bounces.sender.example,且子域没有自己的记录,则继承 sp=quarantine。若使用不存在的 absent.sender.example,则继承 np=reject。两种继承策略均要再考虑 t=y 的降级。p=none 仍可得到 dmarc=pass 或 dmarc=fail,并按条件生成报告。dmarc=none 表示没有发现适用的 DMARC 策略。DNS 暂时故障则应与确实没有策略区分。

收件端最后决定是否接收、隔离或拒绝。例如邮件列表改写主题和正文后,原来对齐的 DKIM 签名可能失效,而重新投递使用的 SPF 域也可能无法对齐。旧版 RFC 7489 同样允许接收方基于本地策略处理邮件。2026 年 5 月发布的 RFC 9989 取代 RFC 7489 和实验性的 RFC 9091,并明确要求接收方不得仅因作者域发布了 p=reject 而拒收。对 DMARC 失败且发布了 p=reject 的邮件,如果缺乏其他信息和分析,接收方须按 quarantine 处理。新版同时弃用了旧记录里的 pct、rf 和 ri。

如果收件服务器选择在 SMTP 阶段拒收,可以在收到完整 DATA 后以 5xx 应答。如果它接受 SMTP 投递,再把邮件放入隔离区,发送方看到的仍是接受应答。dmarc=pass 说明作者域验证通过,接收方还可以按自身的内容策略决定邮件的去向。

对发件域也有一条要求。按 RFC 9989,作者域采用 p=reject 时,域所有者须为邮件应用有效的 DKIM 签名,不能只靠 SPF 保证通过。单封邮件仍按前面的两条路径求结果,发件域则要考虑邮件跨过自动转发后,原信封身份可能无法继续通过 SPF。

聚合报告与失败报告

rua 请求接收方发送聚合报告。报告按时间段和发信来源汇总,通常能看到来源 IP、邮件数量、认证域、SPF/DKIM 对齐结果和接收方记录的策略处置。它可以用来发现哪些平台在使用本域发信,以及是否存在漏配置的合法流量。若 rua 指向外部组织,接收方还要验证报告接收方的 DNS 授权。例如策略来自 sender.example,报告发给 [email protected],可由报告接收方发布 sender.example._report._dmarc.vendor.example 的授权 TXT 记录。

ruf 请求发送逐封失败报告,fo 细分触发条件。失败报告可能带原邮件头、诊断信息或部分原邮件内容,接收方可能脱敏或选择不发送。聚合报告也有观测范围,比如收件服务器在 DATA 之前就拒绝了 SMTP 事务,此时还没有读到 From:,这次投递就不会出现在作者域的 DMARC 聚合报告里。

ARC 中转认证链

[email protected] 这个邮件列表接到 Alice 的原信时,假设已经验证出 dkim=pass 和 dmarc=pass。列表给主题加上 [Reports],给正文附上退订说明,再用自己的信封域投递给订阅者。下一跳的 SPF 可能通过,但通过的是列表的域。原来对齐的 DKIM 签名因为主题或正文变化而失效,From: 仍写 Alice。这时下一跳重新计算 DMARC 可能得到 fail。ARC(Authenticated Received Chain)让列表能把改动前看到的认证结果签下来,随处理过的邮件交给下一跳。

每经过一个添加 ARC 的节点,就可以多一组共享序号 i= 的字段。ARC-Authentication-Results(AAR)记录该节点观察到的认证结果。ARC-Message-Signature(AMS)对该节点完成处理后的消息签名,签名方法与 DKIM 相近。ARC-Seal(AS)封签 ARC 字段并将各组关联,cv= 表示该封签节点所见的既有链状态。第一组使用 i=1; cv=none,验证了已有链并扩展它的下一组可使用 i=2; cv=pass。接收方会检查组号是否连续、每组三类字段是否齐全,并验证最新的 AMS 与链上的封签。

链验证通过,说明这些记录可以归于相应的封签者,接收方仍需判断封签者是否可信。若接收方信任邮件列表服务,就可以把它记录的原始认证评估纳入处置决策。比如本地仍记录 dmarc=fail,同时记录 arc=pass,结合可信中转路径决定投递。这两条结果分别描述当前消息的域对齐验证和 ARC 链验证。

消息签名与加密

TLS 保护相邻服务器间的连接,DKIM 让收件方验证签名域对所覆盖消息的声明。Alice 如果还想让 Bob 在客户端验证她的内容签名,或者让只有持有对应解密密钥的一方能阅读报告,可以把签名或密文直接组织成 MIME 部分。

S/MIME 使用 CMS(Cryptographic Message Syntax)。一种分离签名形式是 multipart/signed。第一个子部分是被签名的 MIME 实体,正文和附件仍可作为其中的内容读出来。第二个子部分为 application/pkcs7-signature,携带 CMS 签名。验签方从签名中取得相应信息,再按证书信任关系检查签名身份。S/MIME 也可把已签内容和签名放在一个 CMS SignedData 对象里,用 application/pkcs7-mime; smime-type=signed-data 传输。

S/MIME 加密时,先把要保护的正文和附件组织为一份 MIME 实体,再加密进 CMS EnvelopedData。CMS 同时携带各收件人取得内容加密密钥所需的信息,外层通常写作 application/pkcs7-mime; smime-type=enveloped-data,二进制 CMS 数据可用 Base64 传输。Bob 的客户端解密后,才拿到内部的 MIME 头、正文和附件。

OpenPGP/MIME 用另一套 MIME 封装携带 OpenPGP 数据。分离签名同样用 multipart/signed,签名部分类型为 application/pgp-signature。加密使用 multipart/encrypted; protocol="application/pgp-encrypted",第一个子部分是 application/pgp-encrypted,内容为版本控制行 Version: 1,第二个子部分是携带密文的 application/octet-stream。收件客户端先按 MIME 拆出密文,再用 OpenPGP 解密内部消息。SMTP 仍根据外层信封投递,外层可见的邮件字段与内部受保护内容应分别读取。

原始邮件头案例

有些客户端在邮件列表中只显示发件人名称。名称里恰好还有一段看起来像邮箱的字符串时,单看界面容易误读。下面这份示意邮件头保留了这种现象,也包含了提交、中转和入站接收留下的痕迹。域名、地址、时间和队列 ID 均为示例。

Received: from relay.mailer.example (relay.mailer.example [203.0.113.25])
 by mx1.receiver.example with ESMTPS id IN14052
 for <[email protected]>; Mon, 9 Nov 2026 10:05:12 +0800
Received: from localhost (localhost [127.0.0.1])
 by relay.mailer.example with ESMTP id RELAY002;
 Mon, 9 Nov 2026 10:04:23 +0800
X-Filter-Note: Missing Date at content-filter entry
Received: from relay.mailer.example ([127.0.0.1])
 by filter.mailer.example with ESMTP id FILTER001;
 Mon, 9 Nov 2026 10:04:19 +0800
Received: from localhost (unknown [198.51.100.88])
 by relay.mailer.example with ESMTPSA id RELAY001;
 Mon, 9 Nov 2026 10:04:18 +0800
X-Gateway-Envelope-From: [email protected]
From: =?UTF-8?B?SVQg5pSv5oyBIDxpdEBjb3JwLmV4YW1wbGU+?=
 <[email protected]>
Reply-To: [email protected]
To: Bob <[email protected]>
Subject: Service desk notification
Message-ID: <[email protected]>
Date: Mon, 9 Nov 2026 10:04:23 +0800

提取作者地址和回复地址

From: 中的 encoded-word 解码后得到显示名 IT 支持 <[email protected]>。字段续行中的 <[email protected]> 才是这个 From: 邮箱的地址。按照 RFC 5322 的结构解析,可以得到三个值。

读取内容 解析结果
发件人显示名 IT 支持 <[email protected]>
Header From 邮箱及作者域 [email protected],作者域为 mailer.example
回复地址 [email protected],来自 Reply-To:

解析时应先按字段语法识别显示名与地址,再分别处理其中的编码。直接搜索第一段包含 @ 的字符串,会把显示名中嵌入的地址错当成作者地址。这个例子的 X-Gateway-Envelope-From 提供了另一条线索。只有确认它由己方网关生成,并了解它记录的是哪一跳,才能把 [email protected] 当作该跳的信封地址。网关的 SMTP 日志可以按入站队列 ID IN14052 关联对应会话,查看当时的 MAIL FROM 和 RCPT TO。

按入站边界读 Received

假定 mx1.receiver.example 是自己管理的入站 MX,首条 Received 也能与本机日志对上,那么 203.0.113.25 就是这次入站 SMTP 连接的对端 IP。with ESMTPS 说明这一跳使用了 TLS,id IN14052 则是这台服务器的队列标识。

继续往下读,可以看到外部系统声称的投递路径。最下面的记录声称 198.51.100.88 通过 TLS 和 SMTP AUTH 向 relay.mailer.example 提交。这里的 from localhost 是连接方报出的名称,括号里的 198.51.100.88 才是该记录报告的对端地址。接着两条 127.0.0.1 记录与内容过滤再交回中继的路径相符。要确认这些上游步骤或认证账户,还需要相应系统的日志。X-Filter-Note 报告过滤时缺少 Date,最终头部又有 Date,两者可以来自不同处理阶段。仅凭这份副本无法确定补写字段的具体组件。

认证结论和内容线索

此处只有一段邮件头,缺少可信网关的认证结果,也没有完整的签名输入或邮件体。可以确定的是,DMARC 的作者域来自 [email protected],因此对齐基准是 mailer.example。如果该域发布了适用的 DMARC 记录,并授权这个投递 IP 使用同域反向路径,收件方就可能通过对齐的 SPF 路径得到 dmarc=pass。有效的同域 DKIM 签名也能提供一条通过路径。两种情况认证的都是 mailer.example 的使用权限,客户端显示名里写着的 corp.example 仍需按另一类身份线索判断。

这类样本可以把显示名中的受保护地址、实际作者域和回复地址分开记录。企业委托服务商发送通知时,合法邮件也可能出现跨域组合,所以需要结合已知的代发关系。拿到完整消息后,再按 MIME 结构拆开纯文本、HTML、内嵌资源和附件。HTML 链接要分别看界面上呈现的文字和 href 的真实目标,附件要分别看声明的文件名、Content-Type 与解码后的字节。到这一步才能区分已经观察到的身份冒用线索,以及仍待确认的链接或附件行为。

规范与资料索引

SMTP、路由与邮箱访问

连接加密和投递通知

邮件头、地址和导出格式

MIME、编码与内嵌资源

日历与内容签名

发件域认证、认证记录与转投

版权声明:除特殊说明,博客文章均为 Shule 原创,依据 CC BY-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇