Skip to content

Python发送邮件和短信 ​

在前面的课程中,我们已经教会大家如何用 Python 程序自动的生成 Excel、Word、PDF 文档,接下来我们还可以更进一步,就是通过邮件将生成好的文档发送给指定的收件人,然后用短信告知对方我们发出了邮件。这些事情利用 Python 程序也可以轻松愉快的解决。

发送电子邮件 ​

在即时通信软件如此发达的今天,电子邮件仍然是互联网上使用最为广泛的应用之一,公司向应聘者发出录用通知、网站向用户发送一个激活账号的链接、银行向客户推广它们的理财产品等几乎都是通过电子邮件来完成的,而这些任务应该都是由程序自动完成的。

我们可以用 HTTP(超文本传输协议)来访问网站资源,HTTP 是一个应用级协议,它建立在 TCP(传输控制协议)之上,TCP 为很多应用级协议提供了可靠的数据传输服务。如果要发送电子邮件,需要使用 SMTP(简单邮件传输协议),它也是建立在 TCP 之上的应用级协议,规定了邮件的发送者如何跟邮件服务器进行通信的细节。Python 通过名为smtplib的模块将这些操作简化成了SMTP_SSL对象,通过该对象的login和send_mail方法,就能够完成发送邮件的操作。

我们先尝试一下发送一封极为简单的邮件,该邮件不包含附件、图片以及其他超文本内容。发送邮件首先需要接入邮件服务器,我们可以自己架设邮件服务器,这件事情对新手并不友好,但是我们可以选择使用第三方提供的邮件服务。例如,我在<www.126.com>已经注册了账号,登录成功之后,就可以在设置中开启 SMTP 服务,这样就相当于获得了邮件服务器,具体的操作如下所示。

image-20210820190306861

用手机扫码上面的二维码可以通过发送短信的方式来获取授权码,短信发送成功后,点击“我已发送”就可以获得授权码。授权码需要妥善保管,因为一旦泄露就会被其他人冒用你的身份来发送邮件。接下来,我们就可以编写发送邮件的代码了,如下所示。

python
import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# 创建邮件主体对象
email = MIMEMultipart()
# 设置发件人、收件人和主题
email['From'] = 'xxxxxxxxx@126.com'
email['To'] = 'yyyyyy@qq.com;zzzzzz@1000phone.com'
email['Subject'] = Header('上半年工作情况汇报', 'utf-8')
# 添加邮件正文内容
content = """据德国媒体报道,当地时间9日,德国火车司机工会成员进行了投票,
定于当地时间10日起进行全国性罢工,货运交通方面的罢工已于当地时间10日19时开始。
此后,从11日凌晨2时到13日凌晨2时,德国全国范围内的客运和铁路基础设施将进行48小时的罢工。"""
email.attach(MIMEText(content, 'plain', 'utf-8'))

# 创建SMTP_SSL对象(连接邮件服务器)
smtp_obj = smtplib.SMTP_SSL('smtp.126.com', 465)
# 通过用户名和授权码进行登录
smtp_obj.login('xxxxxxxxx@126.com', '邮件服务器的授权码')
# 发送邮件(发件人、收件人、邮件内容(字符串))
smtp_obj.sendmail(
    'xxxxxxxxx@126.com',
    ['yyyyyy@qq.com', 'zzzzzz@1000phone.com'],
    email.as_string()
)

如果要发送带有附件的邮件,只需要将附件的内容处理成 BASE64 编码,那么它就和普通的文本内容几乎没有什么区别。BASE64 是一种基于 64 个可打印字符来表示二进制数据的表示方法,常用于某些需要表示、传输、存储二进制数据的场合,电子邮件就是其中之一。对这种编码方式不理解的同学,推荐阅读《Base64笔记》一文。在之前的内容中,我们也提到过,Python 标准库的base64模块提供了对 BASE64 编解码的支持。

下面的代码演示了如何发送带附件的邮件。

python
import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from urllib.parse import quote

