2020-03-02 邮件提醒

1. 发送邮件

目前发送邮件的协议是SMTP(Simple Mail Transfer Protocol,简单邮件传输协议),是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。我们编写代码,实际上就是将待发送的消息使用SMTP协议的格式进行封装,再提交SMTP服务器进行发送的过程。
Python内置的smtplib提供了一种很方便的途径发送电子邮件,可以发送纯文本邮件、HTML邮件及带附件的邮件。Python对SMTP支持有smtplib和email两个模块,email负责构造邮件,smtplib负责发送邮件。
Python创建SMTP对象语法如下:

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

参数说明:

  • host:SMTP服务器主机,可以指定主机的IP地址或域名,可选参数。
  • port:如果提供了host参数,就需要指定SMTP服务使用的端口号,一般情况下SMTP端口号为25。
  • local_hostname:如果SMTP在你的本机上,那么只需要指定服务器地址为localhost即可。

Python SMTP对象使用sendmail方法发送邮件,其语法如下:

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

参数说明:

  • from_addr:邮件发送者地址。
  • to_addr:字符串列表,邮件发送地址。
  • msg:发送消息。

第三个参数msg是字符串,表示邮件。我们知道邮件一般由标题、发信人、收信人、邮件内容、附件等组成,发送邮件时,要注意msg格式。这个格式就是SMTP协议中定义的格式。

示例:构造简单的文本邮件:

from email.mime.text import MIMEText
message = MIMEText('Python 邮件发送测试...', 'plain', 'utf-8')

注意构造MIMEText对象时,第一个参数就是邮件正文,第二个参数是MIME的subtype,传入plain,最终的MIME就是'text/plain',最后一定要用UTF-8编码保证多语言兼容性。
在使用SMTP发送邮件之前,请确保所用邮箱的SMTP服务已开启,例如QQ邮箱,如下图所示:

SMTP设置方法

下面使用Python发送第一封简单的邮件(sendmail.py)。

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

import smtplib
from email.mime.text import MIMEText

# 第三方SMTP服务
mail_host = "smtp.163.com"  # 设置服务器
mail_user = "[email protected]"   # 用户名
mail_pass = "shouquanma"    # 授权码


sender = "[email protected]"
receivers = ["[email protected]"]   # 接收邮件,可设置为QQ邮箱或其他邮箱

message = MIMEText("这是正文:邮件正文……", "plain", "utf-8") # 构造正文
message["From"] = sender    # 发件人,必须构造,也可以使用Header构造
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("发送成功")
except smtplib.SMTPException as e:
    print(f"发送失败,错误原因: {e}")

执行以上程序,屏幕上显示“发送成功”的信息后,即可看到收件箱里的邮件,如下图所示:

运行结果

发送HTML格式的邮件,上面的构造正文部分修改如下:

message = MIMEText(
    '

这是正文标题

\

正文内容超链接...

\ ', "html", "utf-8", ) # 构造正文

执行后邮件内容如下图所示:

运行结果

示例:发送带附件的邮件:

# -*- 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服务
mail_host = "smtp.163.com"    # 设置服务器
mail_user = "[email protected]"    # 用户名
mail_pass = "aaaaa"  # 口令


sender = "[email protected]"    # 发件人
to_receiver = ["[email protected]"]    # 接收邮件,可设置为QQ邮箱或其他邮箱
cc_receiver = ["[email protected]"] # 抄送一份给自己
receivers = to_receiver + cc_receiver

message = MIMEMultipart()


message["From"] = sender    # 构造发件人,也可以使用Header构造
message["To"] = ";".join(to_receiver) # 收件人列表不是必需的
message["Cc"] = ";".join(cc_receiver)
message["Subject"] = "这是主题:SMTP邮件测试2"

# 邮件正文内容



message.attach(MIMEText('

这是正文:图片及附件发送测试

图片演示:

', 'html', 'utf-8')) # 指定图片为当前目录 fp = open("1.jpg", "rb") msgImage = MIMEImage(fp.read()) fp.close() # 定义图片ID,在HTML文本中引用 msgImage.add_header("Content_ID", "") message.attach(msgImage) # 添加附件1,传送当前目录下的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,传送当前目录下的测试.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("发送成功") except smtplib.SMTPException as e: print(f"发送失败,错误原因: {e}")
抄送给自己

注意:发送邮件建议抄送一份给自己,否则有可能会报554 DT:SPM错误。

2. 接收邮件

接收邮件的协议有POP3(Post Office Protocol)和IMAP(Internet Message Access Protocol),Python内置poplib模块实现了POP3协议,可以直接用来接收邮件。
与SMTP协议类似,POP3协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,要把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服务器地址
email = "[email protected]"
password = "******"
pop3_server = "pop.163.com"


# 连接到POP3服务器,如果开启ssl,就使用poplib.POP3_SSL
server = poplib.POP3_SSL(pop3_server)
# 可以打开或关闭调试信息
# server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字
print(server.getwelcome().decode("utf-8"))

# 身份认证:
server.user(email)
server.pass_(password)

# stat()返回邮件数量和占用空间:
print("邮件数量:%s个. 大小:%.2fMB" % (server.stat()[0], server.stat()[1] / 1024 / 1024))


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


# 获取最新的一封邮件,注意索引导从1开始,最新的邮件索引即为邮件的总个数
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----------begin----------")
# 打印发件人信息
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----------")

# 可以根据邮件索引号直接从服务器删除邮件
# server.dele(index)
# 关闭连接:
server.quit()

在代码的64行,我们使用part0.get_content_charset()编码来解码邮件正文。执行上面的代码得到如下结果。

运行结果

3. 将报警信息实时发送至邮箱

在日常运维中经常用到监控,其常用的是短信报警、邮件报警等。相比短信报警,邮件报警是一个非常低成本的解决方法,无须付给运营商短信费用,一条短信有字数限制,而邮件无此限制,因此邮件报警可以看到更多警告信息。
下面使用Python发送邮件的功能来实现报警信息实时发送至邮箱,具体需求说明如下。
(1)文本文件txt约定格式:第一行为收件人列表,以逗号分隔;第二行为主题,第三行至最后一行为正文内容,最后一行如果是文件,则作为附件发送,支持多个附件,以逗号分隔。
下面是一个完整的例子。

[email protected],[email protected]
xxx程序报警
报警信息...
...
...
/home/log/xxx.log,/tmp/yyy.log

(2)持续监控一个目录A下的txt文件,如果有新增或修改,则读取文本中的内容并发送邮件。
(3)有报警需求的程序可生成(1)中格式的文本文件并传送至目录A即可。任意程序基本都可以实现本步骤。
现在使用Python来实现上述需求,涉及的Python知识点有:文件编码、读文件操作、watchdog模块应用及发送邮件。
示例:首先编写一个发送邮件的类,其功能是解析文本文件内容并发送邮件(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服务
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    # 设置发送邮件服务器
        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 = "[email protected]"

    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("邮件发送成功")

    def guess_chardet(self, filename):
        """
        :param filename:传入一个文本文件
        :return: 返回文本文件的编码格式
        """
        encoding = None
        try:
            # 由于本需求所解析的文本文件都不大,可以一次性读入内存
            # 如果是大文件,则读取固定字节数
            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文件发送至邮件,txt文件样例如下
        [email protected],[email protected]...#收信人,逗号分隔
        xxx程序报警 #主题
        程序xxx步骤yyy执行报错,报错代码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")

上述代码实现了自定义的邮件类,功能是解析指定格式的文本文件并发送邮件,支持多个附件上传。
接下来实现监控目录的功能,使用watchdog模块。
文件watchDir.py内容如下:

# -*- 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"当前监控的目录:{dir}")
        observer.start()
        observer.join()

watchdir使用watchdog模块监控指定目录是否有后缀为txt的文本文件,如果有新增或修改的文本文件,则调用txt2mail类的txt_send_mail方法;如果发送不成功则表明文本文件格式错误,捕捉异常是为了避免程序崩溃退出。
执行python watchdir.py后的结果如下所示:

image.png

在./目录下创建一个test.txt,文件内容如下图所示:

image.png

保存后看到运行结果如下图所示:

image.png

接收到的邮件:

image.png

你可能感兴趣的:(2020-03-02 邮件提醒)