Python进阶教程m9c–网络通信–pop3接收邮件

原文链接:http://www.juzicode.com/python-tutorial-pop3/

Python进阶教程m9b–网络通信–smtplib发送邮件 一文中桔子菌给大家介绍了怎么用Python发送邮件,俗话说“来而不往非礼也”,有发就要有收,这篇文章将带大家熟悉下怎么用poplib和email模块接收邮件。

先做好准备工作,仍以163邮箱为例,首先需要先设置好邮箱的授权码,并找到邮箱的pop3服务器地址:

1、创建POP3()实例、登录

首先手动输入接收邮箱的地址和授权码,这里我们用到了getpass模块接收键盘输入的密码,这样密码不会显示在屏幕上,对比input()函数更安全些。

#输入邮箱地址、密码
to_addr = input('输入收件人: ')
password = getpass.getpass('收件人邮箱密码: ')
==========运行结果:
-----欢迎来到www.juzicode.com
-----公众号: 桔子code/juzicode
输入收件人: xxx@163.com
收件人邮箱密码:       ####这里因为使用getpass.getpass()方法,密码不显示

和接收邮件一样,传入pop3服务器地址“pop.163.com”创建一个POP3()实例,为了方便查看通信过程设置好调试打印等级,一般设置到2。因为创建POP3()实例会和服务器发起连接,这里用try语句捕获可能的异常。

#连接邮箱,设置调试等级
try:
    pop3 = poplib.POP3('pop.163.com')
    pop3.set_debuglevel(1) 
    print(pop3)
except:
    print('连接服务器失败')
    sys.exit(1)

使用pop3.user()传入邮箱地址 和 pop3.pass_()方法传入授权码或邮箱密码(注意pass_()方法有下划线!), user() 和 pass_()方法会发起连接,可能会登录失败,也用try语句捕获异常。

#登录邮箱
try:
    ret=pop3.user(to_addr) 
    print(ret)
    ret=pop3.pass_(password)
    print(ret)
except :  
    print('登录邮箱失败')
    sys.exit(1)
print('登录邮箱成功')
==========运行结果:
*cmd* 'USER xxx@163.com'
b'+OK core mail'
*cmd* 'PASS xxx'
b'+OK 5 message(s) [17765 byte(s)]'
登录邮箱成功

2、解析邮件

前面的过程完成了邮箱登录,下面开始读取和解析邮件。

2.1、获取邮件列表

用pop3.list()方法获取邮件消息列表,返回的是一个三元组:(response, [‘mesg_num octets’, …], octets) :

#获取邮件清单
mail_list = response, listings, octets = pop3.list()
print(mail_list)
==========结果:
*cmd* 'LIST'
(b'+OK 5 17765', [b'1 3429', b'2 10596', b'3 879', b'4 1481', b'5 1380'], 40)

listings = [b’1 3429′, b’2 10596′, b’3 879′, b’4 1481′, b’5 1380′] 这个列表就是我们需要的关键信息,表示获取到的邮件列表,包含了5个元素表示有5封邮件,b’1 3429′ 表示第1封邮件有3429字节, b’2 10596′ 表示第2封邮件,以此类推。每个元素内部由空格分隔,前面的数字表示mesg_num,这个值就是后面解析时用来表示的邮件索引号。敲黑板,这个是关键变量!

接下来开始遍历邮件列表listings,对它的单个元素用split()方法切割出mesg_num。另外因为返回的是bytes类型的数据,为了方便后面的处理,我们将其转换为str类型:

for listing in listings:
    print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
    #提取邮箱列表信息
    mesg_num, size = listing.split()
    mesg_num = bytes.decode(mesg_num)
    size = bytes.decode(size)
    print(mesg_num,size)

有了这些邮件消息编号,接下来就可以利用这些编号提取邮件header和正文等内容。

2.2、获取邮件Header

