Email
2019-01-10
介绍
iCal 历史
-
Versit 联盟阶段
1995 年,为了解决不同设备、不同程序之间交互个人数据(联系人、日程)困难的问题,苹果、AT&T、IBM 等公司组成 Versit 联盟,旨在制定个人信息交换(PDI)的开放标准。
后来,该组织设计了以下两个标准:
- vCard,用于交换名片信息,包括姓名、地址、电话号码、邮箱地址等。
- vCalender,用于交换日历信息,如日程、会议等。
基本格式都是这样,只是里面的字段定义不同:
BEGIN:type
key1:value1
key2:value2
END:type
-
IMC 联盟阶段
1996 年,为了促进上面两项标准的推广,Versit 联盟将相关格式所有权移交给了互联网邮件联盟(Internet Mail Consortium,IMC)。
PS:IMC 可能是 Versit 联盟成员拉拢更多公司组成的一个更大的联盟。
PS:2002 年,IMC 组织关门。
IMC closed down in 2002.
-
IETF 阶段
1998 年,IMC 将两个标准提交给 Internet Engineering Task Force (IETF) 标准化。
PS:vCalendar 经过 IETF 标准化之后改名 iCalendar 了。
支持情况
经过测试,至少 QQ 和 网易邮箱的 WebMail 都提供了日程的支持。
Thunderbird 只能通过日历 App 打开附件 (ics 文件) 的方式添加日程。
效果

原理
在邮件中插入了一个 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。
参考资料与拓展阅读
Email
2018-07-22
假设:
- A 要发一封邮件给 B、C。
- A 的地址是
aaa@163.com,B 的邮箱地址是 bbb@163.com,C 的邮箱地址是 ccc@qq.com。
那么:
- A 先在自己的客户端 Thunderbird 上(WebMail 后面是怎么处理的就取决于各个邮件服务提供商了)将邮件编辑并发送出去。
- 邮件经过 SMTP(需要验证身份),到了网易的邮件发送服务
smtp.163.com
- 网易邮件发送服务检查了两个收信地址,发现:
B 地址所在域和自己一致,就直接投给了自己的用户邮箱
C 地址所在域和自己不一致,就检查 C 域的 MX 记录,得到 mx.qq.com,然后将邮件投递过去(SMTP)。
- B 通过客户端 Foxmail,从配置的
pop3.163.com 下载了邮件(POP3),手机自带的邮件客户端配的 imap.163.com,那就走 IMAP 协议。
- QQ 的 mx 服务器是没有身份验证的,但是会通过一系列手段验证 163 的连接是可靠的,才会接收这封邮件。
- QQ 收到邮件之后,将邮件投进 C 的收件箱。
- 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 服务器

