Python 邮件全攻略:用代码收发邮件
引言:让程序替你"发快递"
想象这些场景:
- 服务器半夜挂了,程序自动给运维发一封告警邮件;
- 用户注册后,系统自动发一封验证邮件;
- 每周一早上,脚本自动把上周的销售报表发到老板邮箱;
- 每天定时收取客户反馈邮箱里的新邮件,自动归档。
这些都不需要人工点"发送"——Python 可以完全接管收发邮件这件事。
发邮件用的是 SMTP 协议,收邮件用的是 POP3 协议(或更现代的 IMAP)。它们就像快递行业的两个环节:
- SMTP = 寄快递:你把包裹(邮件)交给快递点(SMTP 服务器),它负责送到对方手上;
- POP3 = 取快递:你去快递柜(POP3 服务器)把属于你的包裹取回来。
这一篇,我们从邮件协议的基本原理讲起,手把手教你用 Python 发送纯文本邮件、HTML 邮件、带附件邮件、带图片邮件,再讲如何用 POP3 收取和解析邮件,最后覆盖常见误区和完整实战案例。
一、先搞懂原理:邮件是怎么"旅行"的
1.1 一封邮件的旅程
当你用 Python 发一封邮件给 friend@qq.com,背后发生了什么?
你的 Python 程序
│
│ SMTP 协议(寄出)
▼
你的邮箱服务商(如 163 的 SMTP 服务器:smtp.163.com)
│
│ 服务器之间转发
▼
对方的邮箱服务商(如 QQ 邮箱的服务器)
│
│ 存入对方邮箱
▼
对方用 POP3/IMAP 协议取走邮件(或直接在网页/APP 上查看)关键点:
- 发邮件时,你的程序不是直接把邮件塞给对方,而是交给你自己的邮箱服务商代发;
- 所以你需要告诉程序:你的邮箱账号、密码(授权码)、SMTP 服务器地址;
- 收邮件则是主动去服务器"拉取"。
1.2 三个核心协议
| 协议 | 用途 | 默认端口 | 类比 |
|---|---|---|---|
| SMTP | 发送邮件 | 25 / 465(SSL) / 587 | 寄快递 |
| POP3 | 接收邮件(下载后可删除服务器副本) | 110 / 995(SSL) | 取快递(取走即清空) |
| IMAP | 接收邮件(服务器保留副本,多端同步) | 143 / 993(SSL) | 云端查看快递 |
POP3 和 IMAP 的区别:POP3 取走后服务器上通常就没了(像把信从信箱拿走),IMAP 则是在服务器上管理(手机、电脑看到的都是同一份)。本文重点讲 POP3。
1.3 准备工作:开通 SMTP 服务并获取授权码
这一步极其重要,90% 的新手都卡在这里!
现在的邮箱(163、QQ、Gmail 等)为了安全,不允许直接用登录密码让第三方程序发邮件,必须:
- 登录邮箱网页版,找到"设置 → POP3/SMTP 服务";
- 开启 SMTP 服务(通常需要手机验证);
- 获取一个授权码(一串随机字符,专门给第三方程序用)。
生活化理解:登录密码是你家的钥匙,不能随便给别人;授权码是"一次性门禁卡",专门发给快递柜使用,丢了可以随时作废,不影响大门钥匙。
常见邮箱的 SMTP 服务器地址:
| 邮箱 | SMTP 服务器 | SSL 端口 |
|---|---|---|
| QQ 邮箱 | smtp.qq.com | 465 |
| 163 邮箱 | smtp.163.com | 465 / 994 |
| Gmail | smtp.gmail.com | 465 |
| Outlook | smtp.office365.com | 587 |
二、发送第一封邮件:纯文本邮件
2.1 完整代码
import smtplib
from email.mime.text import MIMEText
from email.header import Header
# ========== 配置信息 ==========
sender = 'your_email@163.com' # 发件人(你的邮箱)
password = '你的授权码' # 注意:是授权码,不是登录密码!
receiver = 'friend@qq.com' # 收件人
smtp_server = 'smtp.163.com' # SMTP 服务器
# ========== 构造邮件内容 ==========
# 第一步:创建邮件正文
msg = MIMEText('你好,这是一封来自 Python 的邮件!', 'plain', 'utf-8')
# 第二步:设置邮件头(发件人、收件人、主题)
msg['From'] = Header('Python 助手 <your_email@163.com>', 'utf-8')
msg['To'] = Header('亲爱的朋友 <friend@qq.com>', 'utf-8')
msg['Subject'] = Header('我的第一封 Python 邮件', 'utf-8')
# ========== 发送邮件 ==========
# 第三步:连接 SMTP 服务器(SSL 加密,端口 465)
server = smtplib.SMTP_SSL(smtp_server, 465)
# 第四步:登录
server.login(sender, password)
# 第五步:发送
server.sendmail(sender, [receiver], msg.as_string())
# 第六步:关闭连接
server.quit()
print('邮件发送成功!')2.2 代码逐行解析
整个流程就像写信 + 寄信:
| 步骤 | 代码 | 类比 |
|---|---|---|
| 写信纸内容 | MIMEText(...) | 在信纸上写字 |
| 填信封 | msg['From']/['To']/['Subject'] | 信封上写收件人、寄件人 |
| 到快递点 | SMTP_SSL(server, 465) | 找到快递点 |
| 出示身份 | login(账号, 授权码) | 刷门禁卡 |
| 寄出 | sendmail(...) | 包裹交给快递员 |
| 离开 | quit() | 走人 |
2.3 为什么 From/To 要用 Header 包装?
# 直接写中文可能在某些客户端显示乱码
msg['From'] = 'Python 助手 <your_email@163.com>'
# 用 Header 包装后,中文会被正确编码
msg['From'] = Header('Python 助手 <your_email@163.com>', 'utf-8')Header 的作用:把中文按邮件标准(RFC 2047)编码成 ASCII,保证任何邮件客户端都能正确显示。
三、发送 HTML 邮件:让邮件"穿上花衣服"
3.1 纯文本 vs HTML
纯文本邮件像电报——只有干巴巴的文字。HTML 邮件可以排版:加粗、变色、插链接、放图片。
改动只有一处:把 MIMEText 的第二个参数从 'plain' 改成 'html':
import smtplib
from email.mime.text import MIMEText
from email.header import Header
html_content = '''
<html>
<body>
<h2 style="color: #e74c3c;">订单发货通知</h2>
<p>亲爱的用户:</p>
<p>您的订单 <b>#20260828</b> 已发货!</p>
<p><a href="https://example.com/track">点击查询物流</a></p>
<hr>
<p style="color: gray; font-size: 12px;">本邮件由系统自动发送,请勿回复</p>
</body>
</html>
'''
msg = MIMEText(html_content, 'html', 'utf-8') # ← 这里是 html
msg['From'] = Header('商城通知 <your_email@163.com>', 'utf-8')
msg['To'] = Header('用户 <friend@qq.com>', 'utf-8')
msg['Subject'] = Header('您的订单已发货', 'utf-8')
with smtplib.SMTP_SSL('smtp.163.com', 465) as server:
server.login('your_email@163.com', '你的授权码')
server.sendmail('your_email@163.com', ['friend@qq.com'], msg.as_string())
print('HTML 邮件发送成功!')小技巧:用
with语句管理 SMTP 连接,发送完毕自动断开,不用手动quit()。
四、发送带附件的邮件
4.1 MIMEMultipart:邮件的"集装箱"
一封带附件的邮件,结构是这样的:
邮件(MIMEMultipart 集装箱)
├── 正文部分(MIMEText)
└── 附件部分(MIMEBase)
├── 附件1:报表.xlsx
└── 附件2:照片.zip需要用 MIMEMultipart 把多个部分"拼"在一起:
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.header import Header
from email import encoders
# ========== 构造邮件 ==========
msg = MIMEMultipart() # 集装箱
msg['From'] = Header('报表机器人 <your_email@163.com>', 'utf-8')
msg['To'] = Header('老板 <boss@qq.com>', 'utf-8')
msg['Subject'] = Header('本周销售报表', 'utf-8')
# 第一部分:正文
msg.attach(MIMEText('老板您好,附件是本周销售报表,请查收。', 'plain', 'utf-8'))
# 第二部分:附件
with open('销售报表.xlsx', 'rb') as f:
# MIMEBase 表示"任意类型的二进制内容"
# 'application', 'octet-stream' 表示"通用二进制流"
attachment = MIMEBase('application', 'octet-stream')
attachment.set_payload(f.read())
# Base64 编码(邮件只能传文本,二进制必须编码)
encoders.encode_base64(attachment)
# 设置附件头信息,告诉客户端"这是附件,文件名是什么"
attachment.add_header(
'Content-Disposition',
'attachment',
filename=Header('销售报表.xlsx', 'utf-8').encode() # 中文文件名要编码
)
msg.attach(attachment)
# ========== 发送 ==========
with smtplib.SMTP_SSL('smtp.163.com', 465) as server:
server.login('your_email@163.com', '你的授权码')
server.sendmail('your_email@163.com', ['boss@qq.com'], msg.as_string())
print('带附件的邮件发送成功!')4.2 关键点解析
| 代码 | 作用 | 为什么需要 |
|---|---|---|
MIMEMultipart() | 创建多部分容器 | 一封邮件要装正文 + 多个附件 |
MIMEBase(...) | 包装二进制附件 | 附件是二进制,需要通用容器 |
encoders.encode_base64() | Base64 编码 | 邮件协议只支持文本,二进制必须转码 |
Content-Disposition: attachment | 声明这是附件 | 邮件客户端才知道显示"下载"按钮 |
4.3 发送多个附件
循环添加即可:
for filepath in ['报表.xlsx', '合同.pdf', '照片.zip']:
with open(filepath, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
import os
part.add_header('Content-Disposition', 'attachment',
filename=Header(os.path.basename(filepath), 'utf-8').encode())
msg.attach(part)五、在 HTML 正文中嵌入图片
5.1 两种插图方式
| 方式 | 原理 | 优缺点 |
|---|---|---|
| 外链图片 | HTML 里写 <img src="http://..."> | 简单,但很多客户端默认屏蔽外链图 |
| 嵌入图片 | 图片作为附件,正文用 cid: 引用 | 可靠,图片随邮件走 |
嵌入图片是更专业的做法,结构如下:
邮件(MIMEMultipart)
├── 正文(HTML 里有 <img src="cid:img1">)
└── 图片附件(Content-ID 设为 <img1>)5.2 完整代码
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.header import Header
msg = MIMEMultipart('related') # related 类型支持正文引用附件
msg['From'] = Header('活动通知 <your_email@163.com>', 'utf-8')
msg['To'] = Header('用户 <friend@qq.com>', 'utf-8')
msg['Subject'] = Header('活动海报请查收', 'utf-8')
# HTML 正文,用 cid:poster 引用图片
html = '''
<html><body>
<h2>周年庆活动开始啦!</h2>
<p><img src="cid:poster"></p>
<p>扫码即可参与抽奖</p>
</body></html>
'''
msg.attach(MIMEText(html, 'html', 'utf-8'))
# 嵌入图片
with open('poster.jpg', 'rb') as f:
img = MIMEImage(f.read())
img.add_header('Content-ID', '<poster>') # 与 cid:poster 对应
msg.attach(img)
with smtplib.SMTP_SSL('smtp.163.com', 465) as server:
server.login('your_email@163.com', '你的授权码')
server.sendmail('your_email@163.com', ['friend@qq.com'], msg.as_string())
print('带图片的邮件发送成功!')关键对应关系:HTML 里的 cid:poster ↔ 附件头的 Content-ID: <poster>(注意尖括号)。
六、同时发给多人与抄送/密送
msg['To'] = 'a@qq.com, b@qq.com' # 收件人(互相可见)
msg['Cc'] = 'c@qq.com' # 抄送(所有人可见)
# 密送(Bcc)不需要写在邮件头里,只需传进 sendmail 列表
all_receivers = ['a@qq.com', 'b@qq.com', 'c@qq.com', 'secret@qq.com']
server.sendmail(sender, all_receivers, msg.as_string())生活化理解:
- To(收件人):信是写给你的;
- Cc(抄送):让你知悉,你能看到所有收件人;
- Bcc(密送):偷偷抄送一份,其他人都不知道你也收到了——所以 Bcc 不写在邮件头,只在"寄信"时告诉快递点。
七、收取邮件:POP3 协议实战
7.1 收邮件的两步曲
用 POP3 收邮件分两大步:
poplib:连接服务器,把邮件原始数据下载下来(一堆乱码似的文本);email模块:把原始文本解析成结构化的对象,方便读取主题、发件人、正文、附件。
生活化理解:poplib 是快递员,负责把包裹从快递柜搬回家;email 模块是开箱工具,负责拆包、分类、看懂里面的东西。
7.2 第一步:用 poplib 下载邮件
import poplib
# 连接 POP3 服务器(SSL 加密,端口 995)
server = poplib.POP3_SSL('pop.163.com', 995)
# 登录(同样用授权码)
server.user('your_email@163.com')
server.pass_('你的授权码')
# 查看邮箱状态:邮件数量和总大小
count, size = server.stat()
print(f'共有 {count} 封邮件,总大小 {size / 1024:.1f} KB')
# 列出所有邮件(编号 + 大小)
resp, mails, octets = server.list()
print(mails) # [b'1 1234', b'2 5678', ...] — 编号 大小
# 下载最新一封邮件(编号从 1 开始,最新的编号最大)
resp, lines, octets = server.retr(count)
# lines 是邮件的原始内容(按行分割的字节串列表)
raw_content = b'\r\n'.join(lines)
# 关闭连接
server.quit()
print(f'已下载最新邮件,大小 {len(raw_content)} 字节')7.3 第二步:用 email 模块解析邮件
原始邮件内容长这样(完全没法直接用):
Received: from m12-15.163.com (unknown [220.181.12.15])
by mx31.qq.com (NewMX) with SMTP id ...
Subject: =?UTF-8?B?5ZGo6ZSA5Y+K5Y+R?=
Content-Type: text/plain; charset="utf-8"
...需要用 email 模块"翻译":
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
# 解析原始字节为 Message 对象
msg = Parser().parsestr(raw_content.decode('utf-8', errors='replace'))
def decode_str(s):
"""解码邮件头中的乱码字符串(如 =?UTF-8?B?...?=)"""
value, charset = decode_header(s)[0]
if isinstance(value, bytes):
value = value.decode(charset or 'utf-8', errors='replace')
return value
# 读取发件人
from_name, from_addr = parseaddr(msg['From'])
print(f'发件人: {decode_str(from_name)} <{from_addr}>')
# 读取主题
print(f'主题: {decode_str(msg["Subject"])}')
# 读取日期
print(f'日期: {msg["Date"]}')decode_header 是干什么的? 邮件标准要求头部只能是 ASCII,中文主题会被编码成 =?UTF-8?B?5ZGo6ZSA...?= 这样的乱码,decode_header 负责还原。
7.4 提取正文和附件
邮件正文可能是多层的(纯文本 + HTML + 附件),需要递归遍历:
from email.parser import BytesParser
from email import policy
def get_content(msg):
"""递归提取邮件的正文和附件"""
if msg.is_multipart():
# 多部分:递归处理每一部分
parts = msg.get_payload()
for part in parts:
get_content(part)
else:
content_type = msg.get_content_type()
filename = msg.get_filename()
if filename:
# 这是附件:保存到本地
filename = decode_str(filename)
data = part.get_payload(decode=True) if False else msg.get_payload(decode=True)
with open(filename, 'wb') as f:
f.write(data)
print(f'已保存附件: {filename}')
elif content_type == 'text/plain':
# 纯文本正文
content = msg.get_payload(decode=True)
charset = msg.get_content_charset() or 'utf-8'
print(f'[纯文本正文]\n{content.decode(charset, errors="replace")}')
elif content_type == 'text/html':
# HTML 正文
content = msg.get_payload(decode=True)
charset = msg.get_content_charset() or 'utf-8'
print(f'[HTML 正文]\n{content.decode(charset, errors="replace")[:500]}...')
# 解析并提取
msg = BytesParser(policy=policy.default).parsebytes(raw_content)
get_content(msg)7.5 完整收邮件流程(整合版)
import poplib
from email.parser import BytesParser
from email.header import decode_header
from email.utils import parseaddr
from email import policy
def decode_str(s):
if not s:
return ''
value, charset = decode_header(s)[0]
if isinstance(value, bytes):
value = value.decode(charset or 'utf-8', errors='replace')
return value
def show_mail(msg, indent=0):
prefix = ' ' * indent
if msg.is_multipart():
for part in msg.iter_parts():
show_mail(part, indent + 2)
else:
content_type = msg.get_content_type()
if content_type in ('text/plain', 'text/html'):
content = msg.get_content()
print(f'{prefix}--- {content_type} ---')
print(f'{prefix}{content[:300]}')
# 连接并下载
server = poplib.POP3_SSL('pop.163.com', 995)
server.user('your_email@163.com')
server.pass_('你的授权码')
count, _ = server.stat()
print(f'邮箱共 {count} 封邮件\n')
# 读取最新的 3 封
for i in range(count, max(count - 3, 0), -1):
resp, lines, octets = server.retr(i)
raw = b'\r\n'.join(lines)
msg = BytesParser(policy=policy.default).parsebytes(raw)
name, addr = parseaddr(msg['From'])
print(f'===== 第 {i} 封 =====')
print(f'主题: {decode_str(msg["Subject"])}')
print(f'发件人: {decode_str(name)} <{addr}>')
show_mail(msg)
print()
server.quit()八、常见误区解析
误区 1:用登录密码而不是授权码
现象:报 smtplib.SMTPAuthenticationError: (535, b'Error: authentication failed')。
真相:几乎所有主流邮箱都禁止第三方程序用登录密码。必须去邮箱设置里开启 SMTP 服务并获取授权码。QQ 邮箱、163 邮箱尤其严格。
误区 2:端口和加密方式不匹配
| 组合 | 结果 |
|---|---|
SMTP_SSL(server, 465) | ✅ SSL 端口配 SSL 类 |
SMTP_SSL(server, 25) | ❌ 端口错,连不上或报错 |
SMTP(server, 587) + starttls() | ✅ 587 是 STARTTLS 端口 |
真相:465 端口用 SMTP_SSL;587 端口用 SMTP + server.starttls()。搞混就连不上。
# 587 端口的正确写法
server = smtplib.SMTP('smtp.office365.com', 587)
server.starttls() # 升级为加密连接
server.login(user, password)误区 3:收件人参数传字符串
# ❌ 错误:第二个参数必须是列表
server.sendmail(sender, 'friend@qq.com', msg.as_string())
# 结果:'f', 'r', 'i'...每个字符都被当成一个收件人!
# ✅ 正确:即使只有一个收件人,也要用列表
server.sendmail(sender, ['friend@qq.com'], msg.as_string())误区 4:以为发出去了对方就一定能收到
真相:sendmail 成功只表示"你的邮箱服务器收下了",不代表对方一定收到。可能被判为垃圾邮件、被对方服务器拒收。降低进垃圾箱概率的方法:
- 标题避免"免费""中奖""发票"等敏感词;
- 不要用刚刚注册的新邮箱大量群发;
- 一封邮件不要塞过多收件人(分批发送)。
误区 5:POP3 收完邮件,网页邮箱里还有
真相:这取决于邮箱设置。POP3 默认行为是"下载后删除服务器副本",但现在大多数邮箱默认改为"保留副本"。另外 dele() 方法可以主动删除:
server.dele(1) # 标记删除第 1 封,quit() 时生效需要多端同步的场景,应该用 IMAP(Python 内置 imaplib),而不是 POP3。
误区 6:脚本里明文写授权码
真相:授权码等于密码,写死在代码里并提交到 Git = 公开你的邮箱。正确做法:
import os
password = os.environ.get('EMAIL_PASSWORD') # 从环境变量读取九、综合实战:服务器监控告警系统
把前面所有知识点串起来——当磁盘快满时,自动发送带报表附件的告警邮件:
import os
import smtplib
import psutil
from datetime import datetime
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.header import Header
from email import encoders
SENDER = 'monitor@163.com'
PASSWORD = os.environ.get('EMAIL_PASSWORD') # 授权码放环境变量
RECEIVERS = ['admin@qq.com']
SMTP_SERVER = 'smtp.163.com'
def send_alert(subject, html_body, attachment_path=None):
"""通用告警邮件发送函数"""
msg = MIMEMultipart()
msg['From'] = Header(f'监控告警 <{SENDER}>', 'utf-8')
msg['To'] = ','.join(RECEIVERS)
msg['Subject'] = Header(subject, 'utf-8')
msg.attach(MIMEText(html_body, 'html', 'utf-8'))
if attachment_path:
with open(attachment_path, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment',
filename=Header(os.path.basename(attachment_path), 'utf-8').encode())
msg.attach(part)
with smtplib.SMTP_SSL(SMTP_SERVER, 465) as server:
server.login(SENDER, PASSWORD)
server.sendmail(SENDER, RECEIVERS, msg.as_string())
print(f'[{datetime.now()}] 告警邮件已发送')
def check_disk():
"""检查磁盘,超标则告警"""
usage = psutil.disk_usage('C:\\')
if usage.percent > 85:
html = f'''
<html><body>
<h2 style="color:red;">磁盘空间告警</h2>
<p>服务器磁盘使用率已达 <b>{usage.percent}%</b></p>
<p>已用: {usage.used / 1024**3:.1f} GB / 总计: {usage.total / 1024**3:.1f} GB</p>
<p>请及时清理!</p>
</body></html>
'''
send_alert(f'【告警】磁盘使用率 {usage.percent}%', html)
if __name__ == '__main__':
check_disk()十、知识点速查表
10.1 发送邮件(SMTP)
| 步骤 | 模块/类 | 作用 |
|---|---|---|
| 构造纯文本正文 | MIMEText(text, 'plain', 'utf-8') | 信纸 |
| 构造 HTML 正文 | MIMEText(html, 'html', 'utf-8') | 花信纸 |
| 构造多部分邮件 | MIMEMultipart() | 集装箱(正文+附件) |
| 构造附件 | MIMEBase + encoders.encode_base64 | 打包包裹 |
| 嵌入图片 | MIMEImage + Content-ID + cid: | 信里插图 |
| 头部中文 | Header(text, 'utf-8') | 防止乱码 |
| 连接服务器 | smtplib.SMTP_SSL(host, 465) | 到快递点 |
| 登录 | login(user, 授权码) | 刷门禁 |
| 发送 | sendmail(from, [to列表], msg.as_string()) | 寄出 |
10.2 接收邮件(POP3)
| 步骤 | 方法 | 作用 |
|---|---|---|
| 连接 | poplib.POP3_SSL(host, 995) | 到快递柜 |
| 登录 | user() / pass_() | 验证身份 |
| 查状态 | stat() | 有几封信 |
| 下载 | retr(编号) | 取信(编号从 1 开始) |
| 删除 | dele(编号) | 取完删副本 |
| 解析 | BytesParser(policy=policy.default) | 拆信 |
| 解乱码 | decode_header() | 翻译中文头部 |
10.3 常用服务器地址
| 邮箱 | SMTP (SSL) | POP3 (SSL) |
|---|---|---|
| smtp.qq.com:465 | pop.qq.com:995 | |
| 163 | smtp.163.com:465 | pop.163.com:995 |
| Gmail | smtp.gmail.com:465 | pop.gmail.com:995 |
| Outlook | smtp.office365.com:587 | outlook.office365.com:995 |
十一、小结
| 核心知识点 | 一句话总结 |
|---|---|
| SMTP | 发邮件的协议,Python 用 smtplib |
| POP3 | 收邮件的协议(取走式),Python 用 poplib |
| 授权码 | 第三方程序的"门禁卡",不是登录密码 |
| MIMEText | 构造正文,plain 纯文本 / html 富文本 |
| MIMEMultipart | 多部分容器,正文和附件都往里装 |
| Base64 附件 | 邮件只传文本,二进制附件必须编码 |
| 邮件解析 | poplib 下载 + email 模块解析,两步走 |
| 端口配对 | 465 配 SMTP_SSL,587 配 SMTP + starttls() |
记住三句话:
- 发邮件用 SMTP(465 端口 + SSL),收邮件用 POP3(995 端口 + SSL),登录一律用授权码;
- 简单正文用
MIMEText,带附件上MIMEMultipart,收件人永远传列表; - 授权码不要写死在代码里,放环境变量,别提交到 Git。
参考说明
本文内容基于廖雪峰 Python 教程的"电子邮件"章节(SMTP 发送邮件、POP3 收取邮件)重新整理和扩充,示例代码适用于 Python 3.8+。