首先用pop3.top()方法提取邮件Header,top()方法输入的是邮件列表里解析出来的mesg_num。返回结果也是一个三元组,中间的lines是关键信息,因为是bytes类型,所以也需要转换为str类型。

    #提取邮件头
    res = response, lines, octets = pop3.top(mesg_num, 0)
    #print(lines)
    lines_post=[]
    for l in lines:
        lines_post.append(bytes.decode(l))

接下来用email.message_from_string()构造结构化数据,再提取From,To,Subject,Date等信息:

    #构造message
    message = email.message_from_string('\n'.join(lines_post))
    print('type(message):',type(message))
    print('From:',message['From'])
    print('To:',message['To'])
    print('Subject:',message['Subject'])
    print('Date:',message['Date'])
==========结果:
4 1481
*cmd* 'TOP 4 0'
type(message): <class 'email.message.Message'>
From: xxx <xxx@163.com>
To: xxx <xxx@163.com>
Subject: https://www.juzicode.com/  vx:juzicode
Date: Wed, 11 Nov 2020 20:08:18 +0800 (CST)

2.3、获取邮件正文

用pop3.retr()方法提取邮件正文, retr()方法输入的也是邮件列表里解析出来的mesg_num。返回结果也是一个三元组,中间的lines是关键信息,因为是bytes类型,所以也需要转换为str类型。

    #提取邮件内容
    res = response, lines, octets = pop3.retr(mesg_num)
    lines_post=[]
    for l in lines:
        lines_post.append(bytes.decode(l))

接下来用email.message_from_string()构造结构化数据,这里我们先不考虑复杂的情况,先只处理类型为multipart和text类型的内容。

根据get_content_maintype()方法获取到不同的类型,需要做分支处理。如果是multipart,需要再次遍历;如果是text类型,就可以直接用get_payload()获取邮件正文。

这个例子中邮件内容的解码mail_content.decode() 尝试了UTF8和GBK 2种方式,也可能是其他编码方式,需要根据实际情况作出变化。

    #构造message
    message = email.message_from_string('\n'.join(lines_post))    
    maintype = message.get_content_maintype()    
    print('maintype:',maintype)    
    if maintype == 'multipart':
        for part in message.get_payload():
            print('multipart:',part.get_content_maintype())
            if part.get_content_maintype() == 'text':
                mail_content = part.get_payload(decode=True).strip()
                try:
                    print('mail_content:',mail_content.decode('UTF8'))  
                except:
                    try:
                        print('mail_content:',mail_content.decode('GBK'))  
                    except:
                        continue
    elif maintype == 'text':
        mail_content = message.get_payload(decode=True).strip()
        try:
            print('mail_content:',mail_content.decode('UTF8'))  
        except:  
            try:
                print('mail_content:',mail_content.decode('GBK'))  
            except:
                continue 
==========提取第5封邮件的结果:
*******************
*cmd* 'RETR 5'
maintype: multipart
multipart: text
mail_content: www.juzicode.com   微信公众号:桔子code
multipart: text
mail_content: &lt;div style="line-height:1.7;color:#000000;font-size:14px;font-family:Arial">&lt;p style="margin:0;">www.juzicode.com&amp;nbsp; &amp;nbsp;微信公众号:桔子code&lt;/p>&lt;/div>&lt;br>&lt;br>&lt;span title="neteasefooter">&lt;p>&amp;nbsp;&lt;/p>&lt;/span>

从网页进入邮箱看到的邮件内容是下图这样的,正好和前面第一次取到的text内容是一样的:“mail_content: www.juzicode.com 微信公众号:桔子code”

第2次取到的text内容则是html格式的邮件正文,我们可以创建一个文本文件,输入这段内容:<div style …… /p>,改后缀为html,然后用浏览器打开,看到的效果和第1次取得的text内容是一致的:

2.4 处理附件和复杂的邮件正文

上一节中处理邮件内容作了简化操作,比如解析文本内容的时候采用的是尝试按照UTF8和GBK 2种方式解码,不能完全自适应解析;如果邮件中有附件,也没有实现接收附件的功能 ;当message的主类型为multipart,只处理了2层,实际情况可能更复杂。接下来继续改造解析消息方式,因为multipart可能有多层,需要用递归来实现,我们定义一个parse_msg()的函数。

