Python邮件正文及附件解析

email邮件解析作为比较基础的模块,用来收取邮件、发送邮件。python的mail模块调用几行代码就能写一个发送/接受邮件的脚本。但是如果要做到持续稳定,能够上生产环境的代码,还是需要下一番功夫,解决编码和内容异常的问题。可能遇到的问题如下:

  • 邮件编码问题
  • 邮件日期格式解析
  • 多附件的下载
  • 邮件如何增量解析?

一、连接邮件服务器

首先,将邮件的账户密码配置化:

# config.py
MAIL = {
    "mail_host": "smtp.exmail.qq.com",  # SMTP服务器
    "mail_user": "xxx@abc.com",  # 用户名
    "mail_pwd": "fdaxxx",  # 登录密码
    "sender": "xxx@abc.com",  # 发件人邮箱
    "port":465  # SSL默认是465
}

创建邮件连接,获取邮件列表

from config.py import MAIL

# 连接到腾讯企业邮箱,其他邮箱调整括号里的参数
conn = imaplib.IMAP4_SSL(MAIL['mail_host'], MAIL['port'])
conn.login(MAIL['mail_user'], MAIL['mail_pwd'])
# 选定一个邮件文件夹
conn.select("INBOX")  # 获取收件箱

# 提取了文件夹中所有邮件的编号
resp, mails = conn.search(None, 'ALL')

# 提取了指定编号,按最新时间倒序
mails_list = mails[0].split()
mails_list = list(reversed(mails_list))
mail_nums = len(mails_list)

for i in range(mail_nums):
    print("mail: {}/{}".format(i+1, mail_nums))

    resp, data = conn.fetch(mails_list[i], '(RFC822)')   
    emailbody = data[0][1]
    mail = email.message_from_bytes(emailbody)

二.、邮件编码问题

邮件主题中是一般是可以获取到邮件编码的,但也有获取不准的时候,这时就会报错。这需要做编码兼容性处理。
decode_data()函数优先采用邮件内容获取的编码,如果解析不成功,就依次用UTF-8,GBK,GB2312编码来解析。

# 获取邮件自带的编码
from email.header import decode_header
mail_encode = decode_header(mail.get("Subject"))[0][1]
mail_title = decode_data(decode_header(mail.get("Subject"))[0][0], mail_encode)

def decode_data(bytes, added_encode=None):
    """
    字节解码
    :param bytes:
    :return:
    """
    def _decode(bytes, encoding):
        try:
            return str(bytes, encoding=encoding)
        except Exception as e:
            return None

    encodes = ['UTF-8', 'GBK', 'GB2312']
    if added_encode:
        encodes = [added_encode] + encodes
    for encoding in encodes:
        str_data = _decode(bytes, encoding)
        if str_data is not None:
            return str_data
    return None

三、邮件日期格式解析

邮件日期的格式一般是Mon, 8 Jun 2020 22:02:41 +0800这样的,也有8 Jun 2020 22:02:41 +0800,去掉了星期。
要做到兼容,我只需要解析中间的年月日时分秒。

from datetime import datetime

def parse_mail_time(mail_datetime):
    """
    邮件时间解析
    :param bytes:
    :return:
    """
    print(mail_datetime)
    GMT_FORMAT = "%a, %d %b %Y %H:%M:%S"
    GMT_FORMAT2 = "%d %b %Y %H:%M:%S"
    index = mail_datetime.find(' +0')
    if index > 0:
        mail_datetime = mail_datetime[:index] # 去掉+0800

    formats = [GMT_FORMAT, GMT_FORMAT2]
    for ft in formats:
        try:
            mail_datetime = datetime.strptime(mail_datetime, ft)
            return mail_datetime
        except:
            pass

    raise Exception("邮件时间格式解析错误")

四、邮件增量解析

我们定义邮件的表结构如下:

CREATE TABLE `mail_record_history` (
  `receive_time` datetime NOT NULL COMMENT '邮件接收时间',
  `title` varchar(200) NOT NULL COMMENT '邮件标题',
  `mail_from` varchar(100) DEFAULT NULL,
  `content` text COMMENT '邮件内容',
  `attachment` varchar(400) DEFAULT NULL COMMENT '邮件附件文件',
  `parse_time` datetime DEFAULT NULL COMMENT '解析时间',
  `status` int(11) DEFAULT NULL COMMENT '状态:-1:失败,0:正常; -2: 文件大小为0',
  PRIMARY KEY (`receive_time`,`title`)
)

