15. 电子邮件

电子邮件

电子邮件软件成为MUA:Mail User Agent—-邮件用户代理。E-mail从MUA发出去,不是直接到达对方电脑而是发到MTA(Mail Transfer Agent—邮件传输代理),就是Email服务提供商,比如网易,新浪等。MTA会把email投递到MDA(Mail Delivery Agent—邮件投递代理),存储在某个文件或特殊的数据库里也就是邮箱。要想获取邮件,必须通过MUA从MDA上获取到自己电脑上。

发件人—>MUA—>MTA—>MTA—>若干个MTA—>MDA<—MUA<—收件人

要编写程序来发送和接收邮件,本质上就是:

1、编写MUA把邮件发到MTA
2、编写MUA从MDA上收邮件

发邮件时MUA和MTA使用的协议是SMTP:Simple Mail Transfer Protocol。
收邮件时,MUA和MDA使用的协议有两种:POP:Post Office Protocol,目前版本是3,俗称POP3;IMAP:Internet Message Access Protocol,目前版本是4,优点是不但能取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱移到垃圾箱。

邮件客户端在发邮件时,会让你先配置SMTP服务器,也就是你要发到哪个MTA上。比如你在用163邮箱,就不能直接发送到新浪的MTA上,你要填163提供的SMTP服务器地址:smtp.163.com,还需要输入邮箱地址和邮箱口令来证明你是163用户,这样MUA才能正常地把Email通过SMTP协议发送到MTA。

从MDA收取邮件时,MDA服务器会要求验证邮箱口令,这样MUA才能顺利地通过POP或IMAP协议从MDA获取邮件。

SMTP发送邮件

SMTP是发送邮件的协议,Python内置对SMTP的支持,可以发送纯文本邮件,HTML邮件及其附件。

Python对SMTP支持有smtplibemail两个模块,email负责构造邮件,stmplib负责发送邮件。首先,我们来构造一个最简单的纯文本邮件:

1
2
>>> from email.mime.text import MIMEText
>>> msg=MIMEText('Hello, send by Python...','plain','utf-8')

注意到构造MIMEText对象时,第一个参数就是邮件正文,第二个参数是MIME的subtype,传入plain表示纯文本,最终的MIME就是text/plain,最后一定要用utf-8编码保证多语言兼容性。然后通过SMTP发送出去:

1
2
3
4
5
6
7
8
9
10
11
12
13
from_addr=input('From:')
password=input('Password:')
to_addr=input('To:')
smtp_server=input('SMTP server: ')
import smtplib
server=smtplib.SMTP(smtp_server, 25)#smtp默认的端口是25
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr,[to_addr],msg.as_string())
server.quit()

我们用set_debuglevel(1)就可以打印出和SMTP服务器交互的所有信息。SMTP协议就是简单的文本命令和相应。login()方法用来登录SMTP服务器,sendmail()方法就是发邮件,由于可以一次发给对个人,所以传入一个list,邮件正文是一个stras_string()把MIMEText对象编程str。收到的邮件的问题是没有主题收件人没有显示为友好的名字,且收到了邮件却提示你不在收件人中。这是因为邮件主题、如何显示发件人、收件人的信息并不是通过SMTP协议发给MTA,而是包含在发给MTA的文本中的,所以,我们必须把From、To、Subject添加到MIMEText中才是一封完整的邮件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import smtplib
from email import encoders
from email.mime.text import MIMEText
from email.header import Header
from email.utils import parseaddr, formataddr
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
from_addr=input('From:')
password=input('Password:')
to_addr=input('To:')
smtp_server=input('SMTP server: ')
msg=MIMEText('hello, send by python...', 'plain' ,'utf-8')
msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)
msg['To'] = _format_addr('管理员 <%s>' % to_addr)
msg['Subject'] = Header('来自SMTP的问候……', 'utf-8').encode()
server = smtplib.SMTP(smtp_server, 25) # SMTP协议默认端口是25
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()

我们编写了一个函数_format_addr()来格式化一个邮件地址。注意不能简单地传入name <addr@example.com>,因为如果包含中文,需要通过Header对象编码。

msg['To']发送的是字符串而不是list,如果有多个地址邮件,用,分隔即可。发送邮箱显示:
Alt text

你看到的收件人的名字可能不是我们传入的管理员,因为很多邮件服务商在显示邮件时会把收件人名字自动替换为用户注册的名字。我们插卡Email原始内容如下:

From: =?utf-8?b?UHl0aG9u54ix5aW96ICF?= xxxxxx@163.com
To: =?utf-8?b?566h55CG5ZGY?= xxxxxx@qq.com
Subject: =?utf-8?b?5p2l6IeqU01UUOeahOmXruWAmeKApuKApg==?=