如果邮件带有附件,首先利用get_filename()方法获取附件文件名称,如果这个名称非空,则说明该段message是个附件,通过get_payload()方法可以提取出数据,并将该数据写入到文件中:

    #如果get_filename()返回非None,表示有附件
    filename = message.get_filename()
    if filename:
        print('找到1个附件,文件名:',filename)
        data=message.get_payload(decode=True)
        with open(filename,'wb') as pf:
            pf.write(data)        
        return filename

通过放开调试打印,我们可以看到邮件正文使用的字符集(编码方式): b’Content-Type: text/plain; charset=GBK\r\n’,利用get_content_charset()方法就能获取到邮件正文的编码方式 ,如果是消息主类型为text,则可以用该编码方式解析:

    #如果主类型为text,根据编码方式解析
    content_charset = message.get_content_charset()
    print('content_charset:',content_charset)          
    if maintype == 'text':
        mail_content = message.get_payload(decode=True).strip()
        try:
            print('mail_content:\n',mail_content.decode(content_charset))  
        except:  
            print('解码邮件错误') 

如果主类型是multipart,则调用自己,实现递归解析消息:

    elif maintype == 'multipart':
        for message_part in message.get_payload():
            parse_msg(message_part)        

完整的消息解析函数:

def parse_msg(message):
    type = message.get_content_type() 
    maintype = message.get_content_maintype()    
    subtype = message.get_content_subtype()
    print('type:',type)        
    print('maintype:',maintype)    
    print('subtype:',subtype)  

    boundary = message.get_boundary()
    print('boundary:',boundary) 
    
    #如果get_filename()返回非None,表示有附件
    filename = message.get_filename()
    if filename:
        print('找到1个附件,文件名:',filename)
        data=message.get_payload(decode=True)
        with open(filename,'wb') as pf:
            pf.write(data)        
        return filename
        
    #如果主类型为text,根据编码方式解析
    content_charset = message.get_content_charset()
    print('content_charset:',content_charset)          
    if maintype == 'text':
        mail_content = message.get_payload(decode=True).strip()
        try:
            print('mail_content:\n',mail_content.decode(content_charset))  
        except:  
            print('解码邮件错误')  

    #如果主类型为multipart,递归
    elif maintype == 'multipart':
        for message_part in message.get_payload():
            parse_msg(message_part)            
    return 

这是解析其中一封邮件的过程:

*cmd* 'RETR 5'
type: multipart/mixed
maintype: multipart
subtype: mixed
boundary: ----=_Part_70354_1982485947.1605182620221
content_charset: None
type: multipart/alternative
maintype: multipart
subtype: alternative
boundary: ----=_Part_70356_1595858516.1605182620221
content_charset: None
type: text/plain
maintype: text
subtype: plain
boundary: None
content_charset: gbk
mail_content:
 微信公众号:桔子code  ,这是一个包含二进制附件的邮件
type: text/html
maintype: text
subtype: html
boundary: None
content_charset: gbk
mail_content:
 <div style="line-height:1.7;color:#000000;font-size:14px;font-family:Arial"><p>微信公众号:桔子code&nbsp;&nbsp;,这是一个包含二进制附件的邮件<br></p></div><br><br><span title="neteasefooter"><p>&nbsp;</p></span>
type: application/vnd.ms-excel
maintype: application
subtype: vnd.ms-excel
boundary: None
找到1个附件,文件名: attached1.xls

进入邮箱看到的邮件正文、附件和前面解析到结果是一样的:

总结:到这里整个接收邮件的过程就介绍完了,主要是2大步骤:第一步poplib模块接收邮件:创建连接实例、登录、获取邮件头、获取邮件正文;第二步用email模块进行邮件内容的解析。

推荐阅读:
Python进阶教程m9–网络通信–socket通信
Python进阶教程m9b–网络通信–smtplib发送邮件 

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注