☰
Python-100-Days 实战:用 Python 自动发送邮件与短信(smtplib + 螺丝帽短信网关)
2026/10/1 16:11:50 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】Python-100-Days

Python - 100天从新手到大师

项目地址:https://gitcode.com/GitHub_Trending/py/Python-100-Days
点击查看免费下载

本指南以 Python-100-Days 项目第 29 课为核心,系统讲解如何用 Python 标准库smtplib发送纯文本邮件和带附件邮件,并通过第三方短信平台(螺丝帽)的 HTTP 网关发送短信验证码。读完本文,你将掌握邮件服务器接入、MIME 邮件封装、授权码登录、短信网关调用、验证码生成与业务集成等完整能力,可直接用于报表推送、通知提醒、验证码登录等真实业务场景。

场景引入:让程序替你完成“发送”这件事

在前面课程中,我们已经学会用 Python 程序自动生成 Excel、Word、PDF 文档(见 Day21-30 的 21.文件读写和异常处理.md、24.Python读写Excel文件-1.md、26.Python操作Word和PowerPoint文件.md)。更进一步,我们完全可以通过电子邮件将生成好的文档发送给指定收件人,再用短信告知对方“邮件已发出”。这两类任务都可以用 Python 程序轻松自动化。

在即时通信软件如此发达的今天,电子邮件依然是互联网上使用最广泛的应用之一:公司向应聘者发送录用通知、网站向用户发送账号激活链接、银行向客户推送理财产品,几乎都是程序自动完成的。短信同样如此——注册码、验证码、营销信息基本都通过短信通道触达用户。本章即围绕这两个场景展开。

发送电子邮件:从 SMTP 协议到 smtplib 库

SMTP 协议与 smtplib 的关系

我们平时用 HTTP(超文本传输协议)访问网站资源,HTTP 是建立在 TCP(传输控制协议)之上的应用级协议,TCP 为众多应用级协议提供了可靠的数据传输服务。而发送电子邮件使用的是 SMTP(Simple Mail Transfer Protocol,简单邮件传输协议),它同样是建立在 TCP 之上的应用级协议,规定了邮件的发送者如何与邮件服务器通信的细节。

Python 通过标准库smtplib将这些底层操作封装为SMTP_SSL对象,我们只需要调用该对象的两个方法即可完成发信:

  • login(user, password):用邮箱账号和授权码登录邮件服务器;
  • sendmail(from_addr, to_addrs, msg):发送邮件,参数分别为发件人地址、收件人地址(可传列表表示多个收件人)和邮件内容字符串。

同时配合标准库email模块(email.mime.multipart.MIMEMultipart、email.mime.text.MIMEText、email.header.Header)来构造符合 MIME 规范的邮件主体,再通过email.as_string()将邮件对象序列化为字符串交给sendmail发送。

前提:开启邮件服务商的 SMTP 服务并获取授权码

发送邮件首先需要接入一台邮件服务器。自己架设邮件服务器对新手并不友好,更常见的做法是使用第三方邮箱服务。以网易 126 邮箱为例:注册账号并登录后,在“设置 → POP3/SMTP/IMAP”中开启 SMTP 服务,即可获得一台可用的邮件服务器。

开启服务时,页面会引导用户用手机发送一条指定内容的短信进行身份验证,验证通过后即可获得授权码。

授权码需要妥善保管,一旦泄露,其他人就能冒用你的身份发送邮件。在代码中,授权码承担的是邮箱“密码”的角色。

发送纯文本邮件:最小可运行示例

下面这段代码发送一封最简单的邮件:无附件、无图片、无其他超文本内容。

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() )

关键点解读:

  • MIMEMultipart()创建的是一个多部分(multipart)邮件容器,正文、附件等都以独立 MIME 部分的形式attach进去;
  • email['From']、email['To']、email['Subject']设置邮件头部;中文主题需要用Header(..., 'utf-8')包装,避免乱码;
  • MIMEText(content, 'plain', 'utf-8')表示添加纯文本正文,第三个参数是字符编码;
  • SMTP_SSL('smtp.126.com', 465)使用 SSL 加密连接邮件服务器,126 邮箱的 SMTP 服务器为smtp.126.com,SSL 端口为 465;
  • sendmail的第二个参数传列表即可实现一封邮件同时发给多个收件人。

发送带附件的邮件:BASE64 编码与中文文件名处理

如果要发送带附件的邮件,只需把附件内容处理成BASE64 编码,它与普通文本内容在邮件传输层面几乎没有区别。BASE64 是一种基于 64 个可打印字符(A-Z、a-z、0-9、+、/)来表示二进制数据的编码方法,每 3 个字节的二进制数据编码为 4 个可打印字符,常用于需要表示、传输、存储二进制数据的场合,电子邮件就是其中之一。Python 标准库中的base64模块提供了完整的编解码支持,MIMEText也内置了对base64子类型的处理。

下面的代码演示了如何发送一封带附件、且正文使用 HTML 排版的邮件:

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() )

