2020-03-02 郵件提醒

1. 發(fā)送郵件

目前發(fā)送郵件的協(xié)議是SMTP(Simple Mail Transfer Protocol,簡單郵件傳輸協(xié)議),是一組用于由源地址到目的地址傳送郵件的規(guī)則,由它來控制信件的中轉(zhuǎn)方式。我們編寫代碼,實際上就是將待發(fā)送的消息使用SMTP協(xié)議的格式進行封裝,再提交SMTP服務(wù)器進行發(fā)送的過程。
Python內(nèi)置的smtplib提供了一種很方便的途徑發(fā)送電子郵件,可以發(fā)送純文本郵件、HTML郵件及帶附件的郵件。Python對SMTP支持有smtplib和email兩個模塊,email負責(zé)構(gòu)造郵件,smtplib負責(zé)發(fā)送郵件。
Python創(chuàng)建SMTP對象語法如下:

import smtplib
smtpObj = smtplib.SMTP( [host [, port [, local_hostname]]] )

參數(shù)說明:

  • host:SMTP服務(wù)器主機,可以指定主機的IP地址或域名,可選參數(shù)。
  • port:如果提供了host參數(shù),就需要指定SMTP服務(wù)使用的端口號,一般情況下SMTP端口號為25。
  • local_hostname:如果SMTP在你的本機上,那么只需要指定服務(wù)器地址為localhost即可。

Python SMTP對象使用sendmail方法發(fā)送郵件,其語法如下:

SMTP.sendmail(from_addr, to_addrs, msg[, mail_options, rcpt_options])

參數(shù)說明:

  • from_addr:郵件發(fā)送者地址。
  • to_addr:字符串列表,郵件發(fā)送地址。
  • msg:發(fā)送消息。

第三個參數(shù)msg是字符串,表示郵件。我們知道郵件一般由標題、發(fā)信人、收信人、郵件內(nèi)容、附件等組成,發(fā)送郵件時,要注意msg格式。這個格式就是SMTP協(xié)議中定義的格式。

示例:構(gòu)造簡單的文本郵件:

from email.mime.text import MIMEText
message = MIMEText('Python 郵件發(fā)送測試...', 'plain', 'utf-8')

注意構(gòu)造MIMEText對象時,第一個參數(shù)就是郵件正文,第二個參數(shù)是MIME的subtype,傳入plain,最終的MIME就是'text/plain',最后一定要用UTF-8編碼保證多語言兼容性。
在使用SMTP發(fā)送郵件之前,請確保所用郵箱的SMTP服務(wù)已開啟,例如QQ郵箱,如下圖所示:

SMTP設(shè)置方法

下面使用Python發(fā)送第一封簡單的郵件(sendmail.py)。

# -*- coding: UTF-8 -*-

import smtplib
from email.mime.text import MIMEText

# 第三方SMTP服務(wù)
mail_host = "smtp.163.com"  # 設(shè)置服務(wù)器
mail_user = "test@163.com"   # 用戶名
mail_pass = "shouquanma"    # 授權(quán)碼


sender = "test@163.com"
receivers = ["youremail@qq.com"]   # 接收郵件,可設(shè)置為QQ郵箱或其他郵箱

message = MIMEText("這是正文:郵件正文……", "plain", "utf-8") # 構(gòu)造正文
message["From"] = sender    # 發(fā)件人,必須構(gòu)造,也可以使用Header構(gòu)造
message["To"] = ";".join(receivers) # 收件人列表,不是必須的
message["Subject"] = "這是主題,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("發(fā)送成功")
except smtplib.SMTPException as e:
    print(f"發(fā)送失敗,錯誤原因: {e}")

執(zhí)行以上程序,屏幕上顯示“發(fā)送成功”的信息后,即可看到收件箱里的郵件,如下圖所示:

運行結(jié)果

發(fā)送HTML格式的郵件,上面的構(gòu)造正文部分修改如下:

message = MIMEText(
    '<html><body><h1>這是正文標題</h1>\
    <p>正文內(nèi)容<a href="#">超鏈接</a>...</p>\
    </body></html>',
    "html",
    "utf-8",
)   # 構(gòu)造正文

執(zhí)行后郵件內(nèi)容如下圖所示:

運行結(jié)果

示例:發(fā)送帶附件的郵件:

# -*- coding: UTF-8 -*-

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.header import Header

