#13 Python DKIM 签名

2019-04-16
  • pydkim
  • https://pypi.org/project/pydkim/
  • https://hewgill.com/pydkim/
  • NOTE: This page describes the last release of pydkim from 2008. The latest version is a fork found at dkimpy in Launchpad and is under active development.
  • 最新版本是 2008/06 发布的 v0.3
  • dkimpy
  • https://pypi.org/project/dkimpy/
  • https://launchpad.net/dkimpy
  • 最新版本是昨天发布的 v0.9.2
Date Version
2023-07-28 1.1.5
2023-05-12 1.1.4
2023-04-30 1.1.3
2023-04-09 1.1.2
2023-03-10 1.1.1
2023-02-25 1.1.0
2023-04-30 1.0.6
2020-08-09 1.0.5
2020-04-06 1.0.4
2020-01-15 1.0.3
2019-12-31 1.0.2
2019-12-15 1.0.1
2019-12-09 1.0.0
2019-12-24 0.9.6
2019-10-07 0.9.5
2019-09-25 0.9.4
2019-08-09 0.9.3
2019-04-15 0.9.2
2018-12-09 0.9.1
2018-10-30 0.9.0

说明

默认签名字段(28 个):

cc, content-description, content-id, content-transfer-encoding, content-type,
date,
from,
in-reply-to,
list-archive, list-help, list-id, list-owner, list-post, list-subscribe, list-unsubscribe,
message-id, mime-version,
references, reply-to, resent-cc, resent-date, resent-from, resent-message-id, resent-sender, resent-to,
sender, subject,
to
d = dkim.DKIM('')
print(b', '.join(d.should_sign | d.frozen_sign))
b'list-unsubscribe, content-id, list-id, mime-version, resent-date, sender, cc, reply-to, content-type, list-owner, resent-message-id, resent-cc, resent-from, to, content-description, date, list-post, in-reply-to, content-transfer-encoding, from, references, list-help, subject, list-archive, resent-sender, list-subscribe, message-id, resent-to'
print(b', '.join(sorted(d.should_sign | d.frozen_sign)))
b'cc, content-description, content-id, content-transfer-encoding, content-type, date, from, in-reply-to, list-archive, list-help, list-id, list-owner, list-post, list-subscribe, list-unsubscribe, message-id, mime-version, references, reply-to, resent-cc, resent-date, resent-from, resent-message-id, resent-sender, resent-to, sender, subject, to'

示例

准备实验用的密钥对。

openssl genpkey -algorithm RSA -out /tmp/private_key.pem
openssl rsa -in /tmp/private_key.pem -check
openssl rsa -pubout -in /tmp/private_key.pem -out /tmp/public_key.pem
import dkim

domain = 'mail.markjour.com'
selector = 's20190416'

with open('/tmp/private_key.pem', 'rb') as f:
    privkey = f.read().strip()
# dkim.parse_pem_private_key(privkey)

message = """
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=utf-8
Date: Mon, 24 Sep 2018 12:31:21 +0000 (UTC)
From: Admin <no-reply@mail.markjour.com>
Mime-Version: 1.0
Subject: Hello World
Message-ID: <n4F5zz24LXvYqPHVrZLPJokasT7MlLxYQx6g>
Reply-To: sender@mail.markjour.com
To: kwicoo@gmail.com
List-Unsubscribe: <mailto:unsubscribe@mail.markjour.com?p=Ahi2DRmdOnTdpsDzPClCPqbpwmFyjvGJV2xfJGWqw6eFEKRwI402QeoSsFrArTw1s48A59f60pLl0x71ojsQSWERnp3aMZA6YvEw>
X-SMTP-ID: c89cf6a5-22b7-4d1a-9bce-9f91a6be1bfb

HELLO WORLD
""".strip().encode()
# dkim.rfc822_parse(message)
# print(dkim.DKIM(message).default_sign_headers())
# [b'Content-Transfer-Encoding', b'Content-Type', b'Date', b'From', b'Mime-Version', b'Subject', b'Message-ID', b'Reply-To', b'To', b'List-Unsubscribe', b'From']