这段代码的三个关键技巧:

  1. 附件编码:以二进制模式('rb')读取文件内容,用MIMEText(file.read(), 'base64', 'utf-8')将附件包装为 MIME 子类型base64的文本部分;
  2. 内容类型声明:通过attachment['content-type'] = 'application/octet-stream'告知客户端这是一个通用二进制文件;
  3. 中文文件名转码:urllib.parse.quote()将中文文件名转换为百分号编码(URL 编码),再通过attachment['content-disposition'] = 'attachment; filename="..."'指定收件人下载时保存的文件名,避免中文附件名乱码。

同时注意,正文类型从'plain'换成了'html',收件人客户端会按 HTML 渲染这段正文(<p>、<hr>等标签生效)。

封装成通用发信函数:一个参数搞定邮件推送

为了在实际项目中复用,可将上述逻辑封装为一个通用函数,使用时只需要调整邮件服务器域名、端口、用户名和授权码四个常量即可。该函数完整代码见 29.Python发送邮件和短信.md,核心实现如下:

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=[]): """发送邮件 :param from_user: 发件人 :param to_users: 收件人,多个收件人用英文分号进行分隔 :param subject: 邮件的主题 :param content: 邮件正文内容 :param filenames: 附件要发送的文件路径 """ 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())

从源码结构可以梳理出这个封装函数的设计要点:

  • 强制关键字参数:形参列表中的*要求所有参数必须以关键字形式传入,避免调用时位置参数顺序写错;
  • 多收件人支持:to_users用英文分号;分隔多个收件人,函数内部通过to_users.split(';')拆成列表交给sendmail,与“设置To头时用分号分隔”保持一致;
  • 附件批量处理:循环遍历filenames,每个附件都独立执行 BASE64 编码 +content-type/content-disposition设置,rfind('/')用于从完整路径中提取纯文件名(兼容C:/xxx这类带目录前缀的路径);
  • 中文附件名:每个显示文件名都用quote()转成百分号编码,保证收件端不乱码。

实战建议:filenames=[]作为默认参数属于“可变默认参数”,在 Python 中多次调用且传入不同附件列表时,默认值会被复用,可能带来状态残留。更稳妥的写法是用filenames=None,在函数体内再转为空列表。此外,调用结束后可补充smtp.quit()显式关闭连接,避免资源泄漏。

发送短信:接入螺丝帽短信网关

短信业务与三方平台的接入思路

发送短信同样是项目中的高频功能:网站注册码、登录验证码、营销信息基本都是通过短信触达用户的。发送短信需要第三方短信平台的支持,本课以螺丝帽平台为例演示接入方式。注册账号、购买短信服务的细节不做赘述(多数平台提供少量免费测试短信,可直接咨询平台客服)。

仓库中 Day46-60/57.接入三方平台.md 对这类平台的接入流程做了完整归纳,步骤如下:

  1. 在短信平台注册账号并登录管理后台,进入“短信”版块;
  2. 找到并记录自己的API Key(调用短信网关时的认证凭证);
  3. 配置短信签名——短信内容必须携带签名,例如“【Python小课】”或免费试用专用的“【铁壳测试】”,否则短信无法发送;
  4. 配置IP 白名单——把发起请求的服务器公网 IP 填入白名单(本地调试可查询本机公网 IP),防止短信接口被他人盗用;
  5. 确认剩余短信条数,不足时可在“充值”页面购买短信套餐。

调用短信网关发送验证码

螺丝帽平台提供 REST 风格的短信网关接口。我们可以通过requests库向短信网关发起一个 HTTP POST 请求,把手机号和短信内容作为表单参数提交。代码如下(完整实现见 29.Python发送邮件和短信.md):

import random import requests def send_message_by_luosimao(tel, message): """发送短信(调用螺丝帽短信网关)""" resp = requests.post( url='http://sms-api.luosimao.com/v1/send.json', auth=('api', 'key-注册成功后平台分配的KEY'), data={ 'mobile': tel, 'message': message }, timeout=10, verify=False ) return resp.json() def gen_mobile_code(length=6): """生成指定长度的手机验证码""" return ''.join(random.choices('0123456789', k=length)) def main(): code = gen_mobile_code() message = f'您的短信验证码是{code},打死也不能告诉别人哟!【Python小课】' print(send_message_by_luosimao('13500112233', message)) if __name__ == '__main__': main()

这段代码涉及几个容易忽略的细节:

  • requests.post的第一个参数是平台提供的短信网关地址http://sms-api.luosimao.com/v1/send.json;
  • auth=('api', 'key-...')是 HTTP Basic 认证:用户名为固定值api,密码为“注册成功后平台分配的 KEY”,即上面第 2 步查看到的 API Key(形如key-xxxxxxxx);
  • data中mobile是接收短信的手机号,message是短信内容,示例内容由“验证码 + 提示语 + 短信签名”拼接而成;
  • timeout=10设置请求超时时间为 10 秒,避免网络异常时程序长时间挂起;
  • verify=False表示不对服务器的 HTTPS 证书做校验(平台接口为 HTTP 时该参数无效),在商业环境中建议保留证书校验或改用平台的 HTTPS 地址;
  • gen_mobile_code()用random.choices('0123456789', k=length)从数字字符集中有放回地抽取length个字符拼接成验证码,默认长度 6 位。

