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 = "test@163.com"   # 用户名
mail_pass = "shouquanma"    # 授权码


sender = "test@163.com"
receivers = ["youremail@qq.com"]   # 接收邮件,可设置为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><body><h1>这是正文标题</h1>\
    <p>正文内容<a href="#">超链接</a>...</p>\
    </body></html>',
    "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 = "xxxx@163.com"    # 用户名
mail_pass = "aaaaa"  # 口令


sender = "xxxx@163.com"    # 发件人
to_receiver = ["yyyy@qq.com"]    # 接收邮件,可设置为QQ邮箱或其他邮箱
cc_receiver = ["xxxx@163.com"] # 抄送一份给自己
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('<p>这是正文:图片及附件发送测试</p><p>图片演示:</p><p><img src="cid:image1"></p>', 'html', 'utf-8'))

# 指定图片为当前目录
fp = open("1.jpg", "rb")
msgImage = MIMEImage(fp.read())
fp.close()

# 定义图片ID,在HTML文本中引用
msgImage.add_header("Content_ID", "<image1>")
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 = "xxxxx@163.com"
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约定格式:第一行为收件人列表,以逗号分隔;第二行为主题,第三行至最后一行为正文内容,最后一行如果是文件,则作为附件发送,支持多个附件,以逗号分隔。
下面是一个完整的例子。

xxx@163.com,yyy@163.com
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 = "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("邮件发送成功")

    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文件样例如下
        someone@xxx.com,someone2@xxx.com...#收信人,逗号分隔
        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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容