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邮箱,如下图所示:
下面使用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后的结果如下所示:
在./目录下创建一个test.txt,文件内容如下图所示:
保存后看到运行结果如下图所示:
接收到的邮件: