Email 信息安全
2020-02-11
简介
SMTP 会话中,发行方会通过 MAIL FROM
命令告诉收信方,这封邮件是谁发出的。
但是,由于邮件系统的设计上没有考虑如何对垃圾邮件进行有效防范,收信方需要一个机制来判断这封邮件的来源是否可靠。因此,人们提出了 SPF 方案:
- 每个邮件服务都需要为自己的发信域名在 DNS 中配置一下 SPF 记录(TXT 类型),记录值是出信 IP。
- 收信方拿到
MAIL FROM
地址之后,使用发信方 IP 地址和发信域 DNS 中记录的 IP 地址做一个比对。如果匹配,说明来源正确,否则,这封邮件可以被视为垃圾邮件,直接返回投递失败(硬退)。
该方案 2006 年提交到 Network Working Group 成为草案,然后 2014 年成为建议标准。
注意:SPF 只是验证了邮件确实是从发信域所在网络投递出来的,但是不能防止邮件伪造和欺诈。应该结合其他技术,如 DKIM(DomainKeys Identified Mail)和 DMARC(Domain-based Message Authentication, Reporting, and Conformance),一起增强电子邮件的安全性。
SPF 记录示例
dig +short txt qq.com
"v=spf1 include:spf.mail.qq.com -all"
dig +short txt spf.mail.qq.com
"v=spf1 include:qq-a.mail.qq.com include:qq-b.mail.qq.com include:qq-c.mail.qq.com include:biz-a.mail.qq.com include:biz-b.mail.qq.com include:biz-c.mail.qq.com include:biz-d.mail.qq.com -all"
dig +short txt qq-{a..c}.mail.qq.com biz-{a..d}.mail.qq.com
"v=spf1 ip4:101.226.139.0/25 ip4:101.91.43.0/25 ip4:101.91.44.128/25 ip4:112.64.237.128/25 ip4:116.128.173.0/25 ip4:121.51.40.128/25 ip4:121.51.6.0/25 ip4:162.62.52.214 ip4:162.62.55.67 ip4:162.62.57.0/24 ip4:162.62.58.211 ip4:162.62.58.216 -all"
"v=spf1 ip4:162.62.58.69 ip4:162.62.63.194 ip4:180.163.24.128/25 ip4:183.2.187.0/25 ip4:203.205.221.128/25 ip4:203.205.251.0/25 ip4:210.51.43.0/25 ip4:58.246.222.128/25 ip4:58.250.143.128/25 ip4:61.241.55.128/25 -all"
"v=spf1 ip4:113.108.92.0/25 ip4:121.14.77.0/25 ip4:81.69.217.16/28 ip4:54.164.151.162 -all"
"v=spf1 ip4:114.132.122.39 ip4:114.132.123.192 ip4:114.132.124.171 ip4:114.132.125.233 ip4:114.132.197.227 ip4:114.132.224.180 ip4:114.132.233.22 ip4:114.132.58.0/24 ip4:43.155.65.254 ip4:114.132.62.0/24 ip4:106.55.200.77 -all"
"v=spf1 ip4:114.132.63.24 ip4:114.132.64.0/26 ip4:114.132.65.219 ip4:43.155.67.158 ip4:114.132.67.179 ip4:114.132.73.137 ip4:114.132.74.132 ip4:43.154.209.5 ip4:43.154.197.177 ip4:43.154.155.102 ip4:43.155.80.173 ip4:43.154.221.58 -all"
"v=spf1 ip4:54.204.34.129 ip4:54.204.34.130 ip4:54.243.244.52 ip4:52.205.10.60 ip4:35.173.142.173 ip4:54.207.22.56 ip4:54.207.19.206 ip4:54.254.200.92 ip4:54.254.200.128 ip4:54.92.39.34 ip4:54.206.16.166 ip4:54.206.34.216 ip4:114.132.75.215 -all"
"v=spf1 ip4:52.59.177.22 ip4:18.194.254.142 ip4:18.132.163.193 ip4:18.169.211.239 ip4:13.245.186.79 ip4:13.245.218.24 ip4:15.184.224.54 ip4:15.184.82.18 ip4:114.132.76.87 ip4:114.132.77.159 ip4:114.132.78.196 ip4:114.132.79.153 ip4:43.154.54.12 -all"
语法
-
"v=spf1":指定 SPF 记录的版本,目前只有 SPFv1 版本。
-
机制(Mechanisms):SPF 使用机制来指定哪些服务器被授权发送邮件。常用的机制有:
-
"all":定义了所有情况下的默认结果。可以是 "+all"(通过验证)或 "-all"(不通过验证)。例如,"-all" 表示除非匹配其他通过机制,否则所有情况都不通过验证,即严格拒绝伪造邮件。
- "ip4" 和 "ip6":指定允许发送邮件的 IPv4 和 IPv6 地址。例如:"ip4:192.0.2.1" 表示允许来自 192.0.2.1 的服务器发送邮件。
- "a" 和 "mx":允许域名的 A 记录和 MX 记录指定的主机发送邮件。
-
"include":允许引用其他域名的 SPF 记录。例如:"include:example.com" 表示允许按照 example.com 的 SPF 记录来验证。
-
修饰符(Modifiers):修饰符可以在机制之后指定,用于调整验证的结果。常用的修饰符有:
-
"+":显示通过验证。
- "-":显示不通过验证,相当于 "-all"。
- "~":软失败,邮件可能被接受,但会标记为不可信。
- "?":中性结果,邮件可能被接受,但不影响验证的结果。
SPF 记录示例:
v=spf1 ip4:192.0.2.1 a mx ~all
以上示例表示允许 IP 地址为 192.0.2.1 的服务器、A 记录和 MX 记录指定的主机发送邮件,但验证结果为软失败,邮件可能会被接受,但标记为不可信。
DNS SPF 类型
IANA 设计了一种专门的 DNS 类型用来记录 SPF 信息,但是采用率非常低。
可以忽略,继续使用 TXT 类型来保存 SPF 信息。
参考资料与拓展阅读
Email 信息安全
2020-02-10
DomainKeys Identified Mail,域名密钥识别邮件
作用是使用非对称加密(公钥 + 私钥)对邮件内容进行签名,防止伪造和篡改。
历史
- 2004 年,雅虎 DomainKeys 和思科 Identified Internet Mail 合并为 DKIM。
- 2007 年 2 月,DKIM 被列入互联网工程工作小组(IETF)的标准提案(Proposed Standard),并于同年 5 月成为正式标准(Standards Track)。
示例
DKIM-Signature: v=1; a=rsa-sha256; d=example.net; s=brisbane;
c=relaxed/simple; q=dns/txt; i=foo@eng.example.net;
t=1117574938; x=1118006938; l=200;
h=from:to:subject:date:keywords:keywords;
z=From:foo@eng.example.net|To:joe@example.com|
Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700;
bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;
b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZ
VoG4ZHRNiYzR
Tag |
Required |
Meanning |
v |
✓ |
version |
a |
✓ |
signing algorithm |
d |
✓ |
Signing Domain Identifier (SDID) |
s |
✓ |
selector |
c |
- |
canonicalization algorithm(s) for header and body |
q |
- |
default query method |
i |
- |
Agent or User Identifier (AUID) |
t |
recommended |
signature timestamp |
x |
recommended |
expire time |
l |
- |
body length |
h |
✓ |
header fields - list of those that have been signed |
z |
- |
header fields - copy of selected header fields and values |
bh |
✓ |
body hash |
b |
✓ |
signature of headers and body |
示例 2
邮件中的 DKIM-Signature 头:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=mail.instagram.com;
s=s1024-2013-q3; t=1674163477;
bh=6RXa/HYJQNKpB5PIGtLn7v1NE/4T5FaqxBLWNHVRZu8=;
h=Date:To:Subject:From:MIME-Version:Content-Type;
b=VAY3x16QtXeH1rQxu6eEbzhfgZl69m1sG9XzN3ym4FbWiMg+K+IfMGF4yszGYk8yO
YXAAJZuQfG45pjthISDDSwhhBK0WGgufQ8ofnzhNUN9WT/okEATC+JfzksS9w2Ts4V
ALa/4HHXnikQV5AFNJJNJIvWMN/fJ5c49nLkW024=
域名中的 DKIM 公钥:
dig TXT +short s1024-2013-q3._domainkey.mail.instagram.com
"k=rsa; t=s; h=sha256; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7twdVo+BW8Pv2poU5129KYmE6npHdxUU8fktUKTE9TNovCvLy5LVjYc3TQcUFjOH" "VaZ89ZCjmpAcrA2QnTEKZ/2QWV56gn6bWdFW4SFxnQdHjguBZQykfKe5KTxy2a/OxuA0x2dHfdnYfw7RVzr4uednpKcWJy4Rl3gM6XB1zDwIDAQAB"
Python 编程中的应用
准备工作
- selector 选择 s2020 (随便)
- 生成密钥对
# 方法一:使用 OpenDKIM
sudo apt install -y opendkim-tools
opendkim-genkey --help
opendkim-genkey -D . -d markjour.com -s s2020
# 方法二:使用 OpenSSL
openssl genrsa -out dkim-markjour-s2020.pem 2048
openssl rsa -in dkim-markjour-s2020.pem -pubout -outform der 2>/dev/null | openssl base64 -A > dkim-markjour-s2020.pub
- 配置 DNS
s2020._domainkey.markjour.com
"v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjnfmXLuyBt0Tus/Bdr87GpLqcRCZX7UC61OiPZ1Y3MG42qBcdEAMJfu7qop7KOLL8cywTRxiX39ehmf0ZovAXH1KkijiX/16tkI3cO9T6KS4vyr0Ip3fsGgNgjn5rH3M5AZAmbym6DIzYrtpTiAKgLYmFLALd9SLi/OhFIltWK+QJhaJgcuWUXCzlry01Fdsv1qj28WdZ6PQbQrSffc1qzkvOEOlmZXwWjQfg5X4E3DR4WKenC6f5WdcJeXk4pUeBOdQDoEM+4uCk4S6cN3OuYEQbvVmfQ5RAlCODccx7lJemWZZnlIf+03FppUEEMENZ8tu3iixD24m2q9wDLDL5QIDAQAB
v
版本,只有一种选择:DKIM1,必须放最前面,可以忽略
h
哈希算法,sha1 或者 sha256
k
密钥类型,只有一种选择:rsa,可以忽略
n
注释,可以忽略
p
公钥(ASN.1 DER-encoded + Base64)
s
服务类型,默认 *
,表示所有服务,可以忽略
t
逗号隔开的标记
- y 测试
- s DKIM 签名中的 i= 域名必须是 d= 域名完全相同(子域也不行)。
参考:
签名
headers = {'aaa': '123', 'bbb': '456', 'ccc': '789'}
校验签名
mail = """
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=qq.com; s=s0907;
t=1331524836; bh=Pqr4lbxMcef/3IqsXx/edT0iwPe18N7n8qKmQSnLio8=;
h=X-QQ-SSF:X-QQ-Spam:X-QQ-BUSINESS-ORIGIN:X-Originating-IP:
X-QQ-STYLE:X-QQ-mid:From:To:Sender:Subject:Mime-Version:
Content-Type:Content-Transfer-Encoding:Date:X-Priority:Message-ID:
X-QQ-MIME:X-Mailer:X-QQ-Mailer:X-QQ-ReplyHash;
b=hF3hXt429Mp9WUJx9wQQYYk32EABCQST/OmV+dI+vJ/XIidVkc6fsh8l/vBz/optb
MDp0XIupHHkUozz6jwMryhHd/ZNjLNtBBAIOgl1wH7R016x8uTtDQink5uIPH+5
X-QQ-SSF: 0000000000010060
X-QQ-Spam: true
X-QQ-BUSINESS-ORIGIN: 2
X-Originating-IP: 61.151.148.196
X-QQ-STYLE:
X-QQ-mid: bizmail6t1331524835t2734595
From: "=?gb18030?B?wffE6g==?=" <thinkphp@qq.com>
To: "=?gb18030?B?uvqwug==?=" <ninedoors@126.com>
Sender: liuchen@topthink.net
Subject: =?gb18030?B?UmU60rvOu1RQZXK1xNLJu/M=?=
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_NextPart_4F5D74E3_DF6406C0_249C5176"
Content-Transfer-Encoding: 8Bit
Date: Mon, 12 Mar 2012 12:00:35 +0800
X-Priority: 3
Message-ID: <tencent_519100472CAA258E750FA58D@qq.com>
X-QQ-MIME: TCMime 1.0 by Tencent
X-Mailer: QQMail 2.x
X-QQ-Mailer: QQMail 2.x
X-QQ-ReplyHash: 3949397671
""".strip('\n')
Python Email 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
Email
2019-01-14
紧急程度 X-Priority
有另一个 Priority 头,不知道为什么没有使用。
根据网上的一些资料,这几个头已经被垃圾邮件滥用,导致可能会被拦截。
不过,企业邮箱使用应该没有问题。
网易邮箱发送的时候勾选重要
,就会设置 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
参考资料与拓展阅读
Email
2019-01-10
经过测试,至少 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
说明
多用途互联网邮件扩展
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 |
语法
MIME-Version: 1.0 // MIME 版本
Content-Type: [type]/[subtype]; parameter // 内容类型
Content-Transfer-Encoding: [encoding] // 内容传输编码
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 认证方法:
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-
开头的字段名称表示自定义字段,或者叫拓展字段:
常见的拓展字段: