Python网络编程:使用email、smtplib、poplib、imaplib模块收发邮件
本文详解Python中利用smtplib、poplib及email模块进行邮件收发的完整流程。涵盖SMTP协议发送纯文本、HTML及带附件邮件,POP3协议接收并解析原始邮件,以及IMAP协议管理邮箱。通过具体代码示例,讲解如何构造MIME对象、处理编码、配置SSL加密连接及解析邮件头与正文,帮助开发者掌握Python网络编程中的邮件自动化处理能力。
一封电子邮件的完整传输路径包括以下环节:
- MUA(Mail User Agent,邮件用户代理):即用户使用的邮件客户端软件,例如Outlook。
- MTA(Mail Transfer Agent,邮件传输代理):指代提供邮件服务的运营商平台,如网易、新浪等。
- MDA(Mail Delivery Agent,邮件投递代理):指代邮件服务提供商的具体服务器节点。
数据流向为:发件人 -> MUA -> MTA -> 中间经过若干个MTA -> MDA <- MUA <- 收件人。
开发邮件收发程序的核心逻辑在于:
- 编写MUA将邮件提交至MTA;
- 编写MUA从MDA拉取邮件。
在发送阶段,MUA与MTA之间以及MTA与MTA之间通信均遵循SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)。
在接收阶段,MUA与MDA之间主要使用两种协议:POP(Post Office Protocol,目前版本为POP3)和IMAP(Internet Message Access Protocol,目前版本为IMAP4)。IMAP的优势在于不仅能下载邮件,还能直接在MDA服务器上操作邮件,例如移动至垃圾箱等。
一、SMTP(Simple Mail Transfer Protocol)
即简单邮件传输协议,它定义了一组从源地址到目的地址传送邮件的规则,用于控制信件的中转方式。
Python的smtplib模块提供了一种便捷的途径来发送电子邮件,该模块对SMTP协议进行了简单的封装。
Python对SMTP的支持主要依赖smtplib和email两个模块,其中email负责构造邮件内容,smtplib负责执行发送操作。
1、构造邮件:email.mime类型
构造一个邮件对象本质上是创建一个Messag对象。若构造MIMEText对象,则表示一个纯文本邮件对象;若构造MIMEImage对象,则表示一个包含图片附件的对象。要将多个对象组合在一起,需使用MIMEMultipart对象,而MIMEBase可以表示任何类型的对象。它们的继承关系如下:
Message
+- MIMEBase
+- MIMEMultipart
+- MIMENonMultipart
+- MIMEMessage
+- MIMEText
+- MIMEImage首先,我们来构造一个最简单的纯文本邮件,然后通过SMTP协议将其发送出去。
from email.mime.text import MIMEText
msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')注意在构造MIMEText对象时,第一个参数为邮件正文,第二个参数指定MIME的subtype。传入'plain',最终的MIME类型即为'text/plain'。最后务必使用utf-8编码以保证多语言环境的兼容性。
2、创建 SMTP 对象
语法如下:
import smtplib
smtpObj = smtplib.SMTP( [host [, port [, local_hostname]]] )参数说明:
- host:SMTP服务器主机地址。可以指定主机的IP地址或域名(如runoob.com),此为可选参数。
- port:若提供了host参数,需指定SMTP服务使用的端口号,通常默认为25。
- local_hostname:若SMTP服务运行在本机,只需将服务器地址指定为localhost即可。
3、Python SMTP 对象使用 sendmail 方法发送邮件
语法如下:
SMTP.sendmail(from_addr, to_addrs, msg[, mail_options, rcpt_options])参数说明:
- from_addr:邮件发送者地址。
- to_addrs:字符串列表,表示邮件发送的目标地址。
- msg:发送的消息内容。
此处需特别注意第三个参数msg,它必须是字符串格式的邮件内容。邮件通常由标题、发信人、收件人、邮件正文及附件等构成,发送时需确保msg符合SMTP协议定义的格式。
二、实例
2:本机已安装支持 SMTP 的服务
以下执行实例的前提是你本机已安装了支持SMTP的服务。
sendmail()方法用于发送邮件,由于支持一次发送给多个人,因此传入一个list。邮件正文是一个str字符串,as_string()方法将MIMEText对象转换为str字符串。
经过Header对象编码的文本,包含了UTF-8编码信息和Base64编码的文本数据。
以下是一个使用Python发送邮件的简单实例:
import smtplib
from email.mime.text import MIMEText
from email.header import Header
sender = 'from@runoob.com'
receivers = ['429240967@qq.com'] # 接收邮件,可设置为你的QQ邮箱或者其他邮箱
# 三个参数:第一个为文本内容,第二个 plain 设置文本格式,第三个 utf-8 设置编码
message = MIMEText('Python 邮件发送测试内容...', 'plain', 'utf-8')
message['From'] = Header("菜鸟教程", 'utf-8') # 发送者
message['To'] = Header("测试", 'utf-8') # 接收者
message['Subject'] = Header('Python SMTP 邮件测试主题', 'utf-8')
try:
smtpObj = smtplib.SMTP('localhost')
smtpObj.sendmail(sender, receivers, message.as_string())
print "邮件发送成功"
except smtplib.SMTPException:
print "Error: 无法发送邮件"2、使用第三方 SMTP 服务
若本机未安装sendmail服务,可以使用其他邮件服务商提供的SMTP服务(如QQ、网易、Google等)。
login()方法用于登录SMTP服务器。
发件人或收件人的名字未显示为友好名称,例如Mr Green ;
使用formataddr方法来格式化邮件地址。若包含中文,需通过Header对象进行编码处理。
msg['To']接收的是字符串而非列表,若有多个邮件地址,使用,分隔即可。
import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr
# 第三方 SMTP 服务
mail_host = "mail.sss.com" # 设置服务器
mail_user = "it_system@sss.com" # 用户名
mail_pass = "Ssss201709#" # 口令
sender = 'it_system@tcl.com'
receivers = 'sss.yang@tcsssl.com' # 接收邮件,可设置为你的QQ邮箱或者其他邮箱
message = MIMEText('Python 邮件内容测试...', 'plain', 'utf-8')
message['From'] = formataddr(('SCBC-啊iT', sender))
message['To'] = formataddr(('杨生', receivers))
message['Subject'] = 'Python SMTP 邮件测试'
try:
smtpObj = smtplib.SMTP()
smtpObj.connect(mail_host, 25) # 25 为 SMTP 端口号
smtpObj.login(mail_user, mail_pass)
smtpObj.sendmail(sender, receivers, message.as_string())
print("邮件发送成功")
except smtplib.SMTPException:
print("Error: 无法发送邮件")3、使用Python发送HTML格式的邮件
在构造MIMEText对象时,将HTML字符串传入,并将第二个参数由plain更改为html即可实现:
具体代码如下:
mail_msg = """
Python 邮件内容测试...
这是一个链接
"""
message = MIMEText(mail_msg, 'html', 'utf-8')4、Python 发送带附件的邮件
带附件的邮件可视为包含若干部分的邮件:文本和各个附件本身。因此,可以构造一个MIMEMultipart对象代表邮件整体,然后往里面添加一个MIMEText作为邮件正文,再继续添加表示附件的MIMEBase对象。
发送带附件邮件时,首先需创建MIMEMultipart()实例,然后构造附件。若有多个附件,可依次构造,最后利用smtplib.smtp发送。
from email.mime.multipart import MIMEMultipart
# 创建一个带附件的实例
message = MIMEMultipart()
message['From'] = formataddr(('SCBC-啊iT', sender))
message['To'] = formataddr(('杨生', receivers))
message['Subject'] = 'Python SMTP 邮件测试'
mail_msg = """
Python 邮件内容测试...
这是一个链接
"""
# 邮件正文内容
message.attach(MIMEText(mail_msg, 'html', 'utf-8'))
# 构造附件1,传送当前目录下的 test.txt 文件
att = MIMEText(open('32.txt', 'rb').read(), 'base64', 'utf-8')
att["Content-Type"] = 'application/octet-stream'
# 这里的filename可以任意写,写什么名字,邮件中显示什么名字
att["Content-Disposition"] = 'attachment; filename="32.txt"'
message.attach(att)5、在 HTML 文本中添加图片
要将图片嵌入邮件正文,只需按发送附件的方式将邮件作为附件添加进去,然后在HTML中通过引用src="cid:0"即可把附件作为图片嵌入。若有多个图片,依次编号,然后引用不同的cid:x即可。
邮件的HTML文本中一般邮件服务商添加外链是无效的,正确添加图片的实例如下所示:
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
# 创建一个带附件的实例
message = MIMEMultipart()
message['From'] = formataddr(('SCBC-啊iT', sender))
message['To'] = formataddr(('杨生', receivers))
message['Subject'] = 'Python SMTP 邮件测试'
mail_msg = """
Python 邮件内容测试...
这是一个链接
图片演示:
"""
# 邮件正文内容
message.attach(MIMEText(mail_msg, 'html', 'utf-8'))
# 指定图片为当前目录
with open('a.jpg', 'rb') as fp:
msgImage = MIMEImage(fp.read())
# 定义图片 ID,在 HTML 文本中引用
msgImage.add_header('Content-ID', '')
message.attach(msgImage)或者通过MIMEBase来添加图片
# 指定图片为当前目录
with open('a.jpg', 'rb') as fp:
# 设置附件的MIME和文件名,这里是png类型:
mime = MIMEBase('image', 'jpg', filename='a.jpg')
# 加上必要的头信息:
mime.add_header('Content-Disposition', 'attachment', filename='附件显示名称.jpg')
mime.add_header('Content-ID', '') # 如果有多个文件需要使用.format(index)
mime.add_header('X-Attachment-Id', '0') # 如果有多个文件需要使用.format(index)
# 把附件的内容读进来:
mime.set_payload(fp.read())
# 用Base64编码:
encoders.encode_base64(mime)
# 添加到MIMEMultipart:
message.attach(mime)6、同时支持HTML和Plain格式
若发送HTML邮件,收件人通过浏览器或Outlook等软件可正常浏览。但若收件人使用的设备较旧,无法查看HTML邮件怎么办?
解决方法是在发送HTML的同时附加一个纯文本版本。若收件人无法查看HTML格式邮件,系统可自动降级查看纯文本邮件。
利用MIMEMultipart就可以组合一个HTML和Plain格式,需注意指定subtype为alternative:
msg = MIMEMultipart('alternative')
msg['From'] = ...
msg['To'] = ...
msg['Subject'] = ...
msg.attach(MIMEText('hello', 'plain', 'utf-8'))
msg.attach(MIMEText('Hello', 'html', 'utf-8'))
# 正常发送msg对象...7、加密SMTP
使用标准的25端口连接SMTP服务器时,传输的是明文,整个过程可能被窃听。为更安全地发送邮件,可加密SMTP会话,即先创建SSL安全连接,再使用SMTP协议发送邮件。
某些邮件服务商(例如Gmail)提供的SMTP服务强制要求加密传输。下面介绍如何通过Gmail提供的安全SMTP发送邮件。
只需在创建SMTP对象后,立即调用starttls()方法,即可创建安全连接。后续代码与前述发送邮件代码完全一致。
必须注意,Gmail的SMTP端口为587,因此需修改代码如下:
smtp_server = 'smtp.gmail.com'
smtp_port = 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
# 剩下的代码和前面的一模一样:
server.set_debuglevel(1)三、使用poplib接收邮件
收取邮件即编写一个MUA作为客户端,从MDA将邮件获取到用户电脑或手机。收取邮件最常用的协议是POP协议,目前版本为3,俗称POP3。
Python内置一个poplib模块,实现了POP3协议,可直接用于收邮件。
POP3的命令和响应数据均基于ASCII文本,并以CR和LF(/r/n)作为行结束符。响应数据包含表示返回状态的符号(+/)和描述信息。
请求和响应的标准格式如下:
请求标准格式:命令 [参数] CRLF\n响应标准格式:+OK /[-ERR] description CRLF
POP3协议客户端命令与服务器端对应响应数据如下:
- user name:向POP服务器发送登录的用户名。
- pass string:向POP服务器发送登录的密码。
- quit:退出POP服务器。
- stat:统计邮件服务器状态,包括邮件数量和总大小。
- list [msg_no]:列出全部邮件或指定邮件。返回邮件编号和对应大小。
- retr msg_no:获取指定邮件的内容(根据邮件编号获取,编号从1开始)。
- dele msg_no:删除指定邮件(根据邮件编号删除,编号从1开始)。
- noop:空操作。仅用于与服务器保持连接。
- rset:用于撤销dele命令。
poplib模块完全模拟了上述命令,poplib.POP3或poplib.POP3_SSL为上述命令提供了相应方法,开发者只需依次使用上述命令即可从服务器端下载对应的邮件。
需注意,POP3协议收取的不是已可读的邮件本身,而是邮件的原始文本,这与SMTP协议类似,SMTP发送的也是经过编码后的一大段文本。
要将POP3收取的原始文本转换为可阅读的邮件,还需使用email模块提供的各类来解析原始文本,生成可阅读的邮件对象。
因此,收取邮件分为两步:
第一步:使用poplib.POP3或poplib.POP3_SSL按POP3协议将邮件的原始文本下载到本地;
使用POP3获取邮件其实很简单,要获取所有邮件,只需循环使用retr()将每一封邮件内容拿到即可。真正麻烦的是将邮件的原始内容解析为可阅读的邮件对象。
import poplib
from email.parser import Parser
# email.parser 解析电子邮件,返回这个对象的email.message.Message实例
from email.header import decode_header
from email.utils import parseaddr
# 服务器及用户信息
host = 'mail.tcl.com'
username = 'bobin.yang@tcl.com'
password = 'Ybb7654321'
# 连接到POP3服务器
conn = poplib.POP3_SSL(host)
# 注意qq邮箱使用SSL连接
# 设置调试模式,可以看到与服务器的交互信息
conn.set_debuglevel(1)
# 打印POP3服务器的欢迎文字
print(conn.getwelcome().decode("utf-8"))
# 身份认证
conn.user(username)
conn.pass_(password)
# 获取服务器上信件信息,返回一个列表,第一项是一共有多少封邮件,第二项是共有多少字节
# stat()返回邮件数量和占用空间
mail_total, total_size = conn.stat()
print('message: %s.Size:%s' % (mail_total, total_size))
# list()返回(response, ['mesg_num octets', ...], octets),第二项是编号
resp, mails, octets = conn.list()
print(mails)
# 返回的列表类似[b'1 82923', b'2 2184', ...]
# 获取最新一封邮件,注意索引号从1开始
# POP3.retr(which) 检索序号which的这个邮件,然后设置他的出现标志 返回(response, ['line', ...], octets)这个三元组
resp, lines, ocetes = conn.retr(len(mails))
print('lines:', len(lines))
# lines 存储了邮件的原始文本的每一行
# 可以获得整个邮件的原始文本
print("-------------------")第二步:使用email.parser.Parser或BytesParser解析邮件内容为消息对象,然后以适当的形式将邮件内容展示给用户。
解析邮件的过程与上一节构造邮件的过程正好相反。
程序在创建BytesParser(解析字节串格式的邮件数据)或Parser(解析字符串格式的邮件数据)时,必须指定policy=default;否则,BytesParse或Parser解析邮件数据得到的就是过时的Message对象,而不是新的EmailMessage,处理起来非常不便。
1、使用 email.parser.Parser解析邮件内容为 email.message.Message(过时,不推荐)
msg = b'\r\n'.join(lines).decode('utf-8')
# 解析出邮件
msg = Parser().parsestr(msg)
# email.Parser.parsestr(text, headersonly=False)
# 与parser()方法类似,不同的是他接受一个字符串对象而不是一个类似文件的对象
# 可选的headersonly表示是否在解析玩标题后停止解析,默认为否
# 返回根消息对象
# 编码处理,文本邮件的内容也是str,还需要检测编码,否则,非UTF-8编码的邮件都无法正常显示
def guess_charset(msg):
charset = msg.get_charset() # 从msg对象获取编码
if charset is None:
content_type = msg.get('Content-Type', '').lower() # 如果获取不到,再从content—type字段获取
if 'charset' in content_type:
charset = content_type.split('charset=')[1].strip()
return charset
return charset
# 数据解码,邮件的Subject或者Email中包含的名字都是经过编码后的str,要正常显示,就必须decode
def decode_str(s):
value, charset = decode_header(s)[0] # 数据,数据编码方式,from email.header import decode_header
# decode_header()返回一个list,因为像Cc、Bcc这样的字段可能包含多个邮件地址,所以解析出来的会有多个元素。上面的代码我们偷了个懒,只取了第一个元素。
if charset:
value = value.decode(charset)
return value
# print_ingo函数:解析邮件与构造邮件的步骤正好相反
def print_info(msg, indent=0): # indent用于缩进显示
if indent == 0:
for header in ['From', 'To', 'Subject']: # 邮件的from、to、subject存在于根对象上
value = msg.get(header, '')
if value:
if header == 'Subject':
value = decode_str(value) # 需要解码subject字符串
else:
# 解码mail地址
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = '%s' % addr
print('%s: %s %s' % (header, value, name))
print('-*-' * 20)
if msg.is_multipart():
# 如果邮件对象是一个is_multipart,get_payload()返回一个list,包含所有子对象
parts = msg.get_payload() # 循环获得列表项
for n, part in enumerate(parts):
# print('%spart %s' % (' ' * indent, n))
# print('%s------------' % (' ' * indent))
# 递归打印没一个子对象
print_info(part, indent + 1)
else:
# 邮件对象不是一个is_multipart,就根据content_type判断
content_type = msg.get_content_type() # 数据类型
if content_type == 'text/plain' or content_type == 'text/html': # 纯文本 html文本
# 纯文本或html内容
content = msg.get_payload(decode=True) # 获得文本对象的字符串而非对象本身
charset = guess_charset(msg) # 要检测文本编码
if charset: content = content.decode(charset)
content = '%s' % (content)
print(content) # 获取邮件文本内容,如果只有文本,打印显示的结果和邮件中看的效果一模一样
else:
print(content_type+'不是文本')
print_info(msg, 0)
# 退出
conn.quit()2、使用email.parser.BytesParser 解析成email.message.EmailMessage对象
若程序需获取邮件的发件人、收件人和主题,直接通过EmailMessage的相应属性获取即可,这与前面为EmailMessage设置发件人、收件人和主题的方式是对应的。若程序需读取EmailMessage的各部分,则需调用该对象的walk()方法,该方法返回一个可迭代对象。程序使用for循环遍历walk()方法的返回值,对邮件内容进行逐项处理:
- 若邮件某项的maintype为'multipart',则说明该项是容器,用于包含邮件内容、附件等其他项。
- 若邮件某项的maintype为'text',则说明该项内容为文本,通常即邮件正文或文本附件。对于此类文本内容,程序直接将其输出到控制台中。
- 若邮件某项的maintype为其他值,则说明该项内容为附件,程序将附件内容保存在本地文件中。
import os
import poplib
import mimetypes
from email.parser import Parser, BytesParser
from email.policy import default
msg_data = b'\r\n'.join(lines)
# 将字符串内容解析成邮件,此处一定要指定policy=default
msg = BytesParser(policy=default).parsebytes(msg_data)
print(type(msg))
print('发件人:' + msg['from'])
print('收件人:' + msg['to'])
print('主题:' + msg['subject'])
print('第一个收件人名字:' + msg['to'].addresses[0].username)
print('第一个发件人名字:' + msg['from'].addresses[0].username)
for part in msg.walk():
counter = 1
# 如果maintype是multipart,说明是容器(用于包含正文、附件等)
if part.get_content_maintype() == 'multipart':
continue
# 如果maintype是multipart,说明是邮件正文部分
elif part.get_content_maintype() == 'text':
print(part.get_content())
# 处理附件
else:
# 获取附件的文件名
filename = part.get_filename()
# 如果没有文件名,程序要负责为附件生成文件名
if not filename:
# 根据附件的contnet_type来推测它的后缀名
ext = mimetypes.guess_extension(part.get_content_type())
# 如果推测不出后缀名
if not ext:
# 使用.bin作为后缀名
ext = '.bin'
# 程序为附件来生成文件名
filename = 'part-%03d%s' % (counter, ext)
counter += 1
# 将附件写入的本地文件
with open(os.path.join('.', filename), 'wb') as fp:
fp.write(part.get_payload(decode=True))
# 退出服务器,相当于发送POP 3的quit命令
conn.quit()四、利用imaplib读取邮件文本内容及附件内容
通过IMAP协议来管理邮箱,称为交互邮件访问协议。
! encoding:utf8
'''
环境:
Win10 64位 Python 2.7.5
参考:
http://www.pythonclub.org/python-network-application/email-format
http://blog.sina.com.cn/s/blog_4deeda2501016eyf.html
'''
import imaplib
import email
def parseHeader(message):
""" 解析邮件首部 """
subject = message.get('subject')
h = email.Header.Header(subject)
dh = email.Header.decode_header(h)
subject = unicode(dh[0][0], dh[0][1]).encode('gb2312')
# 主题
print subject
print '
'
# 发件人
print 'From:', email.utils.parseaddr(message.get('from'))[1]
print '
'
# 收件人
print 'To:', email.utils.parseaddr(message.get('to'))[1]
print '
'
# 抄送人
print 'Cc:',email.utils.parseaddr(message.get_all('cc'))[1]
def parseBody(message):
""" 解析邮件/信体 """
# 循环信件中的每一个mime的数据块
for part in message.walk():
# 这里要判断是否是multipart,是的话,里面的数据是一个message 列表
if not part.is_multipart():
charset = part.get_charset()
# print 'charset: ', charset
contenttype = part.get_content_type()
# print 'content-type', contenttype
name = part.get_param("name") #如果是附件,这里就会取出附件的文件名
if name:
# 有附件
# 下面的三行代码只是为了解码象=?gbk?Q?=CF=E0=C6=AC.rar?=这样的文件名
fh = email.Header.Header(name)
fdh = email.Header.decode_header(fh)
fname = dh[0][0]
print '附件名:', fname
# attach_data = par.get_payload(decode=True) # 解码出附件数据,然后存储到文件中
# try:
# f = open(fname, 'wb') #注意一定要用wb来打开文件,因为附件一般都是二进制文件
# except:
# print '附件名有非法字符,自动换一个'
# f = open('aaaa', 'wb')
# f.write(attach_data)
# f.close()
else:
#不是附件,是文本内容
print part.get_payload(decode=True) # 解码出文本内容,直接输出来就可以了。
# pass
# print '+'*60 # 用来区别各个部分的输出
def getMail(host, username, password, port=993):
try:
serv = imaplib.IMAP4_SSL(host, port)
except Exception, e:
serv = imaplib.IMAP4(host, port)
serv.login(username, password)
serv.select()
# 搜索邮件内容
typ, data = serv.search(None, '(FROM "xx@xxx.com")')
count = 1
pcount = 1
for num in data[0].split()[::-1]:
typ, data = serv.fetch(num, '(RFC822)')
text = data[0][1]
message = email.message_from_string(text) # 转换为email.message对象
parseHeader(message)
print '
'
parseBody(message)
pcount += 1
if pcount > count:
break
serv.close()
serv.logout()
if __name__ == '__main__':
host = "imap.mail_serv.com" # "pop.mail_serv.com"
username = "Trevor@mail_serv.com"
password = "your_password"
getMail(host, username, password)到此这篇关于Python收发邮件的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持本站。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