mail_record_history表的每条记录对应一份邮件,邮件接受时间和邮件标题作为主键。
通过表字段receive_time的最大值来作为增量解析邮件的标准是有缺陷的。
python的mail模块接口没找到指定日期后的邮件,每次都是取全量的邮件序号,从最新的邮件开始解析,如果程序一切顺利(几乎不可能),那是没有问题的。
但是,只有出现一次错误,有可能是网络超时,有可能是邮件服务器不响应,有可能是解析服务器故障,就会出现从最新日期到数据库邮件最大日期之间丢失邮件。
而且下次再触发邮件解析时无法从中断处连续。
这里,我们用redis来存储最大邮件解析的时间点。

REDIS_PARAMS = {
        'host': "192、168.1.111",
        'port': 6379,
        'password': 'xxxxx',
        'db': 14,
    }

def get_redis_client():
    r = redis.Redis(host=REDIS_PARAMS['host'], port=REDIS_PARAMS['port'], password=REDIS_PARAMS['password'], db=REDIS_PARAMS['db'])
    return r

redis_client = get_redis_client()
REDIS_KEY = "max_mail_recieve_time" 

每次解析先获取数据库中最新的邮件时间

def get_max_mail_recieve_time():
    """
    获取数据库最新邮件时间
    :return:
    """
    max_receive_time = redis_client.get(REDIS_KEY)
    if max_receive_time is None or max_receive_time == 'None':
        max_receive_time = "2020-01-01 00:00:00"  #
        redis_client.set(REDIS_KEY, max_receive_time)

    if isinstance(max_receive_time, bytes):
        max_receive_time = str(max_receive_time, encoding='utf-8')
    return max_receive_time


从最新邮件开始解析,当邮件时间小于数据库最新时间时,就终止解析

import arrow

max_recieve_time = get_max_mail_recieve_time()
max_mail_time_str = None
for i in range(mail_nums):
    print("mail: {}/{}".format(i+1, mail_nums))

    resp, data = conn.fetch(mails_list[i], '(RFC822)')
   
    emailbody = data[0][1]
    mail = email.message_from_bytes(emailbody)
    mail_datetime = parse_mail_time(mail.get("date"))

    if arrow.get(mail_datetime) < arrow.get(max_recieve_time):
        return
    if i == 0:
        max_mail_time_str = arrow.get(mail_datetime).format("YYYY-MM-DD HH:mm")

当所有邮件都解析成功时,才更新redis的数据库最新时间(REDIS_KEY)。

if max_mail_time_str:
    redis_client.set(REDIS_KEY, max_mail_time_str)

五、邮件正文解析

mail_body = decode_data(get_body(mail))  

# 解析邮件内容
def get_body(msg):
    if msg.is_multipart():
        return get_body(msg.get_payload(0))
    else:
        return msg.get_payload(None,decode=True)

六、邮件附件下载

MAIL_DIR = '/tmp'
mail_date_str = '2020-06-09'

# 获取邮件附件
fileNames = []
for part in mail.walk():        
    fileName = part.get_filename()

    # 如果文件名为纯数字、字母时不需要解码,否则需要解码
    try:
        fileName = decode_header(fileName)[0][0].decode(decode_header(fileName)[0][1])
    except:
        pass

    # 如果获取到了文件,则将文件保存在制定的目录下
    if fileName:
        dirPath = os.path.join(MAIL_DIR, mail_date_str)
        os.system("chmod -R 777 {}".format(dirPath))
        if not os.path.exists(dirPath):
            os.makedirs(dirPath)

        filePath = os.path.join(dirPath, fileName)

        try:
            if not os.path.isfile(filePath):
                fp = open(filePath, 'wb')
                fp.write(part.get_payload(decode=True))
                fp.close()
                print("附件下载成功,文件名为:" + fileName)
            else:
                print("附件已经存在,文件名为:" + fileName)
        except Exception as e:
            print(e)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355