# 第三方SMTP服務(wù)
mail_host = "smtp.163.com"    # 設(shè)置服務(wù)器
mail_user = "xxxx@163.com"    # 用戶名
mail_pass = "aaaaa"  # 口令


sender = "xxxx@163.com"    # 發(fā)件人
to_receiver = ["yyyy@qq.com"]    # 接收郵件,可設(shè)置為QQ郵箱或其他郵箱
cc_receiver = ["xxxx@163.com"] # 抄送一份給自己
receivers = to_receiver + cc_receiver

message = MIMEMultipart()


message["From"] = sender    # 構(gòu)造發(fā)件人,也可以使用Header構(gòu)造
message["To"] = ";".join(to_receiver) # 收件人列表不是必需的
message["Cc"] = ";".join(cc_receiver)
message["Subject"] = "這是主題:SMTP郵件測試2"

# 郵件正文內(nèi)容



message.attach(MIMEText('<p>這是正文:圖片及附件發(fā)送測試</p><p>圖片演示:</p><p><img src="cid:image1"></p>', 'html', 'utf-8'))

# 指定圖片為當(dāng)前目錄
fp = open("1.jpg", "rb")
msgImage = MIMEImage(fp.read())
fp.close()

# 定義圖片ID,在HTML文本中引用
msgImage.add_header("Content_ID", "<image1>")
message.attach(msgImage)


# 添加附件1,傳送當(dāng)前目錄下的test.txt文件
att1 = MIMEText(open("test.txt", "rb").read(), "base64", "utf-8")
att1["Content-Type"] = "application/octet-stream"
# 這里的filename可以任意寫,寫什么名字,郵件中顯示什么名字
att1["Content-Disposition"] = 'attachment; filename="test.txt"'
message.attach(att1)

# 添加附件2,傳送當(dāng)前目錄下的測試.txt文件
att2 = MIMEText(open("測試.txt", "rb").read(), "base64", "utf-8")
att2["Content-Type"] = "application/octet-stream"
# 這里的filename可以任意寫,寫什么名字,郵件中顯示什么名字
att2.add_header("Content-Disposition", "attachment", filename=("gbk", "", "測試.txt"))
message.attach(att2)


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("發(fā)送成功")
except smtplib.SMTPException as e:
    print(f"發(fā)送失敗,錯誤原因: {e}")
抄送給自己

注意:發(fā)送郵件建議抄送一份給自己,否則有可能會報554 DT:SPM錯誤。

2. 接收郵件

接收郵件的協(xié)議有POP3(Post Office Protocol)和IMAP(Internet Message Access Protocol),Python內(nèi)置poplib模塊實現(xiàn)了POP3協(xié)議,可以直接用來接收郵件。
與SMTP協(xié)議類似,POP3協(xié)議收取的不是一個已經(jīng)可以閱讀的郵件本身,而是郵件的原始文本,要把POP3收取的文本變成可以閱讀的郵件,還需要用email模塊提供的各種類來解析原始文本,變成可閱讀的郵件對象。收取郵件分以下兩步。
第一步:用poplib模塊把郵件的原始文本下載到本地。
第二步:用email模塊解析原始文本,還原為郵件對象。
示例:編寫get_mail.py來演示如何使用poplib模塊接收郵件。

# -*- encoding:utf-8 -*-
import poplib
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr

# 輸入郵件地址、口令和POP3服務(wù)器地址
email = "xxxxx@163.com"
password = "******"
pop3_server = "pop.163.com"


# 連接到POP3服務(wù)器,如果開啟ssl,就使用poplib.POP3_SSL
server = poplib.POP3_SSL(pop3_server)
# 可以打開或關(guān)閉調(diào)試信息
# server.set_debuglevel(1)
# 可選:打印POP3服務(wù)器的歡迎文字
print(server.getwelcome().decode("utf-8"))

# 身份認證:
server.user(email)
server.pass_(password)

# stat()返回郵件數(shù)量和占用空間:
print("郵件數(shù)量:%s個. 大?。?.2fMB" % (server.stat()[0], server.stat()[1] / 1024 / 1024))


# list()返回所有郵件的編號:
resp, mails, octets = server.list()
# 可以查看返回的列表,類似[b'1 82923', b'2 2184', ...]


# 獲取最新的一封郵件,注意索引導(dǎo)從1開始,最新的郵件索引即為郵件的總個數(shù)
index = len(mails)
resp, lines, octets = server.retr(index)