Email 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”。
SMTP Email
2018-05-18
最常见的三种 SMTP 认证方法:
Email
2018-05-08
Return-Path 头就是收信方记录 SMTP 会话 MAIL FROM 地址用的,这个地址不会显示在邮件中,只是在邮件需要回退的时候才会起到作用。
Python SMTP PythonSimpleServer Email
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!""")
Email
2018-05-05
相关的文章:
邮件是由纯文本组成,其详细的格式有很多 RFC 规范需要遵守。我这里只能对我所了解的,也是基础的 —— 或者说最核心的 —— 格式做一个说明。
最核心的部分是 1982 年的 RFC 822 (STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES),之后又做过一些更新, 比如 RFC 2822 和 RFC 5322 (Internet Message Format) 和一堆补丁更新。本文要讲的基本格式,从开始到现在并没有什么明显变化。
-
邮件是一种纯文本格式,最开始只包含 ASCII 字符,后来引入了 MIME 之后,可以制定别的编码,比如 UTF-8 等。
-
换行符是 \r\n,也就是 CR + LF。
-
整体来说,一封邮件由邮件头(Headers)和邮件体(Payload)组成。
-
邮件头包含若干个头字段
-
邮件头和邮件体之间用一个空行隔开
-
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- 开头的字段名称表示自定义字段,或者叫拓展字段:
常见的拓展字段:
SMTP Email
2018-04-24
RFC#821 定义的 SMTP 协议非常简单(简陋)。
1993 年,RFC#1425 SMTP Service Extensions 定义了 SMTP 协议的拓展框架。
这个向前兼容的安全拓展框架是通过 EHLO 命令来实现。
Email
2018-04-16
注意:这边不是讨论 邮箱地址的格式。
格式
含义
- SMTP 会话(投递)
- Mail From 真实投递的发信人
- Rcpt To 真实投递的收信人
- 邮件内容(显示)
From 发信人
- 如果和 Mail From 地址不同,可能会显示:由 xxx 代发
To 收信人
Cc 抄送人
Bcc 密送人
Rely-To 回复地址
- 客户端点击回复的时候用的
- 如果没有这个字段,就会回复 From 地址
Sender 发信人
Return-Path / Reverse-Path / Envelope-From
- 作用是在邮件投递出现问题的时候,邮件服务将邮件退回这个地址
- 如果我们看到这几个名字
- 可能是发信人自己在邮件中声明
- 可能是收信方收到邮件之后添加的,单独字段,或放在 Received 头中
关于抄送和密送
碳式复写纸 carbon paper
副本,抄送 carbon copy => CC
密送 blind carbon copy => BCC
按照设计,密送地址不希望被其他收信人、抄送人察觉,只是密送地址才知道自己是密送。
CC, BCC in SMTP
SMTP 服务器不处理 CC、BCC,SMTP 客户端应该自行处理
TO 地址 + CC 地址 + BCC 地址一起放到 SMTP 会话的 RCPT TO 字段
所以,按照我的理解,邮件客户端:
在一次 SMTP 会话中,如果有 3 个 TO/CC 地址,2 个 BCC 地址,应该对那 3 个地址批量发送,然后对那 2 个 BCC 地址分别加上 BCC 头,分别发送。
更稳妥一点:如果是批量发送邮件,不要放 BCC 到邮件头!!!显示一个 密送:xxx 也没啥意义。
- PS:MSN(Outlook),网易邮箱发出去的邮件,不会加 BCC 头
甚至网易可能在显示邮件原文的时候会移除 BCC 头(给网易邮箱发的 BCC 头都不见了)
- PS:Gmail,QQ 邮箱发出去的邮件,密送人会看到 BCC 头
from_addr = "from@markjour.com"
to_addrs = ["to@markjour.com"]
cc_addrs = ["cc1@markjour.com", "cc2@markjour.com"]
bcc_addrs = ["bcc@markjour.com"]
msg = f"""
From: {from_addr}
To: {", ".join(to_addrs)}
Cc: {", ".join(cc_addrs)}
Hello World
""".strip()
send_to = to_addrs + cc_addrs + bcc_addrs
server = smtplib.SMTP('smtp.126.com')
server.set_debuglevel(1)
server.login(api_user, api_key)
server.sendmail(from_addr, send_to, msg)
server.quit()
Email 时间
2018-04-10
比如:
Sun, 20 Jun 2018 00:47:04 -0700 (PDT)
Thu, 10 Jun 2021 16:10:03 -0700 (PDT)
Thu, 10 Jun 2021 08:06:31 -0700 (PDT)
定义
定义在 RFC 822 的 5. DATE AND TIME SPECIFICATION。
date-time = [ day "," ] date time ; dd mm yy hh:mm:ss zzz
day = "Mon" / "Tue" / "Wed" / "Thu" / "Fri" / "Sat" / "Sun"
date = 1*2DIGIT month 2DIGIT ; day month year e.g. 20 Jun 82
month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
time = hour zone ; ANSI and Military
hour = 2DIGIT ":" 2DIGIT [":" 2DIGIT]
; 00:00:00 - 23:59:59
zone = "UT" / "GMT" ; Universal Time
; North American : UT
/ "EST" / "EDT" ; Eastern: - 5/ - 4
/ "CST" / "CDT" ; Central: - 6/ - 5
/ "MST" / "MDT" ; Mountain: - 7/ - 6
/ "PST" / "PDT" ; Pacific: - 8/ - 7
/ 1ALPHA ; Military: Z = UT;
; A:-1; (J not used)
; M:-12; N:+1; Y:+12
/ ( ("+" / "-") 4DIGIT ) ; Local differential
; hours+min. (HHMM)
总结就是:
[day-of-week,] day month year hour:minute[:second] timezone
- 周几和秒是可选的,据我观察,没有邮件省略这两部分
- 周几和月份采用三字母英文缩写(首字母大写)
- 年份是 2 位数字,后来的规范更新中建议采用 4 位数字。出于兼容性考虑,一般都保留了对 RFC 822 两位数字年份的支持。
- 时区除了数字之外,可以使用
UT、GMT、EST、EDT、CST、CDT、MST、MDT、PST、PDT,
还有 25 个字母(J 没有使用),Z 表示 UTC/GMT 时间,A - M 表示 -1 ~ -12 时区,N - Y 表示 1 到 12 时区。
| |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
| West |
A |
B |
C |
D |
E |
F |
G |
H |
I |
K |
L |
M |
| Eest |
N |
O |
P |
Q |
R |
S |
T |
U |
V |
W |
X |
Y |
Python
生成符合要求的时间字符串比较简单:
import time
time.strftime('%a, %d %b %Y %H:%M:%S %z')
# 'Tue, 10 Apr 2018 09:10:05 +0800'
但是由于这个灵活度比较大,解析起来最好借助专业的库(email.utils)来做这个事。
import time
import datetime
import email.utils
import pytz
# 解析 ############################################
date_str = 'Sun, 20 Jun 2018 00:47:04 -0700 (PDT)'
email.utils.parsedate_to_datetime(date_str)
# datetime.datetime(2018, 6, 20, 0, 47, 4, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
email.utils.parsedate_tz(date_str)
(2018, 6, 20, 0, 47, 4, 0, 1, -1, -25200)
# 生成 ############################################
# email.utils.formatdate(timeval=None, localtime=False, usegmt=False)
email.utils.formatdate()
# 'Tue, 10 Apr 2018 09:10:41 -0000'
# email.utils.format_datetime(dt, usegmt=False)
dt = datetime.datetime.now()
email.utils.format_datetime(dt)
# 'Tue, 10 Apr 2018 09:16:43 -0000'
tz = pytz.timezone('Asia/Shanghai') # <DstTzInfo 'Asia/Shanghai' LMT+8:06:00 STD>
dt = datetime.datetime(2018, 4, 10, 9, 10, 0, tzinfo=tz)
# datetime.datetime(2018, 4, 10, 9, 10, tzinfo=<DstTzInfo 'Asia/Shanghai' LMT+8:06:00 STD>)
email.utils.format_datetime(dt)
# 'Tue, 10 Apr 2018 09:10:00 +0806'