【Python】在邮箱收件箱中抽取无效的邮件地址

背景

笔者所在公司在某互联网公司租用了几十个邮箱账号,用于每月向客户发送服务类电子邮件,每个账号每天最多可以发送1000封邮件。如果使用本公司的SMTP服务器发送这些邮件,相信公司办公用邮件服务器IP地址很快就会被列入黑名单(RBL)。

近日笔者偶然发现这些客服用邮箱的收件箱里有大量的退信通知,这些邮件的标题绝大多数是“系统退信”,邮件内容是告知收件人邮件地址并不存在。这些邮件账号仅用于发送服务邮件,但收件箱中少的有2000多封,多的达到1W多封。笔者查看了管理员控制台,粗略算了一下,大约每发出10封邮件,有1封遭到退信。这些无效的邮件地址占用了发送邮件的配额,浪费了账号资源。

于是,笔者编写了一段Python程序,试图遍历所有邮箱,在收件箱中读取每一封邮件,对于邮件标题是“系统退信”的邮件,解析其正文,找出邮件地址,然后保存到一个文本文件list.txt中。同时把这封邮件以邮箱地址为文件名,以eml格式序列化。

程序中主要用到的技术

笔者在编写这个程序中遇到了访问邮箱方法、邮件内容编码和抽取邮件地址等几个具体的问题。

1、IMAPClient

Python访问邮箱读取邮件有多种方式,笔者选择了IMAPClient库。从以下链接可以下载安装包和说明文档。

https://pypi.org/project/IMAPClient

把下载的ZIP文件移到工作目录,在该目录下运行以下命令。

cd IMAPClient-2.1.0
python setup.py install

2、编码

笔者编写的这段程序是以别人程序为基础的。直接运行原始程序,所生成的邮件文件并没有后缀文件名。当笔者用记事本打开时,发现是乱码。不仅不能阅读,更无法使用正则表达式来抽取想要的数据。笔者将所生成的文件扩展名设为.eml,解决了邮件文件阅读问题。参考《【Python】Python新手必碰到的问题--encode与decode,中文乱码》这篇帖子,将邮件内容进行编码,解决了编码问题。

3、电子邮件的正则表达式

笔者在网上查找了许多在文本中抽取邮件地址的正则表达式,放在程序中进行测试。但发现客户邮件地址的格式比较复杂,邮箱名中经常含有下划线和点,域名部分经常有三段而不是两段,域名字符串里有的还含有“-”。最终在测试第7个正则表达式时,找到了比较靠谱的一个。

r'[\w\.-]+@[\w\.-]+\.[\w\.]+'

代码及说明

笔者是以《Python IMAP接收邮件--imapclient使用记录》这篇帖子中提供的代码作为基础进行改进而形成了以下代码。

以下代码在Win7中文版,Anaconda, Spyder(3.3.2), Python(3.6)下测试通过。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Edited by Eddie on 2019/1/4

from imapclient import IMAPClient
from email.header import decode_header
import re

class Imapmail(object):
 
    def __init__(self):  # 初始化数据
        self.serveraddress = None
        self.user = None
        self.passwd = None
        self.prot = None
        self.ssl = None
        self.timeout = None
        self.savepath = None
        self.server = None
 
    def client(self):  # 链接
        try:
            self.server = IMAPClient(self.serveraddress, self.prot, self.ssl, timeout=self.timeout)
            return self.server
        except BaseException as e:
            return "ERROR: >>> " + str(e)
 
    def login(self):  # 认证
        try:
            self.server.login(self.user, self.passwd)
        except BaseException as e:
            return "ERROR: >>> " + str(e)
 
    def getmaildir(self):  # 获取目录列表 [((), b'/', 'INBOX'), ((b'\\Drafts',), b'/', '草稿箱'),]
        dirlist = self.server.list_folders()
        return dirlist
 
    def getallmail(self):  # 收取所有邮件
#        print(self.server)
        self.server.select_folder('INBOX', readonly=True)  # 选择目录 readonly=True 只读,不修改,这里只选择了 收件箱
        result = self.server.search()  # 获取所有邮件总数目 [1,2,3,....]
        
        for _sm in result:
            data = self.server.fetch(_sm, ['ENVELOPE'])
#            size = self.server.fetch(_sm, ['RFC822.SIZE'])
#            print("大小", size)
            envelope = data[_sm][b'ENVELOPE']