signature = dkim.sign(message, selector.encode(), domain.encode(), privkey)
print(signature.decode())
# DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=mail.markjour.com;
#  i=@mail.markjour.com; q=dns/txt; s=s20190416; t=1555392275;
#  h=content-transfer-encoding : content-type : date : from :
#  mime-version : subject : message-id : reply-to : to : list-unsubscribe
#  : from; bh=qg03cTlGc4OH4uPv7BGgoUyhgh23r+o1O6qzYOLixvA=;
#  b=sJ09G6hHPaP6AMp2mqUXjEZ+BfUFz0o6nbpXWxJ4/OG0o9ZwPSj8aJibZtJjTKP3k/TR/
#  6SD543V8iNw+JwwM+XLOUZa0iduK+QkedccqNl5Hcfc9UI/U11NoHz76B3csL9KE9tb40jF
#  mlLCuVUjci4HlOfEoKF8Ame8yWDHXVoNS/YT9/OSSc5q5q+qp6OX6PvzzxDomCHC6kbhOdv
#  Yc/KEXrMQ1JQ971pRUBNQK3eN7bV7g1BwXuMEuhdwDa4aZ4YYcakKywo4Oey7bIy1E7evZN
#  5rUitRExLH4dQNrhxoZd4c3QOjd4ROTwseAaMN10U/egzDXjcw2q0UUC1UKQ==

append_headers = [b'x-smtp-id']

d = dkim.DKIM(message)
include_headers = d.default_sign_headers()
include_headers.extend(append_headers)
signature = d.sign(selector.encode(), domain.encode(), privkey, include_headers=include_headers)
print(signature.decode())
# DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=mail.markjour.com;
#  i=@mail.markjour.com; q=dns/txt; s=s20190416; t=1555392275;
#  h=content-transfer-encoding : content-type : date : from :
#  mime-version : subject : message-id : reply-to : to : list-unsubscribe
#  : from : x-smtp-id; bh=qg03cTlGc4OH4uPv7BGgoUyhgh23r+o1O6qzYOLixvA=;
#  b=MTSeE8X3R+8bn+kkJaX5j/OKPMe+sdombmmwK5zME3SHBqiOLbxCwOGyh3qJKXdLpJlEg
#  pBnsDmNEjgC/rtBoclvnlCsaN7OFcZIe6ehfjwGeaw41r38Y8IgUQCkuN+IiL8FN1IiMI2f
#  kSayumwcOCAwmA4yJfu8n1v4W416jXt775YKR+1bt2Df1fNA6FnfoSMTqZl7rHn9zo76Efg
#  yvm7M0uT3uz0NZbJtqOnMFzRri9TEj4jYiCgsNaBYA9prbZlA02svoJx9qIJ2mKA+EcVpxK
#  IsEAY4ZXzXfhynKLeOYGK786ghiZrtsQYGbP6c1fAzTNy+fLJzRFozsV/wEQ==
headers = set(dkim.DKIM.SHOULD) | set(dkim.DKIM.FROZEN) | append_headers - set(dkim.DKIM.SHOULD_NOT)
signature = dkim.sign(message, selector.encode(), domain.encode(), privkey, include_headers=headers)
print(signature.decode())
# DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=mail.markjour.com;
#  i=@mail.markjour.com; q=dns/txt; s=s20190416; t=1555392275;
#  h=in-reply-to : x-smtp-id : content-type : cc : content-id : list-post
#  : date : resent-from : list-owner : list-id : to : content-description
#  : list-subscribe : message-id : sender : mime-version :
#  resent-message-id : list-help : content-transfer-encoding : resent-cc
#  : resent-date : list-unsubscribe : references : resent-sender : from :
#  list-archive : subject : resent-to : reply-to;
#  bh=qg03cTlGc4OH4uPv7BGgoUyhgh23r+o1O6qzYOLixvA=;
#  b=lLJafHJ8B/DoO4FncLp+BIHaPy4xsq7dRAWjzAvkRoDSwjcg3EloW0FsCXS45EkmwmBZC
#  Vks7zeOR1CS8oxcpauhxj1XnlwfcwLWtAQ3pogQTzNh4EEUFiNfgJTdXefAh7cpGHolQmy7
#  w2TBXDPx+Ikynw2tNnGOBduLWi+BH3Et8KGaskR4D9QHWSrk4pqeaNannNhDPUfE98d2fS3
#  kKBvqiEaTubQBdi8VXcl8J4R1SfdJZR2NfBJkPjJejlwTJaSytF2zyberpgflj0sEc8iHvM
#  2UQRcpxqm8GMRyzzKAXBSTzQhmaTOHntGokDTunNlUc/izFFRJm9SFiVq64g==

OpenDKIM

使用 opendkim-genkey 生成签名私钥和 DNS 配置文件:

$ opendkim-genkey --verbose --domain=mail.markjour.com --selector=s20190416 --directory=/tmp/
opendkim-genkey: generating private key
opendkim-genkey: private key written to s20190416.private
opendkim-genkey: extracting public key
opendkim-genkey: DNS TXT record written to s20190416.txt

域名配置好之后,可以使用 opendkim-testkey 检查:

$ opendkim-testkey -d test.markjour.com -k /tmp/private_key.pem -s s20190416 -v
opendkim-testkey: 's20190416._domainkey.test.markjour.com' record not found

#12 电子邮件的一些小技巧

2019-01-14

紧急程度 X-Priority

有另一个 Priority 头,不知道为什么没有使用。

  • 1:最高
  • 2:高级
  • 3:一般
  • 4:低级
  • 5:最低

根据网上的一些资料,这几个头已经被垃圾邮件滥用,导致可能会被拦截。
不过,企业邮箱使用应该没有问题。

网易邮箱发送的时候勾选重要,就会设置 X-Priority: 1

需要发送回执 Disposition-Notification-To

效果就是客户端会提示:“是否发送回执”,确认 or 取消。
注意,不是所有客户端会支持回执。

mdn-request-header = "Disposition-Notification-To" ":" 1#mailbox
# mailbox 就是 local-poart@domain 这种格式
# RFC 中的这个 1# 我也不知道是干嘛用的

值是正常的邮件地址格式就行了,比如:

Disposition-Notification-To: admin@example.com

Disposition-Notification-To: =?utf-8?b?566h55CG5ZGY?= admin@example.com

注意:会和 Return-Path 的域名部分做对比,如果不一致,不会发送回执。

参考:https://www.rfc-editor.org/rfc/rfc2298.html

回执参考

主要是有这么一段:

Content-Type: message/disposition-notification; name="MDNPart2.txt"
Content-Disposition: inline
Content-Transfer-Encoding: 7bit

Reporting-UA: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Thunderbird/91.7.0
Final-Recipient: rfc822;huang@example.com
Original-Message-ID: <2ec11bc0.59a.1803f61e0f9.Coremail.admin@example.com>
Disposition: manual-action/MDN-sent-manually; displayed

参考资料与拓展阅读

#11 日程邀请邮件

2019-01-10

介绍

iCal 历史

  1. Versit 联盟阶段

    1995 年,为了解决不同设备、不同程序之间交互个人数据(联系人、日程)困难的问题,苹果、AT&T、IBM 等公司组成 ​​Versit 联盟,旨在制定个人信息交换(PDI)的开放标准。
    后来,该组织设计了以下两个标准:

    • vCard,用于交换名片信息,包括姓名、地址、电话号码、邮箱地址等。
    • vCalender,用于交换日历信息,如日程、会议等。

    基本格式都是这样,只是里面的字段定义不同:

    BEGIN:type
    key1:value1
    key2:value2
    END:type
    
  2. IMC 联盟阶段

    1996 年,为了促进上面两项标准的推广,Versit 联盟将相关格式所有权移交给了互联网邮件联盟(Internet Mail Consortium,IMC)。
    PS:IMC 可能是 Versit 联盟成员拉拢更多公司组成的一个更大的联盟。
    PS:2002 年,IMC 组织关门。

    IMC closed down in 2002.

  3. IETF 阶段

    1998 年,IMC 将两个标准提交给 Internet Engineering Task Force (IETF) 标准化。
    PS:vCalendar 经过 IETF 标准化之后改名 iCalendar 了。

