Python 使用 SMTP 发送邮件

SMTP 全称为 Simple Mail Transfer Protocol,即简单邮件传输协议,它是一组用于从源地址到目的地址传送邮件的规则,同时会控制信件的中转方式,一般我们发送邮件都是通过这一协议来完成的。

Python 内置的 smtplib 模块对 SMTP 协议进行了简单的封装,借助它我们可以很轻松的实现用代码来发送邮件。

连接 SMTP 服务器

要发送邮件,很明显需要先连接到一个可用的邮件服务器,为此我们需要指定服务器地址和端口。

由于各种历史遗留问题,现在仍在使用的 SMTP 服务端口有三个,分别是:25端口(明文传输)、465端口(SSL 加密)和 587端口(STARTTLS 加密)。不同的端口处理情况稍有不同,下面在代码中分别演示三种端口的连接方式。

1
2
3
4
5
6
7
8
9
10
11
import smtplib

# 25端口(明文传输)
smtp_server = smtplib.SMTP(host="smtp.xxx.xxx", port=25)

# 465端口(SSL加密)
smtp_server = smtplib.SMTP_SSL(host="smtp.xxx.xxx", port=465)

# 587端口(STARTTLS加密)
smtp_server = smtplib.SMTP(host="smtp.xxx.xxx", port=587)
smtp_server.starttls()

登录 SMTP 服务器

连上服务器之后还需要用我们的邮箱登录才能发送邮件(注意 QQ 邮箱、163 邮箱等使用 SMTP 服务需要的密码是在后台申请的授权码,不是你在网页上登录邮箱时用的密码)。

1
smtp_server.login(user="test@xxx.xxx", password="test_password")

构造邮件

电子邮件本质上可以看作一种按特定格式组织的文本文件,除了正文内容之外,标准邮件一般还需要三个头部信息: From(发件人), To(收件人)和 Subject(邮件主题)。所以说发送邮件并不是将你想发的内容传过去就行了,我们还需要先按照一定的规则“构造”一封邮件。

常见的邮件主要有纯文本邮件、HTML 邮件、带附件的邮件几种类型,下面分别演示这三种类型邮件的构造方式。

纯文本邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from email.mime.text import MIMEText

content = "这是一封纯文本邮件"
subject = "纯文本邮件测试"
from_user = "test@xxx.xxx"
to_user = "test2@xxx.xxx"

# 构造邮件主体
my_mail = MIMEText(content, _subtype="plain", _charset="utf8")
# 添加发件人
my_mail["From"] = from_user
# 添加收件人
my_mail["To"] = to_user
# 添加邮件主题
my_mail["subject"] = subject

HTML 邮件

构造 HTML 邮件只需要将构造纯文本邮件代码中 MIMEText() 的 _subtype 参数修改为 html 即可,如下:

1
my_mail = MIMEText(content, _subtype="html", _charset="utf8")

带附件的邮件

带附件的邮件与上面两种稍有不同,我们需要借助 MIMEMultipart() 构造一封多组件邮件,再将文本内容、附件内容依次添加进去,代码如下:

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
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

content = "这是一封带附件的邮件"
subject = "带附件的邮件测试"
from_user = "test@xxx.xxx"
to_user = "test2@xxx.xxx"

file_path = "xxx/test.txt" # 附件所在的路径
file_name = "test.txt" # 附件在邮件中显示的文件名
file_content = open(file_path, "rb").read() # 读取附件内容

# 构造一封多组件的邮件
my_mail = MIMEMultipart()
# 往多组件邮件中加入文本内容
text_msg = MIMEText(content, _subtype="plain", _charset="utf8")
my_mail.attach(text_msg)
# 往多组件邮件中加入附件
file_msg = MIMEApplication(file_content)
file_msg.add_header("content-disposition", "attachment", filename=file_name)
my_mail.attach(file_msg)
# 添加发件人
my_mail["From"] = from_user
# 添加收件人
my_mail["To"] = to_user
# 添加邮件主题
my_mail["subject"] = subject

发送邮件

构造好了邮件,连接并登录了 SMTP 服务器,接下来要发送邮件就很简单了,直接调用 send_message() 函数即可。

1
2
3
4
5
6
7
8
9
10
11
# 连接到服务器并登录好的SMTP对象
smtp_server = ......
# 前面构造好的邮件
my_mail = ......
# 发件人邮箱
from_user = "test@xxx.xxx"
# 收件邮箱
to_user = "test2@xxx.xxx"

# 发送邮件
smtp_server.send_message(my_mail)

封装邮件发送方法