# 创建邮件主体对象
email = MIMEMultipart()
# 设置发件人、收件人和主题
email['From'] = 'xxxxxxxxx@126.com'
email['To'] = 'zzzzzzzz@1000phone.com'
email['Subject'] = Header('请查收离职证明文件', 'utf-8')
# 添加邮件正文内容(带HTML标签排版的内容)
content = """<p>亲爱的前同事:</p>
<p>你需要的离职证明在附件中,请查收!</p>
<br>
<p>祝,好!</p>
<hr>
<p>孙美丽 即日</p>"""
email.attach(MIMEText(content, 'html', 'utf-8'))
# 读取作为附件的文件
with open(f'resources/王大锤离职证明.docx', 'rb') as file:
    attachment = MIMEText(file.read(), 'base64', 'utf-8')
    # 指定内容类型
    attachment['content-type'] = 'application/octet-stream'
    # 将中文文件名处理成百分号编码
    filename = quote('王大锤离职证明.docx')
    # 指定如何处置内容
    attachment['content-disposition'] = f'attachment; filename="{filename}"'

# 创建SMTP_SSL对象(连接邮件服务器)
smtp_obj = smtplib.SMTP_SSL('smtp.126.com', 465)
# 通过用户名和授权码进行登录
smtp_obj.login('xxxxxxxxx@126.com', '邮件服务器的授权码')
# 发送邮件(发件人、收件人、邮件内容(字符串))
smtp_obj.sendmail(
    'xxxxxxxxx@126.com',
    'zzzzzzzz@1000phone.com',
    email.as_string()
)

为了方便大家用 Python 实现邮件发送,我将上面的代码封装成了函数,使用的时候大家只需要调整邮件服务器域名、端口、用户名和授权码就可以了。

python
import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from urllib.parse import quote

# 邮件服务器域名(自行修改)
EMAIL_HOST = 'smtp.126.com'
# 邮件服务端口(通常是465)
EMAIL_PORT = 465
# 登录邮件服务器的账号(自行修改)
EMAIL_USER = 'xxxxxxxxx@126.com'
# 开通SMTP服务的授权码(自行修改)
EMAIL_AUTH = '邮件服务器的授权码'


def send_email(*, from_user, to_users, subject='', content='', filenames=None):
    """发送邮件
    
    :param from_user: 发件人
    :param to_users: 收件人,多个收件人用英文分号进行分隔
    :param subject: 邮件的主题
    :param content: 邮件正文内容
    :param filenames: 附件要发送的文件路径
    """
    filenames = filenames or []
    email = MIMEMultipart()
    email['From'] = from_user
    email['To'] = to_users
    email['Subject'] = subject

    message = MIMEText(content, 'plain', 'utf-8')
    email.attach(message)
    for filename in filenames:
        with open(filename, 'rb') as file:
            pos = filename.rfind('/')
            display_filename = filename[pos + 1:] if pos >= 0 else filename
            display_filename = quote(display_filename)
            attachment = MIMEText(file.read(), 'base64', 'utf-8')
            attachment['content-type'] = 'application/octet-stream'
            attachment['content-disposition'] = f'attachment; filename="{display_filename}"'
            email.attach(attachment)

    smtp = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT)
    smtp.login(EMAIL_USER, EMAIL_AUTH)
    smtp.sendmail(from_user, to_users.split(';'), email.as_string())

注意:send_email函数的filenames参数默认值是None而不是空列表,这里有一个重要的最佳实践:永远不要用列表、字典等可变对象作为默认参数。因为默认值在函数定义时只会被创建一次,之后会被所有的调用共享,如果某次调用修改了它(例如向列表中添加了元素),后续调用看到的默认值就会发生改变,从而产生难以排查的错误。所以这里用None作为默认值,并在函数体内通过filenames = filenames or []将其处理成空列表。

发送短信 ​

发送短信也是项目中常见的功能,网站的注册码、验证码、营销信息基本上都是通过短信来发送给用户的。发送短信需要三方平台(短信服务商)的支持,国内常用的有阿里云、腾讯云、华为云等云服务商提供的短信服务,下面我们以阿里云的短信服务为例,为大家介绍如何用 Python 程序发送短信。

