邮件是我们日常工作中主要的沟通媒介之一。目前几乎所有编程语言都支持发送和接收电子邮件。
接下来将介绍如何使用Python语言发送和接收邮件。
电子邮件介绍
Email(电子邮件)的历史比Web还要久远。直到现在,Email还是互联网上应用非常广泛的服务。
在我们开始编写邮件操作的相关代码之前,先了解一下电子邮件在互联网上是如何运作的。
电子邮件其实是我们现实生活中快递的电子化,现实中快递是怎么处理的呢?比如你在上海,要邮寄一份文件给北京的朋友。
首先需要准备好邮寄的文件,选择一家快递公司(一般是上门取件或到代理点投递),快递公司会提供对应的信封,在信封上填写地址,剩下的事就由快递公司处理了。
快递公司会将一个地点的信件从就近的小代理点汇聚到一个快递中心,再从快递中心往别的城市发,比如先发到河南某城市的快递中心,再从该处发往北京,也可能由上海直达北京,不过你不用关心具体路线,只需要知道一件事,就是信件走得比较慢,至少要几天时间。
信件到达北京的快递中心后不会直接送到朋友的手里。快递员为了避免你的朋友不在,而让自己白跑一趟,会将信件投递到邮件指定的地址,这个地址可能是你朋友居住附近的快递箱、家里或所在公司。总之,当你的朋友知道自己的信件已经到达时,就可以取到信件了。
电子邮件的流程基本上是按上面的方式运作的,只不过速度不是按天算,而是按秒算。
现在回到电子邮件,假设自己的电子邮件地址是me@163.com,对方的电子邮件地址是friend@aliyun.com。用Outlook或Foxmail之类的软件写好邮件,填上对方的Email地址,单击“发送”按钮,电子邮件就发送出去了。这些电子邮件被称为邮件用户代理(Mail User Agent, MUA)。
Email从MUA发出去后,不是直接到达对方电脑,而是发到邮件传输代理(Mail Transfer Agent,MTA),就是Email服务提供商,如网易、阿里云等。由于自己的电子邮件地址是163.com,因此Email首先被投递到网易提供的MTA,再由网易的MTA发送到对方的服务商,也就是阿里的MTA。在这个过程中可能还会经过别的MTA,但是我们不用关心具体路线,只关心速度即可。
Email到达阿里的MTA后,由于对方使用的是@aliyun.com的邮箱,因此阿里的MTA会把Email投递到邮件的最终目的地邮件投递代理(Mail Delivery Agent, MDA)。Email到达MDA后,会存放在阿里云服务器的某个文件或特殊的数据库里,我们将这个长期保存邮件的地方称之为电子邮箱。
同普通邮件类似,Email不会直接到达对方的电脑,因为对方的电脑不一定开机,开机也不一定联网。对方要取到邮件,必须通过MUA从MDA上获得。
一封电子邮件的旅程是:
发件人→MUA→MTA→MTA→若干个MTA→MDA←MUA←收件人
了解了上述基本概念,要编写程序发送和接收邮件,本质就是:
(1)编写MUA把邮件发到MTA。
(2)编写MUA从MDA上收邮件。
发邮件时,MUA和MTA使用的协议是SMTP(Simple Mail Transfer Protocol),后面的MTA到另一个MTA也是用SMTP协议。
收邮件时,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的用户,SMTP服务器还要求你填写邮箱地址和客户端授权密码,这样MUA才能正常把Email通过SMTP协议发送到MTA。
同样,从MDA收邮件时,MDA服务器也要求验证你的客户端授权密码,确保不会有人冒充你收取邮件。一般Outlook之类的邮件客户端会要求填写POP3或IMAP服务器地址、邮箱地址和授权密码。这样,MUA才能顺利通过POP或IMAP协议,从MDA取到邮件。
在使用Python收发邮件前,需要先准备好至少两个电子邮件,如xxx@163.com,xxx@aliyun.com、xxx@qq.com等,注意两个邮箱不要用同一家邮件服务商。
最后特别注意,目前大多数邮件服务商都需要手动打开SMTP发信和POP收信功能,否则只允许在网页登录。
发送邮件
SMTP是发送邮件的协议,Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件以及带附件的邮件。本节以网易163的服务为例
进行介绍。学习本节内容时,可以自己开通对应的邮箱服务,各个邮件服务公司有介绍邮箱服务的开通方法,参照这些开通方法开通即可。如果已经安装了邮箱服务,就可以使用自己的邮箱服务器进行学习。
1 SMTP发送邮件
Python对SMTP的支持有smtplib和email两个模块,email负责构造邮件,smtplib负责发送邮件。
简单邮件传输协议(Simple Mail Transfer Protocol, SMTP)是从源地址到目的地址传送邮件的规则,由该协议控制信件的中转方式。
Python的smtplib提供了一种很方便的途径发送电子邮件,对SMTP协议进行了简单的封装。
Python创建SMTP对象的语法如下:
smtpObj = smtplib.SMTP([host [, port [, local_hostname]]])
语法中各个参数说明如下。
- host: SMTP服务器主机。可以指定主机的IP地址或域名(如www.baidu.com),是可选参数。
- port:如果提供了host参数,就需要指定SMTP服务使用的端口号。一般情况下SMTP的端口号为25。
- local_hostname:如果SMTP在本地主机上,只需要指定服务器地址为localhost即可。
如果在创建SMTP对象时提供了host和port两个参数,在初始化时会自动调用connect方法连接服务器。
Python SMTP对象使用sendmail方法发送邮件的语法如下:
SMTP.sendmail(from_addr, to_addrs, msg[, mail_options, rcpt_options]
语法中各个参数说明如下。
- from_addr:邮件发送者的地址。
- to_addrs:字符串列表,邮件发送地址。
- msg:发送消息。
msg是字符串,表示邮件内容。我们知道邮件一般由标题、发信人、收件人、邮件内容、附件等构成,发送邮件时,要注意msg的格式。这个格式就是SMTP协议中定义的格式。
普通文本邮件发送的实现关键要将MIMEText中的_subtype设置为plain。首先导入smtplib和mimetext。创建smtplib.smtp实例,连接邮件SMTP服务器,登录后发送,具体代码如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import smtplib
from email.mime.text import MIMEText
from email.header import Header
sender = 'from@163.com'
pwd = 'xxxxx' #开通邮箱服务后,设置的客户端授权密码
receivers = ['to@aliyun.com'] # 接收邮件,可设置为你的邮箱
# 三个参数:第一个为文本内容,第二个 plain 设置文本格式,第三个 utf-8 设置编码
message = MIMEText('Python 邮件发送测试...', 'plain', 'utf-8')
message['From'] = Header("邮件测试", 'utf-8')
message['To'] = Header("测试", 'utf-8')
subject = 'Python SMTP 邮件测试'
message['Subject'] = Header(subject, 'utf-8')
try:
# 使用非本地服务器,需要建立ssl连接
smtpObj = smtplib.SMTP_SSL("smtp.163.com", 465)
smtpObj.login(sender, pwd)
smtpObj.sendmail(sender, receivers, message.as_string())
print ("邮件发送成功")
except smtplib.SMTPException as e:
print ("Error: 无法发送邮件.Case:%s" % e)
我们使用3个引号设置邮件信息。标准邮件需要3个头部信息:From、To和Subject。每个信息直接使用空行分割。
我们通过实例化smtplib模块的SMTP_SSL对象smtpObj连接SMTP访问,并使用sendmail方法发送信息。
执行以上程序,如果你开通了非本地邮件服务,就会输出:
邮件发送成功
如果本地主机安装了sendmail服务,发送邮件的代码可以更改为:
sender = 'from@163.com'
receivers = ['to@aliyun.com'] # 接收邮件,可设置为你的邮箱
# 三个参数:第一个为文本内容,第二个 plain 设置文本格式,第三个 utf-8 设置编码
message = MIMEText('Python 邮件发送测试...', 'plain', 'utf-8')
message['From'] = Header("邮件测试", 'utf-8')
message['To'] = Header("测试", 'utf-8')
subject = 'Python SMTP 邮件测试'
message['Subject'] = Header(subject, 'utf-8')
try:
smtpObj = smtplib.SMTP("localhost")
smtpObj.sendmail(sender, receivers, message.as_string())
print ("邮件发送成功")
except smtplib.SMTPException as e:
print ("Error: 无法发送邮件.Case:%s" % e)
不需要客户端授权密码、SSL连接和登录服务。
2 发送HTML格式的邮件
如果我们要发送的是HTML邮件,而不是普通的纯文本文件怎么办呢?方法很简单,在构造MIMEText对象时把HTML字符串传进去,再把第二个参数由plain变为html就可以了。代码实现如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import smtplib
from email.mime.text import MIMEText
from email.header import Header
sender = 'from@163.com'
pwd = 'xxxxx' #开通邮箱服务后,设置的客户端授权密码
receivers = ['to@aliyun.com'] # 接收邮件,可设置为你的邮箱
mail_msg = """
<p>Python 邮件发送测试...</p>
<p><a href="http://www.runoob.com">这是一个链接</a></p>
"""
message = MIMEText(mail_msg, 'html', 'utf-8')
message['From'] = Header("邮件测试", 'utf-8')
message['To'] = Header("测试", 'utf-8')
subject = 'Python SMTP 邮件测试'
message['Subject'] = Header(subject, 'utf-8')
try:
# 使用非本地服务器,需要建立ssl连接
smtpObj = smtplib.SMTP_SSL("smtp.163.com", 465)
smtpObj.login(sender, pwd)
smtpObj.sendmail(sender, receivers, message.as_string())
print ("邮件发送成功")
except smtplib.SMTPException as e:
print ("Error: 无法发送邮件.Case:%s" % e)
执行以上程序,如果你开通了非本地邮件服务,就会输出:
邮件发送成功
如果本地主机安装了sendmail服务,就不需要客户端授权密码、SSL连接和登录服务,直接使用smtplib模块的SMTP对象连接本地访问即可。
3 发送带附件的邮件
如果Email中要添加附件怎么办?
带附件的邮件可以看作包含文本和各个附件,可以构造一个MIMEMultipart对象代表邮件本身,然后往里面添加一个MIMEText作为邮件正文,再添加表示附件的MIMEBase对象即可。代码实现如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
sender = 'from@163.com'
pwd = 'xxxxx' #开通邮箱服务后,设置的客户端授权密码
receivers = ['to@aliyun.com'] # 接收邮件,可设置为你的邮箱
#创建一个带附件的实例
message = MIMEMultipart()
message['From'] = Header("邮件测试", 'utf-8')
message['To'] = Header("测试", 'utf-8')
subject = 'Python SMTP 邮件测试'
message['Subject'] = Header(subject, 'utf-8')
#邮件正文内容
message.attach(MIMEText('这是Python 邮件发送测试……', 'plain', 'utf-8'))
# 构造附件1,传送当前目录下的 test.txt 文件
att1 = MIMEText(open('test.txt', 'rb').read(), 'base64', 'utf-8')
att1["Content-Type"] = 'application/octet-stream'
# 这里的filename 可以任意写,写什么名字,邮件中就显示什么名字
att1["Content-Disposition"] = 'attachment; filename="test.txt"'
message.attach(att1)
# 构造附件2,传送当前目录下的 runoob.txt 文件
att2 = MIMEText(open('runoob.txt', 'rb').read(), 'base64', 'utf-8')
att2["Content-Type"] = 'application/octet-stream'
att2["Content-Disposition"] = 'attachment; filename="runoob.txt"'
message.attach(att2)
try:
# 使用非本地服务器,需要建立ssl连接
smtpObj = smtplib.SMTP_SSL("smtp.163.com", 465)
smtpObj.login(sender, pwd)
smtpObj.sendmail(sender, receivers, message.as_string())
print ("邮件发送成功")
except smtplib.SMTPException as e:
print ("Error: 无法发送邮件.Case:%s" % e)
执行以上程序,如果你开通了非本地邮件服务,就会输出:
邮件发送成功
如果本地主机安装了sendmail服务,就不需要客户端授权密码、SSL连接和登录服务,直接使用smtplib模块的SMTP对象连接本地访问即可。
4 发送图片
如果要把一个图片嵌入邮件正文,怎么做呢?是否可以直接在HTML邮件中链接图片地址?
大部分邮件服务商都会自动屏蔽带有外链的图片,因为不知道这些链接是否指向恶意网站。要把图片嵌入邮件正文,我们只需按照发送附件的方式把邮件作为附件添加进去,然后在HTML中通过引用src="cid:0"把附件作为图片嵌入。如果有多张图片,就需要给它们依次编号,然后引用不同的cid:x。
#! /usr/bin/python
# -*-coding:UTF-8-*-
import smtplib
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
sender = 'from@163.com'
pwd = 'xxxxx' #开通邮箱服务后,设置的客户端授权密码
receivers = ['to@aliyun.com'] # 接收邮件,可设置为你的邮箱
msgRoot = MIMEMultipart('related')
msgRoot['From'] = Header("邮件测试", 'utf-8')
msgRoot['To'] = Header("测试", 'utf-8')
subject = 'Python SMTP 邮件测试'
msgRoot['Subject'] = Header(subject, 'utf-8')
msgAlternative = MIMEMultipart('alternative')
msgRoot.attach(msgAlternative)
mail_msg = """
<p>Python 邮件发送测试...</p>
<p><a href="https://www.python.org">Python 官方网站</a></p>
<p>图片演示:</p>
<p><img src="cid:image1"></p>
"""
msgAlternative.attach(MIMEText(mail_msg, 'html', 'utf-8'))
# 指定图片为当前目录
fp = open('test.png', 'rb')
msgImage = MIMEImage(fp.read())
fp.close()
# 定义图片 ID,在 HTML 文本中引用
msgImage.add_header('Content-ID', '<image1>')
msgRoot.attach(msgImage)
try:
# 使用非本地服务器,需要建立ssl连接
smtpObj = smtplib.SMTP_SSL("smtp.163.com", 465)
smtpObj.login(sender, pwd)
smtpObj.sendmail(sender, receivers, msgRoot.as_string())
print ("邮件发送成功")
except smtplib.SMTPException as e:
print ("Error: 无法发送邮件.Case:%s" % e)
执行以上程序,如果你开通了非本地邮件服务,就会输出:
邮件发送成功
如果本地主机安装了sendmail服务,就不需要客户端授权密码、SSL连接和登录服务,直接使用smtplib模块的SMTP对象连接本地访问即可。
5 同时支持HTML和Plain格式
如果我们发送HTML邮件,收件人通过浏览器或Outlook之类的软件就可以正常浏览邮件内容。如果收件人使用的设备太古老,查看不了HTML邮件怎么办呢?
办法是在发送HTML的同时附加一个纯文本,如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件。
利用MIMEMultipart可以组合一个HTML和Plain,注意指定subtype是alternative。使用代码格式如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import smtplib
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
sender = '15618942985@163.com'
pwd = 'lyz111LYZ' #开通邮箱服务后,设置的客户端授权密码
receivers = ['yuzhouliu@aliyun.com'] # 接收邮件,可设置为你的邮箱
msgRoot = MIMEMultipart('related')
msgRoot['From'] = Header("邮件测试", 'utf-8')
msgRoot['To'] = Header("测试", 'utf-8')
subject = 'Python SMTP 邮件测试'
msgRoot['Subject'] = Header(subject, 'utf-8')
msgAlternative = MIMEMultipart('alternative')
msgRoot.attach(msgAlternative)
msgAlternative.attach(MIMEText('hello', 'plain', 'utf-8'))
mail_msg = '<html><body><h1>Hello</h1></body></html>'
msgAlternative.attach(MIMEText(mail_msg, 'html', 'utf-8'))
# 指定图片为当前目录
fp = open('test.png', 'rb')
msgImage = MIMEImage(fp.read())
fp.close()
# 定义图片 ID,在 HTML 文本中引用
msgImage.add_header('Content-ID', '<image1>')
msgRoot.attach(msgImage)
try:
# 使用非本地服务器,需要建立ssl连接
smtpObj = smtplib.SMTP_SSL("smtp.163.com", 465)
smtpObj.login(sender, pwd)
smtpObj.sendmail(sender, receivers, msgRoot.as_string())
print ("邮件发送成功")
except smtplib.SMTPException as e:
print ("Error: 无法发送邮件.Case:%s" % e)
执行以上程序,如果你开通了非本地邮件服务,就会输出:
邮件发送成功
查看收到的邮件,如图2所示。
6 加密SMTP
使用标准25端口连接SMTP服务器时使用的是明文传输,发送邮件的整个过程可能会被窃听。要更安全地发送邮件,可以加密SMTP会话,实际上是先创建SSL安全连接,然后使用SMTP协议发送邮件。
某些邮件服务商(如Gmail)提供的SMTP服务必须进行加密传输。下面来看如何通过Gmail提供的安全SMTP发送邮件。
由于Gmail的SMTP端口是587,因此修改代码如下:
smtp_server = 'smtp.gmail.com'
smtp_port = 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
# 剩下的代码和前面的一模一样:
server.set_debuglevel(1)
...
只需要在创建SMTP对象后立刻调用starttls()方法,就可以创建安全连接。后面的代码和前面的发送邮件代码完全一样。
如果因为网络问题无法连接Gmail的SMTP服务器,请相信我们的代码是没有问题的,需要对网络设置做必要的调整。
POP3接收邮件
SMTP用于发送邮件,如果要收取邮件呢?
收取邮件就是编写一个MUA作为客户端,从MDA获取邮件到用户的电脑或手机上。收取邮件最常用的协议是POP,目前版本是3,俗称POP3。
Python内置了一个poplib模块,用于实现POP3协议,可以直接用来收取邮件。
注意POP3协议收取的不是可以阅读的邮件,而是邮件的原始文本。这和SMTP协议很像,SMTP发送的也是经过编码后的一大段文本。
要把POP3收取的文本变成可以阅读的邮件,还需要用email模块提供的各种类解析原始文本。
收取邮件分为以下两个步骤:
(1)用poplib把邮件的原始文本下载到本地。
(2)用email解析原始文本,还原为邮件对象。
1 POP3下载邮件
POP3协议很简单。下面获取最新一封邮件的内容,代码如下:
#! /usr/bin/python
# -*-coding:UTF-8-*-
import poplib
from email.parser import Parser
# 输入邮件地址、口令和POP3服务器地址
email = input('Email: ')
password = input('Password: ')
pop3_server = input('POP3 server: ')
# 连接到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)
# 可以根据邮件索引号直接从服务器删除邮件
# server.dele(index)
# 关闭连接
server.quit()
用POP3获取邮件其实很简单,要获取所有邮件,只需要循环使用retr()把每一封邮件的内容拿到即可。真正麻烦的是把邮件的原始内容解析为可以阅读的邮件对象。
2 解析邮件
解析邮件的过程和构造邮件正好相反,需要先导入必要的模块:
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
import poplib
只需要一行代码就可以把邮件内容解析为Message对象:
msg = Parser().parsestr(msg_content)
这个Message对象可能是一个MIMEMultipart对象,即包含嵌套的其他MIMEBase对象,嵌套可能还不止一层。
我们要递归地输出Message对象的层次结构:
# 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,要正常显示必须进行解码,代码如下:
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编码的邮件都无法正常显示,代码如下:
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