像上面这样一步步构造邮件、发送邮件,写一次还好,经常需要这样写的话还是有点繁琐的,所以我们来给它稍微封装一下,以后直接调用即可:

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import os
import smtplib
import email
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText


class MailSender(object):
"""
邮件发送器,封装smtp发送邮件的常用操作
"""

def __init__(self, user: str, password: str, host: str, port: int):
"""
初始化smtp服务器连接

:param user: 邮箱用户,支持 name<prefix@example.com> 的形式,会自动从中提取邮箱地址用于登录
:param password: smtp登录密码
:param host: smtp服务器地址
:param port: smtp服务器端口,仅能使用25、465和587
"""
self.__user = user
# 提取出邮箱地址用于登录
self.__login_mail = email.utils.getaddresses([user])[0][1]

# 连接到smtp服务器,限制只允许使用25、465、587这三个端口
if port == 25:
self.__smtp_server = smtplib.SMTP(host=host, port=port)
elif port == 465:
self.__smtp_server = smtplib.SMTP_SSL(host=host, port=port)
elif port == 587:
self.__smtp_server = smtplib.SMTP(host=host, port=port)
self.__smtp_server.starttls()
else:
raise ValueError("Can only use port 25, 465 and 587")

# 登录smtp服务器
self.__smtp_server.login(user=self.__login_mail, password=password)

def send(
self, to_user: str, subject: str = "", content: str = "", subtype: str = "plain"
):
"""
发送纯文本邮件

:param to_user: 收件人,支持 name<prefix@example.com> 的形式,如需同时发给多人,将多个收件人用半角逗号隔开即可
:param subject: 邮件主题,默认为空字符串
:param content: 邮件正文,默认为空字符串
:param subtype: 邮件文本类型,只能为 plain 或 html,默认为 plain
"""
self.__check_subtype(subtype)

# 构造邮件
msg = MIMEText(content, _subtype=subtype, _charset="utf-8")
msg["From"] = self.__user
msg["To"] = to_user
msg["subject"] = subject

# 发送邮件
self.__smtp_server.send_message(msg)

def send_with_attachment(
self,
to_user: str,
attachment_path: str,
attachment_name: str = "",
subject: str = "",
content: str = "",
subtype: str = "plain",
):
"""
发送带附件的邮件

:param to_user: 收件人,支持 name<prefix@example.com> 的形式,如需同时发给多人,将多个收件人用半角逗号隔开即可
:param attachment_path: 附件文件的路径
:param attachment_name: 附件在邮件中显示的名字,设为空字符串时(默认)直接使用文件名
:param subject: 邮件主题,默认为空字符串
:param content: 邮件正文,默认为空字符串
:param subtype: 邮件文本类型,只能为 plain 或 html,默认为 plain
"""
self.__check_subtype(subtype)

# 读取附件内容
with open(attachment_path, "rb") as f:
file_content = f.read()
# 默认以文件名作为附件名
if attachment_name == "":
attachment_name = os.path.basename(attachment_path)

# 构造一封多组件邮件
msg = MIMEMultipart()
# 添加文本内容
text_msg = MIMEText(content, _subtype=subtype, _charset="utf-8")
msg.attach(text_msg)
# 添加附件
file_msg = MIMEApplication(file_content)
file_msg.add_header(
"content-disposition", "attachment", filename=attachment_name
)
msg.attach(file_msg)

msg["From"] = self.__user
msg["To"] = to_user
msg["subject"] = subject

# 发送邮件
self.__smtp_server.send_message(msg)

def __check_subtype(self, subtype: str):
if subtype not in ("plain", "html"):
raise ValueError('Error subtype, only "plain" and "html" can be used')
else:
pass

调用时只需要先实例化一个 MailSender 对象,然后就可以使用对应的 send 函数来发送邮件了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if __name__ == "__main__":
test_msg = """
<p>邮件发送测试</p>
<p><a href="https://xirikm.net/">这是一个链接</a></p>
"""
attachment_path = "xxx/xxx.txt" # 附件的文件路径

sender = MailSender("test@xxx.xxx", "test_password", "smtp.xxx.xxx", 587)

sender.send("test2@xxx.xxx", "纯文本邮件", test_msg, "plain")
sender.send("test2@xxx.xxx", "html邮件", test_msg, "html")
sender.send_with_attachment(
"test2@xxx.xxx", attachment_path, "xxx.txt", "带附件的html邮件", test_msg, "html"
)
# 默认参数测试
sender.send_with_attachment("test2@xxx.xxx", attachment_path)