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
# @Author: 夕日 <xirikm@gmail.com>
# @Time: 2020/04/14 19:02:41
# @URL: https://xirikm.net/2020/414-1.html

import smtplib
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: smtp用户名(邮箱)
:param password: smtp登录密码
:param host: smtp服务器地址
:param port: smtp服务器端口,仅能使用25、465和587
"""
self.__user = user

# 连接到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=user, password=password)

def send_text(self, to_user: str, subject: str, content: str):
"""
发送纯文本邮件

:param to_user: 收件人邮箱,如需同时发给多人,将地址使用半角逗号隔开即可
:param subject: 邮件主题
:param content: 邮件正文
"""
# 构造邮件
msg = MIMEText(content, _subtype="plain", _charset="utf-8")
msg["From"] = self.__user
msg["To"] = to_user
msg["subject"] = subject

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

def send_html(self, to_user: str, subject: str, content: str):
"""
发送html邮件

:param to_user: 收件人邮箱,如需同时发给多人,将地址使用半角逗号隔开即可
:param subject: 邮件主题
:param content: 邮件正文
"""
# 构造邮件
msg = MIMEText(content, _subtype="html", _charset="utf-8")
msg["From"] = self.__user
msg["To"] = to_user
msg["subject"] = subject

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

def send_file(
self,
to_user: str,
file_path: str,
file_name: str,
subject: str = "",
content: str = "",
):
"""
发送带附件的邮件

:param to_user: 收件人邮箱,如需同时发给多人,将地址使用半角逗号隔开即可
:param file_path: 附件路径
:param file_name: 附件在邮件中显示的文件名
:param subject: 邮件主题,默认为空字符串
:param content: 邮件正文,默认为空字符串
"""
# 读取附件内容
file_content = open(file_path, "rb").read()

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
if __name__ == "__main__":
test_text_msg = "纯文本邮件发送测试"
test_html_msg = """
<p>html邮件发送测试</p>
<p><a href="https://xirikm.net/">这是一个链接</a></p>
"""
attachment_file_path = "xxx/xxx.txt" # 附件的文件路径
attach_msg = "带附件的邮件发送测试"

sender = MailSender("test@xxx.xxx", "test_password", "smtp.xxx.xxx", 587)
sender.send_text("test2@xxx.xxx", "纯文本邮件", test_text_msg)
sender.send_html("test2@xxx.xxx", "html邮件", test_html_msg)
sender.send_file("test2@xxx.xxx", attachment_file_path, "xxx.txt", "带附件的邮件", attach_msg)