Python郵件正文及附件解析

email郵件解析作為比較基礎(chǔ)的模塊,用來收取郵件、發(fā)送郵件。python的mail模塊調(diào)用幾行代碼就能寫一個(gè)發(fā)送/接受郵件的腳本。但是如果要做到持續(xù)穩(wěn)定,能夠上生產(chǎn)環(huán)境的代碼,還是需要下一番功夫,解決編碼和內(nèi)容異常的問題??赡苡龅降膯栴}如下:

  • 郵件編碼問題
  • 郵件日期格式解析
  • 多附件的下載
  • 郵件如何增量解析?

一、連接郵件服務(wù)器

首先,將郵件的賬戶密碼配置化:

# config.py
MAIL = {
    "mail_host": "smtp.exmail.qq.com",  # SMTP服務(wù)器
    "mail_user": "xxx@abc.com",  # 用戶名
    "mail_pwd": "fdaxxx",  # 登錄密碼
    "sender": "xxx@abc.com",  # 發(fā)件人郵箱
    "port":465  # SSL默認(rèn)是465
}

創(chuàng)建郵件連接,獲取郵件列表

from config.py import MAIL

# 連接到騰訊企業(yè)郵箱,其他郵箱調(diào)整括號(hào)里的參數(shù)
conn = imaplib.IMAP4_SSL(MAIL['mail_host'], MAIL['port'])
conn.login(MAIL['mail_user'], MAIL['mail_pwd'])
# 選定一個(gè)郵件文件夾
conn.select("INBOX")  # 獲取收件箱

# 提取了文件夾中所有郵件的編號(hào)
resp, mails = conn.search(None, 'ALL')

# 提取了指定編號(hào),按最新時(shí)間倒序
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)

二.、郵件編碼問題

郵件主題中是一般是可以獲取到郵件編碼的,但也有獲取不準(zhǔn)的時(shí)候,這時(shí)就會(huì)報(bào)錯(cuò)。這需要做編碼兼容性處理。
decode_data()函數(shù)優(yōu)先采用郵件內(nèi)容獲取的編碼,如果解析不成功,就依次用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):
    """
    字節(jié)解碼
    :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,去掉了星期。
要做到兼容,我只需要解析中間的年月日時(shí)分秒。

from datetime import datetime

def parse_mail_time(mail_datetime):
    """
    郵件時(shí)間解析
    :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("郵件時(shí)間格式解析錯(cuò)誤")

四、郵件增量解析

我們定義郵件的表結(jié)構(gòu)如下:

CREATE TABLE `mail_record_history` (
  `receive_time` datetime NOT NULL COMMENT '郵件接收時(shí)間',
  `title` varchar(200) NOT NULL COMMENT '郵件標(biāo)題',
  `mail_from` varchar(100) DEFAULT NULL,
  `content` text COMMENT '郵件內(nèi)容',
  `attachment` varchar(400) DEFAULT NULL COMMENT '郵件附件文件',
  `parse_time` datetime DEFAULT NULL COMMENT '解析時(shí)間',
  `status` int(11) DEFAULT NULL COMMENT '狀態(tài):-1:失敗,0:正常; -2: 文件大小為0',
  PRIMARY KEY (`receive_time`,`title`)
)

mail_record_history表的每條記錄對(duì)應(yīng)一份郵件,郵件接受時(shí)間和郵件標(biāo)題作為主鍵。
通過表字段receive_time的最大值來作為增量解析郵件的標(biāo)準(zhǔn)是有缺陷的。
python的mail模塊接口沒找到指定日期后的郵件,每次都是取全量的郵件序號(hào),從最新的郵件開始解析,如果程序一切順利(幾乎不可能),那是沒有問題的。
但是,只有出現(xiàn)一次錯(cuò)誤,有可能是網(wǎng)絡(luò)超時(shí),有可能是郵件服務(wù)器不響應(yīng),有可能是解析服務(wù)器故障,就會(huì)出現(xiàn)從最新日期到數(shù)據(jù)庫郵件最大日期之間丟失郵件。
而且下次再觸發(fā)郵件解析時(shí)無法從中斷處連續(xù)。
這里,我們用redis來存儲(chǔ)最大郵件解析的時(shí)間點(diǎn)。

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" 

每次解析先獲取數(shù)據(jù)庫中最新的郵件時(shí)間

def get_max_mail_recieve_time():
    """
    獲取數(shù)據(jù)庫最新郵件時(shí)間
    :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


從最新郵件開始解析,當(dāng)郵件時(shí)間小于數(shù)據(jù)庫最新時(shí)間時(shí),就終止解析

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")

當(dāng)所有郵件都解析成功時(shí),才更新redis的數(shù)據(jù)庫最新時(shí)間(REDIS_KEY)。

if max_mail_time_str:
    redis_client.set(REDIS_KEY, max_mail_time_str)

五、郵件正文解析

mail_body = decode_data(get_body(mail))  

# 解析郵件內(nèi)容
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()

    # 如果文件名為純數(shù)字、字母時(shí)不需要解碼,否則需要解碼
    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("附件已經(jīng)存在,文件名為:" + fileName)
        except Exception as e:
            print(e)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容