# lines存儲了郵件的原始文本的每一行可以獲得整個郵件的原始文本
msg_content = b"\r\n".join(lines).decode("utf-8")
# 稍后解析出郵件
msg = Parser().parsestr(msg_content)


def decode_str(s):
    value, charset = decode_header(s)[0]
    if charset:
        value = value.decode(charset)
    return value


print("解析獲取到的郵件內(nèi)容如下:\n----------begin----------")
# 打印發(fā)件人信息
print(
    f"{ decode_str(parseaddr(msg.get('From',''))[0])}<{decode_str(parseaddr( msg.get('From',''))[1])}>"
)
# 打印收件人信息
print(
    f"{ decode_str(parseaddr(msg.get('To',''))[0])}<{decode_str(parseaddr( msg.get('To',''))[1])}>"
)
# 打印主題信息
print(decode_str(msg["Subject"]))
# 打印第一條正文信息
part0 = msg.get_payload()[0]
content = part0.get_payload(decode=True)
print(content.decode(part0.get_content_charset()))
print("----------end----------")

# 可以根據(jù)郵件索引號直接從服務(wù)器刪除郵件
# server.dele(index)
# 關(guān)閉連接:
server.quit()

在代碼的64行,我們使用part0.get_content_charset()編碼來解碼郵件正文。執(zhí)行上面的代碼得到如下結(jié)果。

運行結(jié)果

3. 將報警信息實時發(fā)送至郵箱

在日常運維中經(jīng)常用到監(jiān)控,其常用的是短信報警、郵件報警等。相比短信報警,郵件報警是一個非常低成本的解決方法,無須付給運營商短信費用,一條短信有字數(shù)限制,而郵件無此限制,因此郵件報警可以看到更多警告信息。
下面使用Python發(fā)送郵件的功能來實現(xiàn)報警信息實時發(fā)送至郵箱,具體需求說明如下。
(1)文本文件txt約定格式:第一行為收件人列表,以逗號分隔;第二行為主題,第三行至最后一行為正文內(nèi)容,最后一行如果是文件,則作為附件發(fā)送,支持多個附件,以逗號分隔。
下面是一個完整的例子。

xxx@163.com,yyy@163.com
xxx程序報警
報警信息...
...
...
/home/log/xxx.log,/tmp/yyy.log

(2)持續(xù)監(jiān)控一個目錄A下的txt文件,如果有新增或修改,則讀取文本中的內(nèi)容并發(fā)送郵件。
(3)有報警需求的程序可生成(1)中格式的文本文件并傳送至目錄A即可。任意程序基本都可以實現(xiàn)本步驟。
現(xiàn)在使用Python來實現(xiàn)上述需求,涉及的Python知識點有:文件編碼、讀文件操作、watchdog模塊應(yīng)用及發(fā)送郵件。
示例:首先編寫一個發(fā)送郵件的類,其功能是解析文本文件內(nèi)容并發(fā)送郵件(txt2mail.py)。

# -*- coding: utf-8 -*-
import smtplib
import chardet
import codecs
import os
from email.mime.text import MIMEText
from email.header import Header
from email.mime.multipart import MIMEMultipart