支持情况

经过测试,至少 QQ 和 网易邮箱的 WebMail 都提供了日程的支持。
Thunderbird 只能通过日历 App 打开附件 (ics 文件) 的方式添加日程。

效果

event mail

原理

在邮件中插入了一个 text/calendar 类型的附件:

Content-Type: text/calendar; charset=utf-8; method=REQUEST;
    name=ATT1547083200618.ics
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="ATT1547083200618.ics"

解析出来这样一个 iCal 文件:

BEGIN:VCALENDAR
PRODID:-//Netease//WebMail
VERSION:2.0
METHOD:REQUEST
BEGIN:VEVENT
SUMMARY:日程:上午十点的面试
LOCATION:小会议室
DTSTART:20190110T013000Z
DTEND:20190110T023000Z
UID:11c27eb7-adab-4549-8aea-efadcec7bb6c
SEQUENCE:0
STATUS:CONFIRMED
ORGANIZER;CN=张三:mailto:zhangsan@example.com
ATTENDEE:mailto:lisi@example.com
END:VEVENT
END:VCALENDAR

回复

这个回复就各异了,没有同意的格式。
QQ 邮件只会有一个 自动回复: xxxx,不知道是接受还是拒绝。
QQ 企业邮件会有详细的信息,附带了原日程,并有一句话:xxx 已经接受你的邀请: 日程:xxx

参考资料与拓展阅读

#10 电子邮件是如何传输的?

2018-07-22

假设:

  1. A 要发一封邮件给 B、C。
  2. A 的地址是 aaa@163.com,B 的邮箱地址是 bbb@163.com,C 的邮箱地址是 ccc@qq.com

那么:

  1. A 先在自己的客户端 Thunderbird 上(WebMail 后面是怎么处理的就取决于各个邮件服务提供商了)将邮件编辑并发送出去。
  2. 邮件经过 SMTP(需要验证身份),到了网易的邮件发送服务 smtp.163.com
  3. 网易邮件发送服务检查了两个收信地址,发现:
    B 地址所在域和自己一致,就直接投给了自己的用户邮箱
    C 地址所在域和自己不一致,就检查 C 域的 MX 记录,得到 mx.qq.com,然后将邮件投递过去(SMTP)。
  4. B 通过客户端 Foxmail,从配置的 pop3.163.com 下载了邮件(POP3),手机自带的邮件客户端配的 imap.163.com,那就走 IMAP 协议。
  5. QQ 的 mx 服务器是没有身份验证的,但是会通过一系列手段验证 163 的连接是可靠的,才会接收这封邮件。
  6. QQ 收到邮件之后,将邮件投进 C 的收件箱。
  7. C 则在 Web 上浏览邮件,这时候 WebMail 通过某种技术,从 QQ 邮箱服务拉取到了邮件列表,C 就发现这封邮件,然后点开看到了 WebMail 获取到的邮件内容。
  QQ 163  
SMTP smtp.qq.com 465/587 smtp.163.com 25, 465/587
POP3 pop.qq.com 995 pop.163.com 110, 995
IMAP imap.qq.com 993 imap.163.com 143, 993

玩概念

可以看到各种 MxA:

  • MUA: user 用户代理,也就是邮件客户端
  • WebMail 或者 Foxmail、Thunderbird 这样的工具。
  • MSA: submission 发送代理
  • 比如 smtp.163.com 这样的收信服务
  • MTA: transfer 传送代理,或者说交换代理,又叫 Mail Exchanger 或 MX Host
  • 接收别的邮件服务转过来的邮件
  • 和 MSA 的区别是没有身份认证,但是有对投递者(服务器)身份信息的检验
  • Postfix 服务器
  • MDA: delivery 投递代理
  • 主要是对 MSA/MTA 收到的邮件进行过滤处理:
    • 检查和过滤垃圾邮件、病毒邮件等,
    • 然后外部地址的邮件就交换出去,
    • 内部地址的邮件就存储起来
  • MRA: retrieval,receive 接收/检索代理
  • POP,IMAP 协议
  • Dovecot 服务器

