Python 使用 SMTP 发送邮件

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

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

连接 SMTP 服务器

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

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

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 服务需要的密码是在后台申请的授权码,不是你在网页上登录邮箱时用的密码)。

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

构造邮件

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

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

纯文本邮件

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 即可,如下:

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

带附件的邮件

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

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() 函数即可。

# 连接到服务器并登录好的SMTP对象
smtp_server = ......
# 前面构造好的邮件
my_mail = ......
# 发件人邮箱
from_user = "test@xxx.xxx"
# 收件邮箱
to_user = "test2@xxx.xxx"

# 发送邮件
smtp_server.send_message(my_mail, from_addr=from_user, to_addrs=to_user)

封装邮件发送方法

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

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, from_addr=self.__user, to_addrs=to_user)

    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, from_addr=self.__user, to_addrs=to_user)

    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, from_addr=self.__user, to_addrs=to_user)

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

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)