#            print(envelope)
            try:
                subject = envelope.subject.decode()
            except:
                continue
            if subject:
                subject, de = decode_header(subject)[0]
                subject = subject if not de else subject.decode(de)
#            dates = envelope.date
#            print("主题", subject)
#            print("时间", dates)
            if subject == "系统退信":
                msgdict = self.server.fetch(_sm, ['BODY[]'])  # 获取邮件内容
                mailbody = msgdict[_sm][b'BODY[]']  # 获取邮件内容
                try:
                    mailbody1 = str(mailbody, encoding='utf-8') # 此时mailbox类型是bytes
                except UnicodeError:
                    print('UnicodeError')
                    with open(self.savepath + str(_sm) + '.eml', 'wb') as f:  # 存放邮件内容
                        f.write(mailbody)
                    continue
                except UnicodeDecodeError:
                    print('UnicodeDecodeError')
                    continue
                except UnicodeEncodeError:
                    print('UnicodeEncodeError')
                    continue
                except UnicodeTranslateError:
                    print('UnicodeTranslateError')
                    continue
                except :
                    print('Other Problem')
                    continue
                else:
                    print(mailbody.__class__)
                ''' 退信邮件中,是否是因为邮件不存在,有几种标志
                最后 4 个是对方反垃圾策略导致的
                '''
                notfoundreason=[
                        r'Mailbox not found',
                        r'User not found',
                        r'User no found',
                        r'Can not connect to',
                        r'Bad address syntax',
                        r'DNS query error',
                        r'The email account that your tried to reach does not exist',
                        r'no such user',
                        r'Recipient address rejected',
                        r'user access deny',
                        r'SpamTrap=reject mode',
                        r'relaying denied for'
                        ]
                notfoundre=[]
                notfound=[]
                notfoundreasonflag = 0
                for i in range(len(notfoundreason)):
                    notfoundre=re.compile(notfoundreason[i])
                    notfound=notfoundre.findall(mailbody1)
                    if notfound:
                        notfoundreasonflag = i+1
                        continue
                    
                mailre=re.compile(r'[\w\d\.-]+@[\w\.-]+\.[\w\.]+')
                text=mailre.findall(mailbody1)
#                不同的邮件内容,不同位置的邮件地址就是目标邮件地址
                
                if notfoundreasonflag > 0 and notfoundreasonflag <=8:
                    mailaddr2 = text[-2] #倒数第二个
                    mailaddr1=text[-1] #倒数第一个
                    if mailaddr2 in mailaddr1:
                        mailaddr=mailaddr2
                    else:
                        mailaddr=mailaddr1
                    print('The target email address is: No', str(_sm), '  ',mailaddr,'===')
                    with open(self.savepath +'_list.txt','a+') as l:
                        l.write(mailaddr+','+ str(notfoundreasonflag) + '\n')
#                    print('找到邮件地址的数量 ',str(len(text)))
#                    for i in range(len(text)):
#                        print(text[i])
                    mailfile = mailaddr
                    with open(self.savepath + mailfile + '.eml', 'wb') as f:  # 存放邮件内容
                        f.write(mailbody)
                              
    def close(self):
        self.server.logout()
 
 
if __name__ == "__main__":
    for i in range(n1,n2):
        imap = Imapmail()
        imap.serveraddress = "imap.domain.com"  # 邮箱地址
        imap.user = "mailbox"+str(i)+"@service.company.cn"  # 邮箱账号
        imap.passwd = "password"  # 邮箱密码
        imap.savepath = "c:\\163mail-" + str(i) + "\\"  # 邮件存放路径
        imap.client()
        imap.login()
        imap.getallmail()
        imap.close()

后记

由于是每个月都会给客户发邮件,所以针对某个邮件地址退信会分布在各个邮件账号中。将不同目录下的_list.txt文件中的数据放入其它软件中进行合并和去重处理,再清除一些明显错误的邮件地址,得到的数据发给业务部门,请他们不再给这些邮件地址发送邮件。同时把eml文件交给他们作为证据保留。

笔者发现list.txt文件中有一些是明显的错误邮件地址,有一个居然是XXX@263.NOT。这里笔者不得不吐槽一下业务部门、开发部门和数据运维部门,稍微留意就可以滤掉这些不正确的地址。

应该说这个程序十分粗糙,产生的数据中仍有一些瑕疵。未来如有需要,可以通过邮件的发出时间来缩小检查的范围,提高邮件地址识的精准度。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值