使用阿里云短信服务之前,需要先完成下面几项准备工作:

  1. 注册阿里云账号,并在控制台的“短信服务”页面开通该服务;
  2. 申请短信签名:签名是短信开头的标识(如“【某某科技】”),需要在“国内消息”的“签名管理”中提交申请,审核通过后才能使用;
  3. 申请短信模板:模板是短信正文的样式,验证码类型的模板中可以使用${code}这样的变量占位符,需要在“模板管理”中提交申请并等待审核通过;
  4. 创建 AccessKey:调用阿里云 OpenAPI 的密钥,可以在控制台的“AccessKey 管理”页面创建,创建后会得到AccessKey ID和AccessKey Secret。

注意:AccessKey 就是你阿里云账号的“钥匙”,千万不要把密钥直接写进代码或提交到代码仓库,推荐的做法是将密钥保存到环境变量中(如ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET),程序运行时再读取。密钥一旦泄露,可能被人恶意调用短信接口,给你带来大量的费用损失。

阿里云为短信服务提供了官方的 Python SDK,安装方式如下所示。

Bash
pip install alibabacloud_dysmsapi20170525

提示:如果你使用uv管理依赖,等价的命令是uv add alibabacloud_dysmsapi20170525。

下面的代码演示了如何通过官方 SDK(V2 版本)向指定手机号发送短信验证码。

python
import os
import random

from alibabacloud_dysmsapi20170525.client import Client
from alibabacloud_dysmsapi20170525 import models as dysms_models
from alibabacloud_tea_openapi import models as open_api_models


def gen_mobile_code(length=6):
    """生成指定长度的手机验证码"""
    return ''.join(random.choices('0123456789', k=length))


def send_sms(tel, code):
    """发送短信(调用阿里云短信服务)"""
    # 创建配置对象,从环境变量中读取AccessKey,不要把密钥写进代码
    config = open_api_models.Config(
        access_key_id=os.environ['ALIBABA_CLOUD_ACCESS_KEY_ID'],
        access_key_secret=os.environ['ALIBABA_CLOUD_ACCESS_KEY_SECRET'],
        # 短信服务的访问域名
        endpoint='dysmsapi.aliyuncs.com'
    )
    client = Client(config)
    request = dysms_models.SendSmsRequest(
        # 接收短信的手机号码
        phone_numbers=tel,
        # 短信签名名称(需要提前申请并审核通过)
        sign_name='阿里云短信测试',
        # 短信模板CODE(需要提前申请并审核通过)
        template_code='SMS_154950909',
        # 模板中变量对应的值(JSON格式的字符串)
        template_param=f'{{"code": "{code}"}}'
    )
    # 调用SendSms接口发送短信
    resp = client.send_sms(request)
    # 响应的code字段等于'OK'才说明短信发送成功
    if resp.body.code == 'OK':
        print('短信发送成功')
    else:
        print(f'短信发送失败:{resp.body.code},{resp.body.message}')
    return resp.body


def main():
    code = gen_mobile_code()
    send_sms('13500112233', code)


if __name__ == '__main__':
    main()

上面的代码调用了send_sms方法(对应阿里云的SendSms接口),它会返回包含响应信息的对象,通过resp.body.code判断发送结果,只有它的值等于'OK'才说明短信已经发送成功;如果不等于'OK',可以结合resp.body.message以及错误码(如isv.BUSINESS_LIMIT_CONTROL表示触发发送频率限制、isv.SIGN_NAME_ILLEGAL表示签名不存在或状态不正确等)到阿里云的官方文档中排查具体原因。需要注意的是,短信的内容并不是在代码中拼出来的,而是取自审核通过的模板,代码只负责通过template_param给模板中的变量传值。

目前,大多数短信平台都会要求短信内容必须附上签名,阿里云也不例外,签名会出现在短信的开头;除了签名之外,发送的短信内容还必须对应一个审核通过的模板。另外,平台为了防范短信被盗用,通常还会对同一个手机号的发送频率做出限制,开发调试时注意不要频繁向同一个手机号发送短信。

当然国内的短信平台很多,读者可以根据自己的需要进行选择(通常会考虑费用预算、短信达到率、使用的难易程度等指标),如果需要在商业项目中使用短信服务建议购买短信平台提供的套餐服务。

总结 ​

其实,发送邮件和发送短信一样,也可以通过调用三方服务来完成,在实际的商业项目中,建议自己架设邮件服务器或购买三方服务来发送邮件,这个才是比较靠谱的选择。