发送HTML邮件

如果我们要发送HTML邮件而不是普通的纯文本文件怎么办,就在构造MIMEText对象时,把HTML字符串传进去,再把第二个参数由plain改为plain就可以了:

1
msg=MIMEText('<html><body><h1>Hello</h1>' + '<p>send by <a href="http://www.python.org">Python</a>...</p>'+'</body></html>', 'html', 'utf-8')

再发送一遍邮件:
Alt text

发送附件

如果Email中要加上附件,可以把带附件的邮件看做包含若干部分的邮件:文本和各个附件本身,所以可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文,再继续往里面加上表示附件的MIMEBase对象即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 邮件对象:
msg = MIMEMultipart()
msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)
msg['To'] = _format_addr('管理员 <%s>' % to_addr)
msg['Subject'] = Header('来自SMTP的问候……', 'utf-8').encode()
# 邮件正文是MIMEText:
msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))
# 添加附件就是加上一个MIMEBase,从本地读取一个图片:
with open('/Users/michael/Downloads/test.png', 'rb') as f:
# 设置附件的MIME和文件名,这里是png类型:
mime = MIMEBase('image', 'png', filename='test.png')
# 加上必要的头信息:
mime.add_header('Content-Disposition', 'attachment', filename='test.png')
mime.add_header('Content-ID', '<0>')
mime.add_header('X-Attachment-Id', '0')
# 把附件的内容读进来:
mime.set_payload(f.read())
# 用Base64编码:
encoders.encode_base64(mime)
# 添加到MIMEMultipart:
msg.attach(mime)

发送结果:
Alt text

发送图片

如果要把一个图片嵌入到邮件正文中,直接在HTML邮件中连接图片地址是不行的,我们需要按照发送附件的方法,把邮件作为附件添加进去,然后在HTML中通过引用src="cid:0"就可以把附件作为图片嵌入。如果有多个图片,可以依次编号,然后引用不同的cid:x就可以。

把上面代码中加入MIMEMultipartMIMETextplain改为html,然后在适当的位置引用图片:

1
2
3
msg.attach(MIMEText('<html><body><h1>Hello</h1>' +
'<p><img src="cid:0"></p>' +
'</body></html>', 'html', 'utf-8'))

发送结果:
Alt text

同时支持HTML和Plain格式

如果我们发送HTML邮件,收件人通过浏览器或者Outlook之类的软件是可以正常浏览邮件内容的,但是如果收件人的设备无法查看HTML邮件时,我们可以在发送HTML的同时再附加一个纯文本,如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件。

利用MIMEMultipart可以组合一个HTML和Plain,要注意指定subtype是alternative:

1
2
3
4
5
6
7
8
msg = MIMEMultipart('alternative')
msg['From'] = ...
msg['To'] = ...
msg['Subject'] = ...
msg.attach(MIMEText('hello', 'plain', 'utf-8'))
msg.attach(MIMEText('<html><body><h1>Hello</h1></body></html>', 'html', 'utf-8'))
# 正常发送msg对象...

加密SMTP

使用标准的25端口连接SMTP服务器时,使用的是明文传输,发送邮件的整个过程可能会被窃听。要更安全地发送邮件,可以加密SMTP会话,实际上就是先创建SSL安全连接,然后再使用SMTP协议发送邮件。

Gmail提供的SMTP服务必须要加密传输。首先知道Gmail的SMTP端口是587,修改代码如下:

1
2
3
4
5
smtp_server = 'smtp.gmail.com'
smtp_port = 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
...

只需要在创建SMTP对象后,立刻调用starttls()方法,就创建了安全链接。后面的代码和前面的发送邮件代码完全一样。

使用Python的smtplib发送邮件很简单,只要掌握了各种邮件类型的构造方法,正确设置好邮件头就可以顺利发出。构造一个邮件对象就是一个Message对象,如果构建一个MIMEText对象,就表示一个作为附件的图片,要把多个对象组合起来,就用MIMEMultipartMIMEBase可以表示任何对象,继承关系如下:
Alt text


POP3收取邮件

收邮件就是编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或者手机上。收取邮件最常用的协议是POP协议。Python内置了一个poplib模块,实现了POP3协议。

POP3收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP一样,SMTP发送的也是经过编码后的一大段文本。要把POP3收取的文本变为可以阅读的邮件,还需要用email模块提供的各种类来解析原始文本,变成可阅读的邮件对象。收取邮件分为两部分:

1、用poplib把邮件的原始文本下载到本地;
2、用email解析原始文本,还原为邮件对象。

通过POP3下载邮件