#9 MIME 编码

2018-05-19

说明

MIME 是 Multipurpose Internet Mail Extensions 的缩写,多用途互联网邮件扩展 的意思。

最初的电子邮件标准 RFC 822 只支持发送 ASCII 字符文本内容,通过 MIME 这个拓展(RFC 2822),可以发送所有类型的内容。
后面的 HTTP 协议也是在 MIME 这个框架内构建的,Web 开发者比较熟悉的那几个 Content-XXX 头就是从 MIME 里面来的。

规范文件

RFC No. Type Title
RFC 2045 Standards Track Multipurpose Internet Mail Extensions (MIME) Part One:
Format of Internet Message Bodies
RFC 2046 Standards Track Multipurpose Internet Mail Extensions (MIME) Part Two:
Media Types
RFC 2047 Standards Track MIME (Multipurpose Internet Mail Extensions) Part Three:
Message Header Extensions for Non-ASCII Text
RFC 2048 Best Current Practice Multipurpose Internet Mail Extensions (MIME) Part Four:
Registration Procedures
RFC 2049 Standards Track Multipurpose Internet Mail Extensions (MIME) Part Five:
Conformance Criteria and Examples

语法

5 个核心头(RFC 2045)和 Content-Disposition 扩展(RFC 2183):

MIME-Version: 1.0                           // MIME 版本
Content-Type: [type]/[subtype]; parameter   // 内容类型
Content-Transfer-Encoding: [encoding]       // 内容传输编码
Content-ID: xxx
Content-Description: xxx                    // some descriptive information
Content-Disposition: [disposition]          // 内容配置

内容类型 Content-Type

又叫互联网媒体类型(Internet media type)或者 MIME 类型(MIME type)。
类型信息的注册事宜,由 IANA(Internet Assigned Numbers Authority)统一管理。

按照注册来源分成几种类型,只用关心标准数,这个了解一下就行了:

  • 标准树 类型名 / 子类型名 [ + 后缀 ] [ ; 可选参数 ]
  • 厂商树 类型名 / vnd.子类型名 [ + 后缀 ] [ ; 可选参数 ],例如:application/vnd.debian.binary-package
  • 个人树 类型名 / prs.子类型名 [ + 后缀 ] [ ; 可选参数 ]
  • 未注册的 x.树 类型名 / x.子类型名 [ + 后缀 ] [ ; 可选参数 ]

常见的类型

详细类型在 IANA 官网有,链接在下面参考资料部分我贴了一个。

Type Subtype Description
text plain 文本
text html HTML
text xml XML
text javascript JavaScript
text css CSS
text csv CSV
text vcard vCard 电子名片
image bmp
image jpeg
image png
image gif
image webp
image svg+xml
image icon
audio mpeg
audio mp4
audio ogg
audio webm
audio flac
video mpeg
video mp4
video ogg
video webm
application xml XML
application json JSON
application ecmascript
application javascript
application zip
application gzip
application pdf
application rss+xml
application atom+xml
application octet-stream

内容传输编码

编码类型主要是这几种:“7bit”,“8bit”,“binary”,“quoted-printable”,“base64”。

#6 Python 应用: 简易 SMTP 服务器

2018-05-07

Python2

python2 -m smtpd -h
# An RFC 2821 smtp proxy.
# Usage: /usr/lib/python2.7/smtpd.py [options] [localhost:localport [remotehost:remoteport]]
#     --nosetuid, -n
#     --version, -V
#     --class classname, -c classname
#     --debug, -d
#     --help, -h

Python3

python -m smtpd -h
# An RFC 5321 smtp proxy with optional RFC 1870 and RFC 6531 extensions.
# Usage: /usr/lib/python3.9/smtpd.py [options] [localhost:localport [remotehost:remoteport]]
#     --nosetuid, -n  默认会设置用户为 nobody,如果不是 root 会因权限不足失败
#     --version, -V
#     --class classname, -c classname 默认: PureProxy
#     --size limit, -s limit 消息大小限制(RFC 1870 SIZE extension),默认是 33554432 字节,即 32MB
#     --smtputf8, -u 启用 SMTPUTF8 扩展(RFC 6531)
#     --debug, -d
#     --help, -h