响应解析与错误码排查

请求螺丝帽短信网关会返回 JSON 格式的数据:如果返回{'error': 0, 'msg': 'OK'},说明短信发送成功;如果error的值不是0,则说明发送失败,需要对照平台开发文档排查具体原因。仓库 Day46-60/57.接入三方平台.md 中记录了该平台常见错误码的含义,例如:

  • -20:余额不足(需要充值短信条数);
  • -32:缺少短信签名(短信内容未附带已配置的签名,或签名不在白名单内)。

说明:同一平台在不同时期的接口文档中,返回字段名可能略有差异(例如本课正文使用error字段,而仓库第 57 课中记录的是err字段)。实际开发时以平台官方当前返回为准,判断逻辑应兼容“成功码为 0”这一约定。

签名、模板与防刷:短信功能的工程化细节

大多数短信平台都会强制要求短信内容携带签名,签名需要在平台管理后台提前配置。下图是本课在螺丝帽平台配置的短信签名“【Python小课】”。

此外还有几点工程化注意:

  • 短信模板:涉及敏感内容的短信通常需要提前配置模板,模板中预留的占位符(如新闻标题、链接)在发送时动态填充。仓库公开课 第04次公开课-好玩的Python/好玩的Python.md 中有一个完整示例:爬取新闻后将“发现一条您可能感兴趣的新闻 - {标题},详情点击 {链接} 查看。【Python小课】”作为短信内容发送给用户,其中模板与签名都需先在平台配置;
  • IP 白名单:平台为了防范短信接口被盗用,会要求设置“IP 白名单”,只允许白名单内的服务器 IP 发起请求,不清楚配置方式可以咨询平台客服;
  • 频率限制与有效期:Day46-60/57.接入三方平台.md 中的 Django 项目展示了更完整的工程实践:通过 Redis 记录vote:block-mobile:{tel}键并设置 60 秒过期,阻止用户 60 秒内重复发送验证码;同时用vote2:valid-mobile:{tel}保存验证码、10 分钟(ex=600)后过期,即验证码有效期 10 分钟;再配合正则1[3-9]\d{9}对手机号做前置校验,非法的手机号直接返回错误提示而不消耗短信条数。
TEL_PATTERN = re.compile(r'1[3-9]\d{9}') def check_tel(tel): """检查手机号""" return TEL_PATTERN.fullmatch(tel) is not None def random_code(length=6): """生成随机短信验证码""" return ''.join(random.choices('0123456789', k=length))

短信平台的选择

国内的短信平台很多,读者可根据费用预算、短信到达率、使用难易程度等指标选择,商业项目中建议购买短信平台的套餐服务以保证额度与到达率。需要特别说明的是,短信内容的合规性由平台规则约束,违规短信无法发送。

结合项目:邮件与短信的异步化与部署选型

在真实项目中,发送邮件/短信通常不是同步阻塞在主流程里的,仓库中的商业项目实践可以给我们两点启发:

  • 异步化:Day91-100/95.使用Django开发商业项目.md 展示了通过 Celery 将send_email(from_user, to_user, cc_user, subject, content)封装为异步任务,用send_email.delay(...)投递到消息队列,避免发信耗时阻塞用户请求;
  • 部署选型:Day91-100/98.项目部署上线和性能调优.md 中列出了邮件服务的两种主流选型——自建邮件服务器(Postfix、Sendmail)与商业邮件服务(如 SendCloud),这印证了本课总结的观点。

总结

发送邮件与发送短信本质上都可以通过调用第三方服务来完成:

  • 邮件:使用标准库smtplib+email模块,对接任意支持 SMTP 的邮箱服务,关键动作是开启 SMTP 服务、获取授权码、构造 MIME 邮件、login后sendmail;带附件时用 BASE64 编码并处理中文文件名;
  • 短信:通过requests向短信平台网关发起 HTTP 请求,使用平台分配的 API Key 做认证,需配置好签名、模板与 IP 白名单,并按返回的 JSON 判断是否成功。

在商业项目中,建议自己架设邮件服务器或购买三方邮件服务来发送邮件(如 Day91-100/98.项目部署上线和性能调优.md 提到的 Postfix、Sendmail、SendCloud 等),并在业务代码中做好频率限制、验证码有效期、异常重试与异步化处理,这才是生产环境比较靠谱的选择。

  • 文档
  • 教程

【免费下载链接】Python-100-Days

Python - 100天从新手到大师

项目地址:https://gitcode.com/GitHub_Trending/py/Python-100-Days
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询