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>
字段名称允许使用的字符范围非常宽泛,而且大小写不敏感,
但是一般实践中:
常用字段
- ReceivedMTA 轨迹(传输过程中的相关信息)
- Date发信时间,格式:- Fri, 21 Nov 1997 09:55:06 -0600
- SenderMail 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'
	
	
	
		
		 Email  
		2018-04-07
		
规范
规范定义比较复杂,甚至支持注释。
我简化一下(去掉注释,去掉双引号,去掉 [IPv4] / [IPv6] / 主机名 做域):
- 格式:域内部分@域
- 
域内部分: 
- 
长度不超过 64 
- 大小写字母 + 数字(62)
- 
ASCII 标点符号(19) !#$%&'*+-/=?^_`{|}~
 
- 
可以加入点号(.)隔开,不放首尾,不连续出现
 
- 
域名 
- 
每一级域名 1 - 63 个字符,总长度不超过 253
 这个限制和 DNS 报文设计有关
 国际化域名转换成 Punycode 之后也必须遵守这个约定
 
- 允许包含数字、字母(大小写不敏感)和短横线(-)
- 短横线不能出现在首尾位置
实践
实际上的邮件地址会更加简单:
- 长度限制
- QQ 邮箱 3 - 18
- 网易邮箱 6 - 18
- Gmail 6 - 30
- 新浪邮箱 4 - 16
- 字符限制:字母数字 + .-_
- 一般大小写不敏感
- 连字符(.-_)不可连续出现
- 网易免费邮箱只支持下划线,网易 VIP 邮箱支持点和下划线
- Gmail 只支持点和加号
- 在实际投递中,点和加号会被忽略
- 点可以用作单词风格
- 加号通常用做来信归类,比如注册淘宝时 +taobao,订阅开发者头条时+toutiao,相关邮件就方便搜索归类。
 
 
- 部分邮箱不支持全数字(别有用途,或是避免 QQ 号冲突,或是避免手机号冲突)
- 开头结尾字符限制:
- 字母数字开头 + 字母数字结尾
- 字母开头 / 字母数字结尾
 
正则表达式
以规范为准,参考真实场景下的实践:
域内部分:
/[a-z0-9]+([.-_#][a-z0-9]+)+/;
域名部分:
/[a-z0-9]+(-[a-z0-9]+)?(\.[a-z0-9]+(-[a-z0-9]+)?)+/;
汇总在一起就是:
/^[a-z0-9]+([.-_#][a-z0-9]+)+@[a-z0-9]+(-[a-z0-9]+)?(\.[a-z0-9]+(-[a-z0-9]+)?)+$/;
参考资料与拓展阅读