发送邮件
目前发送邮件的协议是 SMTP(Simple Mail Transfer Protocol, 简单邮件传输协议),是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。我们编写代码,实际上就是将待发送的消息使用 SMTP 协议的格式进行封装,再提交 SMTP 服务器进行发送的过程。
Python 内置的 smtplib
提供了一种很方便的途径发送电子邮件,可以发送纯文本邮件、HTML 邮件以及带附件的邮件。Python 对 SMTP 支持有 smtplib 和 email 两个模块,email 负责构造邮件,smtplib 负责发送邮件。
Python 创建 SMTP 对象的语法如下
1 | import smtplib |
参数说明:
- host: SMTP 服务器主机,可以指定主机的 IP 地址或域名,是可选参数;
- port: 如果提供了 host 参数,就需要指定 SMTP 服务使用的端口号,一般情况下 SMTP 端口号为 25;
- local_hostname: 如果 SMTP 在你的本机上,那么只需要指定服务器地址为 localhost 即可;
Python SMTP 对象使用 sendmail 方法发送邮件,其语法如下:
1 | SMTP.sendmail(from_adde, to_addrs, msg [, mail_options, rcpt_options]) |
参数说明:
- from_addr: 邮件发送者地址;
- to_addrs: 字符串列表,收件人地址;
- msg: 发送的消息;
第三个参数 msg 是字符串,表示邮件。我们知道邮件一般由标题、发件人、收件人、邮件内容、附件等组成,发送邮件时,要注意 msg 的格式。这个格式就是 SMTP 协议中定义的格式。
实例1: 构造简单的文本邮件
1
2
3from email.mime.text import MIMEText
message = MIMEText('Python 邮件发送测试...', 'plan', 'utf-8')注意构造 MIMEText 对象时,第一个参数就是邮件正文,第二个参数是 MIME 的 subtype,传入 plain,最终的 MIME 就是 ‘text/plain’,最后一定要用 UTF-8 编码保证多语言兼容性。
使用 Python 发送第一封简单的邮件(sendmail.py)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import smtplib
from email.mime.text import MIMEText
# 第三方 SMTP 服务
mail_host = "smtp.163.com" # 设置服务器
mail_user = "15626580887@163.com" # 设置用户名
mail_pass = "SZGVPPWCNAQBIXHU" # 设备授权码(或者用户密码)
sender = "15626580887@163.com"
receivers = ["15626580887@sina.cn", "2350686113@qq.com"] # 接收邮件用户
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) # 登录SMTP
smtpObj.sendmail(sender, receivers, message.as_string())
print("邮件发送成功!")
except smtplib.SMTPException as e:
print(f"邮件发送失败,错误原因: {e}")执行以上程序,屏幕上显示 “邮件发送成功!” 的信息后,即可看到收件箱里的邮件,如下
如果要发送 HTML 格式的邮件,需要修改构造正文部分,修改后内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13# 构造 HTML 格式的邮件正文
message = MIMEText(
'''
<html>
<body>
<h1> 这是邮件正文标题</h1>
<p>正文内容 <a href="#">超链接</a>...</p>
</body>
</html>
''',
"html",
"utf-8"
)使用 Python 发送一封带附件的邮件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
# 第三方 SMTP 服务
mail_host = "smtp.163.com" # 设置服务器
mail_user = "15626580887@163.com" # 设置用户名
mail_pass = "SZGVPPWCNAQBIXHU" # 设备授权码(或者用户密码)
sender = "15626580887@163.com"
receivers = ["15626580887@sina.cn", "2350686113@qq.com"] # 接收邮件用户
message = MIMEMultipart()
message["From"] = sender # 发件人,必须构造,也可以使用 Header 构造
message["To"] = ";".join(receivers) # 收件人列表
message["Subject"] = "邮件主题: SMTP 邮件测试"
# 邮件正文内容
message.attach(MIMEText('<p>这是正文: 图片及附件发送测试</p><p>图片演示: </p><p><img src="cid:image1"></p>', 'html', 'utf-8'))
# 指定图片文件
fp = open("/Users/wanwu/Pictures/5.jpg", "rb")
msgImage = MIMEImage(fp.read())
fp.close()
# 定义图片ID,在 HTML 文本中引用
msgImage.add_header("Content-ID", "<image1>")
message.attach(msgImage)
# 添加附件1,传送当前目录下的 wb.txt 文件
file1 = open("wb.txt", "rb")
att1 = MIMEText(file1.read(), "base64", "utf-8")
file1.close()
att1["Content-Type"] = "application/octet-stream"
att1["Content-Disposition"] = "attachment; filename=test.txt" # 这里的 filename 可以任意写,写什么名字,邮件中就显示什么名字
message.attach(att1)
# 添加附件2,传送当前目录下的 simple.log 文件
file2 = open('simple.log', 'rb')
att2 = MIMEText(file2.read(), "base64", "utf-8")
file2.close()
att2["Content-Type"] = "application/octet-stream"
att2.add_header(
"Content-Disposition",
"attachment",
filename=("utf-8", "", "simple.log") # 这里的 filename 可以任意写,写什么名字,邮件中就显示什么名字
)
message.attach(att2)
try:
smtpObj = smtplib.SMTP()
smtpObj.connect(mail_host, 25) # 25 为 SMTP 端口号
smtpObj.login(mail_user, mail_pass) # 登录SMTP
smtpObj.sendmail(sender, receivers, message.as_string())
print("邮件发送成功!")
except smtplib.SMTPException as e:
print(f"邮件发送失败,错误原因: {e}")
实战: 将报警信息发送至邮箱
在日常运维中经常用到监控,其常用的是短信报警、邮件报警等。相比短信报警,邮件报警是一个非常低成本的解决方法,无须付给运营商短信费用,而且一条短信有字数限制,而邮件无此限制,因此邮件报警可以看到更多的警告信息。
下面使用 Python 发送邮件的功能来实现报警信息实时发送至邮箱,具体需求说明如下:
文本文件 txt 约定格式: 第一行为收件人列表,以逗号分隔;第二行为主题,第三行至最后一行为正文内容,最后一行如果是文件,则作为附件发送,支持多个附件,以逗号分隔。如下
1
2
3
4
5
6xxx@163.com, yyyy@163.com
xxxx 程序报警
报警信息: ...
....
....
/home/log/xxx.log, /tmp/yyy.log持续监控一个目录 A 下的 txt 文件,如果有新增或修改,则读取文本中的内容并发送邮件
有报警需求的程序可生成以上示例格式的文本文件并传送至目录 A 即可。任意程序基本都可以实现该步骤
现在我们就用 Python 来实现上述需求,涉及的 Python 知识点有: 文本编码,读取文件,watchdog 模块应用以及发送邮件.
首先编写一个发送邮件的类,其功能是解析文本文件内容并发送邮件。txt2mail.py 文件内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101import 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" # 设置发送邮件服务器
self.auth_user = "15626580887@13.com" # 设置邮箱账号
self.auth_password = "SZGVPPWCNAQBIXHU" # 设置邮箱密码
self.sender = "15626580887@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 = os.path.basename(att)
attachment.add_header(
"Content-Disposition",
"attachment",
filename=("utf-8", "", filename)
)
message.attach(attachment)
smtpObj = smtplib.SMTP_SSL()
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): # 处理 UTF-8 编码
encoding = "utf-8-sig"
else:
result = chardet.detect(raw)
encoding = result["encoding"]
except Exception as e:
pass
return encoding
def txt_send_mail(self, filename):
"""
将制定格式的 txt 文件发送至邮箱,txt 文件样例如下
someone@xxx.com,someone2@xxx.com,... # 收件人
xxxx 程序报警 # 主题
程序 xxx 步骤 yyy 执行报错,错误代码 zz # 正文
详细信息请看附件 # 正文
file1,file2
:param filename:
:return:
"""
with open(filename, encoding=self.guess_chardet(filename)) as f:
lines = f.readlines()
recipient_list = lines[0].strip().split(",")
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 len(attachment_list) == 0:
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="./web.txt")上述代码实现了自定义的邮件类,功能是解析指定格式的文本文件并发送邮件,支持多个附件上传。接下来我们实现监控目录的功能,使用 watchdog 模块。
文件 watchDir.py 内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47import 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()
dirname = "./"
observer.schedule(event_handler, dirname, False)
print(f"当前监控的目录: {dirname}")
observer.start()
observer.join()watchDir 使用 watchdog 模块监控指定目录是否有后缀为 txt 的文本文件,如果有新增或修改的文本文件,则调用 txt2mail 中的 TXTMail 类中的 txt_send_mail 方法;如果发送不成功则表示文本文件格式错误,捕捉异常是为了避免程序崩溃退出。
接下来执行
watchDir.py
程序,结果如下1
当前监控的目录: ./
修改当前目录的 wb.txt 文件,保存后输出如下
1
2
3
4
5
6file modified:/Users/wanwu/Projects/system-scripts/Chapter02/wb.txt
文本文件格式不正确!
file created:/Users/wanwu/Projects/system-scripts/Chapter02/wb.txt~
directory modified:/Users/wanwu/Projects/system-scripts/Chapter02
file modified:/Users/wanwu/Projects/system-scripts/Chapter02/wb.txt~
directory modified:/Users/wanwu/Projects/system-scripts/Chapter02