# 如果不指定主机,就使用 localhost
# 如果主机是 localhost,端口使用 8025
# 如果是其他主机,端口使用 25
python3 -m smtpd -n

# 默认的 PureProxy 会给转信出去,正常情况会被服务器拒绝
python3 -m smtpd -n -c smtpd.DebuggingServer

Python 3.9 的 PureProxy 有 BUG,会报 process_message() got an unexpected keyword argument 'mail_options'

自定义黑洞服务器

blackhole.py

import smtpd
import time
class BlackHoleServer(smtpd.SMTPServer):
    def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
        print('%s %s %s -> %s' % (time.strftime('%Y-%m-%d %H:%M:%S'), peer, mailfrom, rcpttos))

setup.py

import setuptools
setuptools.setup(name="blackhole", py_modules=["blackhole"])

附件下载:blackhole.zip

python setup.py install --user
python -m smtpd -n -c blackhole.BlackHoleServer

测试

import smtplib
smtp = smtplib.SMTP('localhost', 8025)
from_addr = 'admin@markjour.com'
to_addr = 'you@markjour.com'
smtp.sendmail(from_addr, to_addr,
    f"""From: {from_addr}\nTo: {to_addr}\nSubject: just4fun\n\nhello, world!""")

#5 邮件格式 (RFC 822)

2018-05-05

相关的文章:

邮件是由纯文本组成,其详细的格式有很多 RFC 规范需要遵守。我这里只能对我所了解的,也是基础的 —— 或者说最核心的 —— 格式做一个说明。

最核心的部分是 1982 年的 RFC 822 (STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES),之后又做过一些更新, 比如 RFC 2822RFC 5322 (Internet Message Format) 和一堆补丁更新。本文要讲的基本格式,从开始到现在并没有什么明显变化。

  1. 邮件是一种纯文本格式,最开始只包含 ASCII 字符,后来引入了 MIME 之后,可以制定别的编码,比如 UTF-8 等。

  2. 换行符是 \r\n,也就是 CR + LF

  3. 整体来说,一封邮件由邮件头(Headers)和邮件体(Payload)组成。

  4. 邮件头包含若干个头字段

  5. 邮件头和邮件体之间用一个空行隔开

  6. RFC2882 和 RFC5322 都规定了电子邮件每一行的长度,排除行末 CRLF,不可以超过 998 个字符,建议不超过 78 个字符。

There are two limits that this specification places on the number of
characters in a line.  Each line of characters MUST be no more than
998 characters, and SHOULD be no more than 78 characters, excluding
the CRLF.

如果太长,应该拆分成多行,下一行行首加上至少一个空格或者制表符,表示是上一行的延续。

邮件示例

From: Bob <bob@markjour.com>
To: Mark <mark@markjour.com>
Subject: Hello

Hello, Bob,

Would you like to join me for dinner?

--
Mark

RFC 822 中关于字符的定义

                                            ; (  Octal, Decimal.)
# 字符
CHAR        =  <any ASCII character>        ; (  0-177,  0.-127.)
# 字母
ALPHA       =  <any ASCII alphabetic character>
                                            ; (101-132, 65.- 90.)
                                            ; (141-172, 97.-122.)
# 数字
DIGIT       =  <any ASCII decimal digit>    ; ( 60- 71, 48.- 57.)
# 控制字符
CTL         =  <any ASCII control           ; (  0- 37,  0.- 31.)
                character and DEL>          ; (    177,     127.)
# 回车
CR          =  <ASCII CR, carriage return>  ; (     15,      13.)
# 换行
LF          =  <ASCII LF, linefeed>         ; (     12,      10.)
# 空格
SPACE       =  <ASCII SP, space>            ; (     40,      32.)
# 制表符
HTAB        =  <ASCII HT, horizontal-tab>   ; (     11,       9.)
# 引号
<">         =  <ASCII quote mark>           ; (     42,      34.)
# 回车换行
CRLF        =  CR LF
# 空白
LWSP-char   =  SPACE / HTAB                 ; semantics = SPACE
# 连贯空白, 折行空白
linear-white-space =  1*([CRLF] LWSP-char)  ; semantics = SPACE
                                            ; CRLF => folding
# 特殊字符
specials    =  "(" / ")" / "<" / ">" / "@"  ; Must be in quoted-
            /  "," / ";" / ":" / "\" / <">  ;  string, to use
            /  "." / "[" / "]"              ;  within a word.
# 分隔符
delimiters  =  specials / linear-white-space / comment
# 文本
text        =  <any CHAR, including bare    ; => atoms, specials,
                CR & bare LF, but NOT       ;  comments and
                including CRLF>             ;  quoted-strings are
                                            ;  NOT recognized.
# 原子字符
atom        =  1*<any CHAR except specials, SPACE and CTLs>
quoted-string = <"> *(qtext/quoted-pair) <">; Regular qtext or
                                            ;   quoted chars.
qtext       =  <any CHAR excepting <">,     ; => may be folded
                "\" & CR, and including
                linear-white-space>
domain-literal =  "[" *(dtext / quoted-pair) "]"
dtext       =  <any CHAR excluding "[",     ; => may be folded
                "]", "\" & CR, & including
                linear-white-space>
# 注释
comment     =  "(" *(ctext / quoted-pair / comment) ")"
ctext       =  <any CHAR excluding "(",     ; => may be folded
                ")", "\" & CR, & including
                linear-white-space>
quoted-pair =  "\" CHAR                     ; may quote any char
phrase      =  1*word                       ; Sequence of words
word        =  atom / quoted-string

对应上 ASCII:

  0 -  31 控制字符, 其中包括常用的:
          - HT ( 9)  水平制表符
          - LF (10) 换行
          - CR (13) 回车
       32 空格
 33 -  47 符号 !"#$%&'()*+,-./
 48 -  57 数字
 58 -  64 符号 :;<=>?@
 65 -  90 大写字母
 91 -  96 符号 [\]^_`
 97 - 122 小写字母
123 - 126 符号 {|}~
      127 控制字符(DEL)

CHAR 0-127
CTL  0-37 + 127

符号中:
()<>[]@,;:\".
    13 个被视作特殊字符,需要转义
!#$%&'*+-/=?^_`
    15 个就是普通符号

atom = 数字 + 字母 + 普通符号

邮件头格式

field       =  field-name ":" [ field-body ] CRLF
     field-name  =  1*<any CHAR, excluding CTLs, SPACE, and ":">
     field-body  =  field-body-contents
                    [CRLF LWSP-char field-body]
     field-body-contents =
                   <the ASCII characters making up the field-body, as
                    defined in the following sections, and consisting
                    of combinations of atom, quoted-string, and
                    specials tokens, or else consisting of texts>

字段名称允许使用的字符范围非常宽泛,而且大小写不敏感,

但是一般实践中:

  • 由字母和中划线组成
  • 首字母大写,或全部大写

常用字段

  • Received MTA 轨迹(传输过程中的相关信息)
  • Date 发信时间,格式:Fri, 21 Nov 1997 09:55:06 -0600
  • Sender Mail From 地址
  • From 发件人
  • Subject 邮件标题
  • To 收件人
  • Cc 抄送
  • Bcc 密送
  • Reply-To 回复地址
  • Message-ID 邮件标识
  • References 回复邮件标识,逗号隔开
  • In-Reply-To 回复邮件标识(会话发起的第一封)
  • Return-Path 发信任地址(2020/07/31,邮件的 Return-Path 头是什么
  • Comments 说明
  • Keywords 关键字

一般采用 X- 开头的字段名称表示自定义字段,或者叫拓展字段:

常见的拓展字段:

  • X-Mailer

#4 SMTP 拓展

2018-04-24

RFC#821 定义的 SMTP 协议非常简单(简陋)。
1993 年,RFC#1425 SMTP Service Extensions 定义了 SMTP 协议的拓展框架。
这个向前兼容的安全拓展框架是通过 EHLO 命令来实现。