大家平时在邮箱网页里填写收件人、主题和正文,然后点击发送。今天我们用 Python 完成相同的事情。网页里的“发送”按钮背后,也需要把邮件交给邮件服务器。Python 程序相当于我们自己编写的一个简易邮件客户端。
| 邮件中的概念 | 寄信类比 | 示例 |
|---|---|---|
| 邮件正文 | 信纸上的内容 | “你好,这是课堂实验邮件” |
| 邮件头 | 信件上的信息 | 发件人、收件人、主题 |
| 邮件地址 | 寄件人或收件人地址 | 你的账号@163.com |
| SMTP 服务器 | 接收寄件的邮局 | smtp.163.com |
| 端口 | 邮局办理某项业务的窗口 | 465 |
| 身份认证 | 证明自己有权使用这个寄件账号 | 邮箱地址和授权码 |
| SMTP | 寄送邮件时共同遵守的规则 | 简单邮件传输协议 |
Python 程序
│
│ 登录并提交邮件
▼
163 的 SMTP 服务器
│
│ 转交邮件
▼
收件方邮件服务器,例如 QQ 邮箱服务器
│
▼
收件人通过网页或客户端查看邮件
问:使用 163 邮箱给 QQ 邮箱发送邮件,Python 应该连接 163 还是 QQ 的 SMTP 服务器?
答:连接发件方的 163 SMTP 服务器。收件人的邮箱可以是 QQ、Outlook 或其他邮箱,跨邮箱投递由邮件服务器处理。
注:
- SMTP 负责发送邮件。
- POP3、IMAP 主要用于读取或同步邮箱邮件。
- 本节课实现发送功能,不需要编写收信程序。
使用python发送邮件,程序只需要做两件事:1:把邮件写好。2:把邮件交给服务器发送。
python发送邮件需要的库
| 工具 | 负责什么 |
|---|---|
email包 | 构造邮件内容和邮件头 |
smtplib模块 | 连接 SMTP 服务器、登录、发送 |
ssl模块 | 配置加密连接 |
getpass模块 | 在终端输入授权码时隐藏输入内容 |
这些都是python标准库,不必执行pip install 。本节课使用MIMEText。Python 也提供了较新的EmailMessage接口,后续课程介绍。
第一步:现在代码中生成一份邮件,不用连接服务器,也不用发送。
from email.mime.text import MIMEText from email.header import Header from email.utils import formataddr sender = "your_account@163.com" receiver = "receiver@example.com" message = MIMEText( "你好,这是我使用 Python 编写的第一封邮件。", "plain", "utf-8" ) message["From"] = formataddr(("Python课堂", sender)) message["To"] = formataddr(("收件同学", receiver)) message["Subject"] = Header("Python邮件发送实验", "utf-8") print(message.as_string())代码介绍:
1.MIMEText():构造文本邮件
常用形式:MIMEText(_text, _subtype="plain", _charset=None)
| 参数 | 含义 | 本课填写 |
|---|---|---|
_text | 正文内容,通常是字符串 | "你好……" |
_subtype | 文本类型 | "plain" |
_charset | 文本采用的字符编码 | "utf-8" |
message = MIMEText("你好", "plain", "utf-8")创建一份邮件正文,内容是“你好”,按纯文本处理,采用 UTF-8 编码。
邮件有两个常见类型
# 纯文本 MIMEText("你好,同学!", "plain", "utf-8") # HTML MIMEText("<h1>你好,同学!</h1>", "html", "utf-8")plain会把正文当普通文字;html允许邮件客户端按 HTML 展示。
函数返回的是一个邮件对象,因此message并不是普通字符串。参数名开头的下划线属于库的命名。
2.message["From"]等:填写邮件头
message["From"] = ... message["To"] = ... message["Subject"] = ...| 字段 | 含义 |
|---|---|
From | 邮件中显示的发件人 |
To | 邮件中显示的收件人 |
Subject | 邮件主题 |
message像一张已经准备好的邮件表单,我们现在填写发件人、收件人和主题。这是一种类似字典的操作方式,但message本身是邮件对象。
3.formataddr():把昵称与邮箱地址组合起来
常用形式:
formataddr(pair, charset="utf-8")| 参数 | 含义 |
|---|---|
pair | 一个二元组,依次放显示名称和邮箱地址 |
charset | 显示名称使用的编码,默认 UTF-8 |
formataddr(("张老师", "your_account@163.com")) # 这里有两层括号: formataddr( ("张老师", "your_account@163.com") ) # └──────── 一个元组参数 ──────────┘可以理解为:
昵称:张老师 地址:your_account@163.com组合成符合邮件头格式的字符串。中文昵称可能被编码,邮箱客户端会负责解码显示。
4.Header():处理中文主题
Header(s, charset)| 参数 | 含义 | 示例 |
|---|---|---|
s | 邮件头中的文字 | "Python邮件发送实验" |
charset | 文字编码 | "utf-8" |
例如:
message["Subject"] = Header("Python邮件发送实验", "utf-8")正文有正文的编码,主题也需要正确处理中文。Header帮我们构造邮件头中的文字。
5.as_string():把邮件对象转换成可传输的文本
mail_text = message.as_string()邮件对象---- as_string()------>包含邮件头、正文及编码信息的完整邮件字符串
观察print(message.as_string())的结果。看到编码后的字符时,这不一定是乱码,而可能是邮件传输格式;邮箱客户端会解码展示。
这个方法只负责转换格式,不会发送邮件。
二.连接、登录和发送
执行顺序:建立连接 → 验证身份 → 提交邮件 → 关闭连接
1.SMTP_SSL():建立加密连接
server = smtplib.SMTP_SSL( host="smtp.163.com", port=465, timeout=20, context=ssl.create_default_context() )| 参数 | 含义 | 本课取值 |
|---|---|---|
host | 发件服务器地址 | "smtp.163.com" |
port | 服务器端口,使用整数 | 465 |
timeout | 阻塞网络操作的超时时间,单位秒 | 20 |
context | TLS 配置对象 | ssl.create_default_context() |
local_hostname | 向服务器介绍本机时使用的名称 | 省略 |
source_address | 指定本机连接使用的地址和端口 | 省略 |
timeout=20不是“20 秒后发送”,也不是整个程序必须在 20 秒内结束。Python 等待服务器响应的时候,如果连续 20 秒没有收到任何数据,就直接抛出超时异常,不再死等。
服务器地址不要写成:"https://smtp.163.com" 。这里不需要网页协议前缀
2.ssl.create_default_context():创建加密配置
返回一个 TLS 配置对象,用于采用合理的默认设置并验证服务器证书。
这个对象告诉 Python,建立连接时怎样加密,以及怎样确认连接的是可信服务器。
3.login():验证发件人身份
server.login(user, password)| 参数 | 本课填写 |
|---|---|
user | 完整的 163 邮箱地址 |
password | 163 邮箱生成的客户端授权码 |
4.sendmail():提交邮件
server.sendmail( from_addr, to_addrs, msg, mail_options=(), rcpt_options=() )| 参数 | 含义 | 示例 |
|---|---|---|
from_addr | 实际投递使用的发件地址 | sender |
to_addrs | 实际收件地址,课堂统一用列表 | [receiver] |
msg | 完整邮件字符串或字节串 | message.as_string() |
mail_options | 扩展发送选项 | 省略 |
rcpt_options | 扩展收件选项 | 省略 |
示例:
server.sendmail(sender, [receiver], message.as_string())receiver # 一个字符串
[receiver] # 包含一个字符串的列表
该方法返回“被拒绝收件人”的字典。返回{}表示本次提交的收件地址全部被服务器接受;全部收件人被拒绝时会抛出异常。
5.quit():结束连接
server.quit()无需传参数,用于结束 SMTP 会话。
最终示例使用:
with smtplib.SMTP_SSL(...) as server: ...退出with代码块时会自动执行退出和连接清理,因此不必再手动调用quit()。
重点区分两点:邮件头与投递地址
# 这是邮件里显示的收件人信息。 message["To"] = formataddr(("收件同学", receiver)) # 这里的 [receiver] 才是本次实际投递的收件地址。 server.sendmail(sender, [receiver], message.as_string())课堂实验保持两者一致。可以用“信纸上写的收件人”和“投递时使用的信封地址”帮助区分。
三.实验前准备:开启 163 的 SMTP 服务
准备:
- 一个能正常登录的普通 163.com 邮箱。
- 一个真实的收件邮箱,例如自己的 QQ 邮箱。
- Python 3 和可联网的电脑。
在 163 邮箱网页版中,从“设置”进入“POP3/SMTP/IMAP”的页面,开启包含 SMTP 的服务,并按照页面要求完成身份验证、生成客户端授权码。
配置:
| 项目 | 填写内容 |
|---|---|
| 发件服务器 | smtp.163.com |
| 端口 | 465 |
| 连接方式 | SSL/TLS |
| 登录账号 | 完整的 163 邮箱地址 |
| 登录凭证 | 客户端授权码 |
强调三点:
- 授权码由发件人的 163 邮箱生成。
- 收件人不需要提供密码或授权码,也不必为了收这封邮件开启 SMTP。
- 授权码不要写进课件、作业截图或提交的源码中。授权码不要让别人知道。
四、最终实验:163 邮箱给其他邮箱发送邮件
import smtplib import ssl from getpass import getpass from email.mime.text import MIMEText from email.header import Header from email.utils import formataddr # 1. 获取发件人、收件人和授权码 sender = input("请输入发件人的163邮箱:").strip() receiver = input("请输入收件邮箱,例如你的QQ邮箱:").strip() auth_code = getpass("请输入163邮箱客户端授权码:").strip() # 2. 编写正文 body = """你好! 这是我在 Python 课堂上发送的一封实验邮件。 本次实验中,我学习了: 1. 构造邮件正文和邮件头。 2. 连接163邮箱的SMTP服务器。 3. 使用授权码登录并发送邮件。 祝学习顺利! """ # 3. 构造邮件对象 message = MIMEText(body, "plain", "utf-8") message["From"] = formataddr(("Python课堂", sender)) message["To"] = formataddr(("收件同学", receiver)) message["Subject"] = Header("Python课堂邮件实验", "utf-8") # 4. 创建TLS配置 context = ssl.create_default_context() # 5. 连接、登录、发送 try: with smtplib.SMTP_SSL( host="smtp.163.com", port=465, timeout=20, context=context ) as server: server.login(sender, auth_code) refused = server.sendmail( sender, [receiver], message.as_string() ) if not refused: print("163服务器已接受邮件,请到收件邮箱检查。") else: print("以下收件地址被拒绝:", refused) except smtplib.SMTPAuthenticationError as error: print("登录失败:请检查163邮箱、授权码及SMTP服务状态。") print("详细信息:", error) except smtplib.SMTPRecipientsRefused as error: print("收件地址被拒绝:请检查收件邮箱是否填写正确。") print("详细信息:", error) except smtplib.SMTPException as error: print("SMTP通信发生异常:", error) except OSError as error: print("网络连接或TLS连接发生异常:", error)getpass()在普通终端中输入时通常不会显示字符或星号,这是正常现象。输入完毕按回车;建议在终端运行,部分 IDE 输出窗口不支持隐藏输入。
| 写法 | 解释 |
|---|---|
input("提示文字") | 显示提示,读取一行输入,返回字符串 |
.strip() | 本课不传参数,去掉输入首尾的空白 |
getpass("提示文字") | 提示输入授权码,通常隐藏输入,返回字符串 |
"""多行文字""" | 定义跨越多行的字符串 |
print(...) | 把一个或多个值显示到终端 |
try | 尝试执行可能失败的代码 |
except ... as error | 捕获指定类型的异常,用error保存异常信息 |
with ... as server | 创建并管理连接,将连接对象命名为server |
if not refused | 当返回的字典为空时进入该分支 |
这里的try、except、with、if是语法关键字,不是函数,不需要寻找它们的“函数参数”。
实验结果要检查两层:
- 程序层面:163 服务器是否接受邮件。
- 收件层面:收件邮箱是否实际收到,必要时查看垃圾邮件文件夹。
服务器接受邮件,不等于保证立即进入对方收件箱。如果程序已经显示“已接受”,随后退出连接时报错,也应先检查收件情况,避免重复发送。
import smtplib import ssl from getpass import getpass from email.mime.text import MIMEText from email.header import Header from email.utils import formataddr # 1. 获取发件人、收件人和授权码 sender = "发送邮箱" receiver = ["接收邮箱"] auth_code = "授权码" # 2. 编写正文 body = """你好! 这是我在 Python 课堂上发送的一封实验邮件。 本次实验中,我学习了: 1. 构造邮件正文和邮件头。 2. 连接163邮箱的SMTP服务器。 3. 使用授权码登录并发送邮件。 祝学习顺利! """ # 3. 构造邮件对象 message = MIMEText(body, "plain", "utf-8") message["From"] = formataddr(("Python课堂", sender)) message["To"] = formataddr(("收件同学", receiver[0])) message["Subject"] = Header("Python课堂邮件实验", "utf-8") # 4. 创建TLS配置 context = ssl.create_default_context() # 5. 连接、登录、发送 try: with smtplib.SMTP_SSL( host="smtp.163.com", port=465, timeout=20, context=context ) as server: server.login(sender, auth_code) refused = server.sendmail( sender, receiver, message.as_string() ) if not refused: print("163服务器已接受邮件,请到收件邮箱检查。") else: print("以下收件地址被拒绝:", refused) except smtplib.SMTPAuthenticationError as error: print("登录失败:请检查163邮箱、授权码及SMTP服务状态。") print("详细信息:", error) except smtplib.SMTPRecipientsRefused as error: print("收件地址被拒绝:请检查收件邮箱是否填写正确。") print("详细信息:", error) except smtplib.SMTPException as error: print("SMTP通信发生异常:", error) except OSError as error: print("网络连接或TLS连接发生异常:", error)