POP3的协议本身很简单,以下面的代码为例,我们来获取最新的一封邮件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from email.parser import Parser
import poplib
#输入邮箱地址,口令和POP3服务器地址:
email = input('Email:')
password =input('Password:')
pop3_server = input('POP3 server:')
#连接到POP3服务器
server = poplib.POP3(pop3_server)
#可以打开或关闭调试信息
server.set_debuglevel(1)
print(server.getwelcome().decode('utf-8'))
#身份认证
server.user(email)
server.pass_(password)
#start()返回邮件数量和占用空间
print('Messages: %s, Size: %s' %server.stat())
#list()返回所有邮件的编号
resp, mails, octets = server.list()
print(mails)
#获取最新一封邮件,索引号从1开始
index = len(mails)
resp, lines, octets = server.retr(index)
msg_content = b'\r\n'.join(lines).decode('utf-8')
msg = Parser().parsestr(msg_content)
server.quit()

用POP3协议很简单,要获取所有邮件,只需要循环使用retr()把每一封邮件内容拿到即可。

解析邮件

解析邮件的过程和上一节构造邮件刚好相反,必须导入必要的模块:

1
2
3
4
5
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
import poplib

一行代码可以把邮件内容解析为Message对象:

1
msg = Parser().parsestr(msg_content)

这个Message对象本身可能是一个MIMEMultipart对象,即包含嵌套的其他MIMEBase对象,嵌套可能还不止一层。所以我们要递归地打印出Message对象的层次结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# indent用于缩进显示:
def print_info(msg, indent=0):
if indent == 0:
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header=='Subject':
value = decode_str(value)
else:
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr)
print('%s%s: %s' % (' ' * indent, header, value))
if (msg.is_multipart()):
parts = msg.get_payload()
for n, part in enumerate(parts):
print('%spart %s' % (' ' * indent, n))
print('%s--------------------' % (' ' * indent))
print_info(part, indent + 1)
else:
content_type = msg.get_content_type()
if content_type=='text/plain' or content_type=='text/html':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
print('%sText: %s' % (' ' * indent, content + '...'))
else:
print('%sAttachment: %s' % (' ' * indent, content_type))

邮件的Subject或者Email中包含的名字都是经过编码后的str,要正常显示,就必须decode:

1
2
3
4
5
def decode_Str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value

decode_header()返回一个list,因为像Cc``Bcc这样的字段可能包含多个邮件地址,所以解析出来的会有多个元素。上面的代码我们只取了第一个元素。文本邮件的内容也是str,还需要检测编码,否则非UTF-8编码的邮件都无法正常显示:

1
2
3
4
5
6
7
8
def gusee_charset(msg):
charset = msg.get_charset()
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset

把上面的代码整理好,我们就可以来试试获取一封邮件。先往自己的邮箱发送一封邮件,然后用Python程序把它收到本地:
Alt text

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
import poplib
# 输入邮件地址, 口令和POP3服务器地址:
email = input('Email: ')
password = input('Password: ')
pop3_server = input('POP3 server: ')
def guess_charset(msg):
charset = msg.get_charset()
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
def print_info(msg, indent=0):
if indent == 0:
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header=='Subject':
value = decode_str(value)
else:
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr)
print('%s%s: %s' % (' ' * indent, header, value))
if (msg.is_multipart()):
parts = msg.get_payload()
for n, part in enumerate(parts):
print('%spart %s' % (' ' * indent, n))
print('%s--------------------' % (' ' * indent))
print_info(part, indent + 1)
else:
content_type = msg.get_content_type()
if content_type=='text/plain' or content_type=='text/html':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
print('%sText: %s' % (' ' * indent, content + '...'))
else:
print('%sAttachment: %s' % (' ' * indent, content_type))
# 连接到POP3服务器:
server = poplib.POP3(pop3_server)
# 可以打开或关闭调试信息:
server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字:
print(server.getwelcome().decode('utf-8'))
# 身份认证:
server.user(email)
server.pass_(password)
# stat()返回邮件数量和占用空间:
print('Messages: %s. Size: %s' % server.stat())
# list()返回所有邮件的编号:
resp, mails, octets = server.list()
# 可以查看返回的列表类似[b'1 82923', b'2 2184', ...]
print(mails)
# 获取最新一封邮件, 注意索引号从1开始:
index = len(mails)
resp, lines, octets = server.retr(index)
# lines存储了邮件的原始文本的每一行,
# 可以获得整个邮件的原始文本:
msg_content = b'\r\n'.join(lines).decode('utf-8')
# 稍后解析出邮件:
msg = Parser().parsestr(msg_content)
print_info(msg)
# 可以根据邮件索引号直接从服务器删除邮件:
# server.dele(index)
# 关闭连接:
server.quit()

Donate comment here