本文目录导读:

- 目录导读
- 问题现象与根源分析
- 常见错误代码示例
- 正确解决方案(一):使用Header对象编码
- 正确解决方案(二):设置MIMEBase的Content-Disposition
- 终极兼容方案:RFC 2231/5987 编码
- 完整可运行代码示例
- QA常见问题解答
- 实战技巧与避坑指南
Python脚本发送邮件附件中文名乱码?一文解决所有编码陷阱
目录导读
- 问题现象与根源分析
- 常见错误代码示例
- 正确解决方案(一):使用Header对象编码
- 正确解决方案(二):设置MIMEBase的Content-Disposition
- 终极兼容方案:RFC 2231/5987 编码
- 完整可运行代码示例
- QA常见问题解答
- 实战技巧与避坑指南
问题现象与根源分析
很多Python开发者在用smtplib和email.mime模块发送带中文名称的附件时,收件人看到的是类似=?UTF-8?B?5rWL6K+V5paH5qyW?=或者直接显示为乱码。
根本原因:邮件协议(MIME)要求附件文件名必须明确指定字符编码(尤其是非ASCII字符),否则邮件客户端无法正确解析,Python标准库email模块在默认情况下不会自动对附件名进行编码,需要开发者手动处理。
关键字:
email.header.HeaderContent-Disposition参数- base64 或 quoted-printable 编码
常见错误代码示例
先看一个“反面教材”,你很可能正在用这种写法:
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
msg = MIMEMultipart()
with open('报告.pdf','rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment', filename='中文报告.pdf')
msg.attach(part)
输出效果:
- 网易、QQ邮箱: 显示为乱码或乱码+文件名截断
- Gmail: 可能显示为压缩包名称乱码
原因分析:add_header 参数中的filename='中文报告.pdf'是原始字节(Python3中是str类型),没有经过编码转换,邮件传输时会被当成ASCII处理,导致乱码。
正确解决方案(一):使用Header对象编码
官方推荐用email.header.Header对文件名进行编码,再把编码后的字符串传给filename。
from email.header import Header
# 将文件名进行Base64编码
encoded_name = Header('中文报告.pdf', 'utf-8').encode()
part.add_header('Content-Disposition', 'attachment', filename=encoded_name)
关键点:
Header('原始字符串', '字符编码')会自动生成类似=?UTF-8?B?5Lit5paH5oKf5ZGK?=的编码字符串。- 此方案兼容Outlook、Thunderbird等主流客户端。
- 注意:Header默认使用base64编码,长度可控。
但有一个遗留问题:某些老旧邮件系统(如Exchange 2007)遇到这种编码的附件名可能显示异常。
正确解决方案(二):设置MIMEBase的Content-Disposition
除了用Header,也可以直接设置Content-Disposition字段的filename*参数(注意带星号),这种写法是RFC 5987定义的现代标准,效果更好。
from email.mime.base import MIMEBase
from email import encoders
with open('用户手册.pdf', 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
# 用 filename* 替代 filename
part.add_header('Content-Disposition', 'attachment',
filename*=('utf-8', '', '用户手册.pdf'))
msg.attach(part)
参数解释:
filename*=后必须是一个三元组:(charset, language, value)language可以留空字符串filename*=('utf-8', '', '用户手册.pdf')
此方案对Gmail、Apple Mail支持极佳,但Outlook(尤其是旧版)可能忽略filename*而优先使用filename,因此更稳妥的做法是同时提供两个版本。
终极兼容方案:RFC 2231/5987 编码
同时设置 filename 和 filename*,让客户端按优先级选择。
代码示例:
part.add_header('Content-Disposition', 'attachment',
filename=Header('中文报告.pdf', 'utf-8').encode(),
filename*=('utf-8', '', '中文报告.pdf'))
顺序:
- 现代客户端(如Gmail、Outlook 2013+)会识别
filename*并使用UTF-8。 - 老旧客户端优先选择简单的
filename,其值为Base64编码,也能正确解码。 - 如果只设置
filename*,部分手机邮件客户端可能显示空白。
完整可运行代码示例
以下是一个发送带中文附件邮件的完整脚本,融合了全部最佳实践:
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.header import Header
from email import encoders
def send_mail_with_attachment(sender, receiver, subject, body, filepath, filename):
# 创建邮件主体
msg = MIMEMultipart()
msg['From'] = sender
msg['To'] = receiver
msg['Subject'] = Header(subject, 'utf-8')
# 添加正文
msg.attach(MIMEText(body, 'plain', 'utf-8'))
# 添加附件(带中文名)
with open(filepath, '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(filename, 'utf-8').encode(),
filename*=('utf-8', '', filename))
msg.attach(part)
# 发送邮件
with smtplib.SMTP('smtp.qq.com', 587) as server:
server.starttls()
server.login('your@qq.com', '授权码')
server.send_message(msg)
# 调用示例
send_mail_with_attachment(
sender='your@qq.com',
receiver='friend@gmail.com',
subject='重要:中文报告',
body='请查收附件。',
filepath='./output.pdf',
filename='中文报告.pdf'
)
QA常见问题解答
*Q1:为什么我设置了`filename,对方还是看到乱码?** A:很可能是邮件服务器或客户端不支持RFC 5987,建议同时设置filename(用Header编码),如果对方用Outlook 2010以下版本,可能需要将文件名限制在ASCII范围内,或用Base64编码的filename`。
Q2:用Header编码后,文件名变成了很长的=?UTF-8?B?...?=,正常吗?
A:完全正常,这是MIME编码的标准形式,所有邮件客户端都会自动还原,如果不想看到这种长字符串,可以改为使用filename*方式。
Q3:发送多份附件时,文件名乱码是否独立?
A:是的,每个MIME对象都要单独编码,千万别在循环外只编码一次。
Q4:除了.pdf,.xls、.docx等也会乱码吗?
A:只要文件名包含非ASCII字符(中文、日文、重音符号等),就一定会乱码,与文件类型无关。
Q5:有没有更简单的第三方库?
A:yagmail库可以自动处理编码,但底层原理仍是上述方案,如果想完全掌控,坚持用标准库。
实战技巧与避坑指南
- 编码前先检查文件名:如果文件名本身是未知编码(例如从Windows系统传入的GBK),要用
filename.encode('utf-8').decode('utf-8')先统一为Unicode。 - 文件名长度:某些邮件系统对
filename*中的字符串长度有限制(比如255字节),超长内容可能被截断,如果文件名过长,建议截取或省略。 - 测试邮件客户端:至少用Gmail、Outlook Web、网易邮箱大师、Apple Mail验证一遍。
- 不要使用
part.set_param():add_header才是标准的添加头字段方法,set_param会被误用。 - 如果你的脚本部署在Linux服务器:确保服务器默认语言设置是UTF-8,可以在Python脚本开头强制设置:
import locale locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
Python发送带中文附件邮件时,务必对Content-Disposition中的文件名进行MIME编码,最推荐的做法是:*同时使用 Header 编码的 filename + RFC 5987 标准的 `filename`**,这样既能兼容老系统,也能完美支持现代客户端,记住这条黄金法则,你的邮件再也不会出现“%E4%B8%AD%E6%96%87”这样的乱码。
转发给被中文附件折磨的同事吧!