# 第三方SMTP服務(wù)
class txtMail(object):

    def __init__(self, host=None, auth_user=None, auth_password=None):
        self.host = "smtp.163.com" if host is None else host    # 設(shè)置發(fā)送郵件服務(wù)器
        self.auth_user = "xxxxx" if auth_user is None else auth_user    # 上線時使用專用報警賬戶的用戶名
        self.auth_password = (
            "******" if auth_password is None else auth_password
        )   # 上線時使用專用報警賬戶的密碼
        self.sender = "xxxxx@163.com"

    def send_mail(self, subject, msg_str, recipient_list, attachment_list=None):
        message = MIMEMultipart()
        message["From"] = self.sender
        message["To"] = Header(";".join(recipient_list), "utf-8")
        message["Subject"] = Header(subject, "utf-8")
        message.attach(MIMEText(msg_str, "plain", "utf-8"))

        # 如果有附件,則添加附件
        if attachment_list:
            for att in attachment_list:
                attachment = MIMEText(open(att, "rb").read(), "base64", "utf-8")
                attachment["Content-Type"] = "application/octet-stream"
                # 這里的filename可以任意寫,寫什么名字,郵件中顯示什么名字
                # attname=att.split("/")[-1]
                filename = os.path.basename(att)
                # attm["Content-Disposition"] = 'attachment; filename=%s'%attname
                attachment.add_header(
                    "Content-Disposition",
                    "attachment",
                    filename=("utf-8", "", filename),
                )
                message.attach(attachment)

        smtpObj = smtplib.SMTP_SSL(self.host)
        smtpObj.connect(self.host, smtplib.SMTP_SSL_PORT)
        smtpObj.login(self.auth_user, self.auth_password)
        smtpObj.sendmail(self.sender, recipient_list, message.as_string())
        smtpObj.quit()
        print("郵件發(fā)送成功")

    def guess_chardet(self, filename):
        """
        :param filename:傳入一個文本文件
        :return: 返回文本文件的編碼格式
        """
        encoding = None
        try:
            # 由于本需求所解析的文本文件都不大,可以一次性讀入內(nèi)存
            # 如果是大文件,則讀取固定字節(jié)數(shù)
            raw = open(filename, "rb").read()
            if raw.startswith(codecs.BOM_UTF8):
                encoding = "utf-8-sig"
            else:
                result = chardet.detect(raw)
                encoding = result["encoding"]
        except:
            pass
        return encoding

    def txt_send_mail(self, filename):
        '''
        :param filename:
        :return:
        將指定格式的txt文件發(fā)送至郵件,txt文件樣例如下
        someone@xxx.com,someone2@xxx.com...#收信人,逗號分隔
        xxx程序報警 #主題
        程序xxx步驟yyy執(zhí)行報錯,報錯代碼zzz  #正文
        詳細信息請看附件    #正文
        file1,file2 #附件,逗號分隔,非必須
        '''

        with open(filename, encoding=self.guess_chardet(filename)) as f:
            lines = f.readlines()
        recipient_list = lines[0].strip().splipt(",")
        subject = lines[1].strip()
        msg_str = "".join(lines[2:])
        attachment_list = []
        for file in lines[-1].strip().split(","):
            if os.path.isfile(file):
                attachment_list.append(file)
            # 如果沒有附件,則為None
            if attachment_list == []:
                attachment_list = None
            self.send_mail(
                subject=subject,
                msg_str=msg_str,
                recipient_list=recipient_list,
                attachment_list=attachment_list,
            )


    if __name__ == "__main__":
        mymail = txtMail()
        mymail.txt_send_mail(filename="./test.txt")

上述代碼實現(xiàn)了自定義的郵件類,功能是解析指定格式的文本文件并發(fā)送郵件,支持多個附件上傳。
接下來實現(xiàn)監(jiān)控目錄的功能,使用watchdog模塊。
文件watchDir.py內(nèi)容如下:

# -*- coding: utf-8 -*-

import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from txt2mail import txtMail


class FileEventHandler(FileSystemEventHandler):
    
    def __init__(self):
        FileSystemEventHandler.__init__(self)
        
    def on_created(self, event):
        if event.is_directory:
            print("directory created:{0}".format(event.src_path))
        else:
            print("file created:{0}".format(event.src_path))
            if event.src_path.endswith(".txt"):
                time.sleep(1)
                mail = txtMail()
                try:
                    mail.txt_send_mail(filename=event.src_path)
                except:
                    print("文本文件格式不正確")
                
    def on_modified(self, event):
        if event.is_directory:
            print("directory modified:{0}".format(event.src_path))
        else:
            print("file modified:{0}".format(event.src_path))
            if event.src_path.endswith(".txt"):
                time.sleep(1)
                mail = txtMail()
                try:
                    mail.txt_send_mail(filename=event.src_path)
                except:
                    print("文本文件格式不正確")
                    
                    
    if __name__ == "__main__":
        observer = Observer()
        event_handler = FileEventHandler()
        dir = "./"
        observer.schedule(event_handler, dir, False)
        print(f"當(dāng)前監(jiān)控的目錄:{dir}")
        observer.start()
        observer.join()

watchdir使用watchdog模塊監(jiān)控指定目錄是否有后綴為txt的文本文件,如果有新增或修改的文本文件,則調(diào)用txt2mail類的txt_send_mail方法;如果發(fā)送不成功則表明文本文件格式錯誤,捕捉異常是為了避免程序崩潰退出。
執(zhí)行python watchdir.py后的結(jié)果如下所示:

image.png

在./目錄下創(chuàng)建一個test.txt,文件內(nèi)容如下圖所示:

image.png

保存后看到運行結(jié)果如下圖所示:

image.png

接收到的郵件:

image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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