因为smtplib只传输原始字节流,不处理mime结构,手动拼接易破坏boundary导致附件丢失或html被当纯文本;必须用email模块的mimemultipart等类构建符合rfc 2822的邮件体,正确设置层级、编码、content-disposition头及smtp配置。

为什么直接用 smtplib.sendmail() 会丢失附件或HTML渲染?
因为 smtplib 本身不处理邮件结构,它只负责把原始字节流发出去。如果你手动拼接 HTML 内容和附件二进制数据,很容易破坏 MIME 边界(boundary),导致收件端无法识别附件或把 HTML 当纯文本显示。
真正该用的是 email 标准库里的 MIMEMultipart 和 MIMEText、MIMEBase 等类来构造符合 RFC 2822 的多部分邮件体。
- 必须用
MIMEMultipart("mixed")作为根容器,再嵌套MIMEMultipart("alternative")放 HTML/plain 文本 - 附件要用
MIMEApplication或MIMEBase包装,并调用set_payload()+encoders.encode_base64() -
Content-Disposition: attachment头不能漏,否则 Outlook 等客户端可能内联显示而非下载
怎么正确添加 HTML 正文和多个附件?
关键在于层级:外层 mixed → 中层 alternative(放 text/html + text/plain)→ 外层同级追加附件节点。顺序错了,附件就变成正文的一部分。
示例片段(注意变量名和路径):
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
import smtplib
<p>msg = MIMEMultipart("mixed")
msg["Subject"] = "测试带附件的HTML邮件"
msg["From"] = "sender@example.com"
msg["To"] = "receiver@example.com"</p><h1>构造HTML正文部分</h1><p>html_part = MIMEMultipart("alternative")
html_body = MIMEText("</p><h2>你好</h2><p>这是 <strong>HTML</strong> 邮件</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill7351" title="testing-python"><img
src="https://img.php.cn/upload/skill/000/000/081/179143938488980.jpg" alt="testing-python" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill7351" title="testing-python" class="overflowclass">testing-python</a>
<p class="overflowclass">使用pytest编写和评估有效的Python测试。适用于编写测试、审查测试代码、调试测试失败或提高测试覆盖率。</p>
</div>
<a rel="nofollow" href="/xiazai/skill7351" title="testing-python" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>", "html", "utf-8")
plain_body = MIMEText("这是纯文本备用内容", "plain", "utf-8")
html_part.attach(plain_body)
html_part.attach(html_body)
msg.attach(html_part)<h1>添加附件(假设文件在当前目录)</h1><p>for file_path in ["report.pdf", "data.xlsx"]:
with open(file_path, "rb") as f:
part = MIMEApplication(f.read(), _subtype="pdf" if file_path.endswith(".pdf") else "vnd.openxmlformats-officedocument.spreadsheetml.sheet")
part.add_header("Content-Disposition", "attachment", filename=file_path)
msg.attach(part)</p><h1>发送</h1><p>server = smtplib.SMTP("smtp.example.com", 587)
server.starttls()
server.login("sender@example.com", "app_password")
server.send_message(msg)
server.quit()</p>
常见报错和对应修复点
smtplib.SMTPDataError: (554, b'Transaction failed') —— 多半是附件没设 Content-Disposition 或文件路径不存在;
UnicodeEncodeError: 'ascii' codec can't encode characters —— HTML 字符串没指定 charset="utf-8",或 MIMEText 初始化时漏了第三个参数;
- 所有
MIMEText必须显式传入"utf-8"编码,不能依赖默认 - 附件文件名含中文?用
Header类编码:from email.header import Header; part.add_header("Content-Disposition", "attachment", filename=Header("中文.pdf", "utf-8").encode()) - Gmail 拒绝登录?改用应用专用密码(App Password),且确保账户开启两步验证
附件太大或 HTML 含图片资源怎么办?
smtplib 对单次发送的数据量没硬限制,但邮箱服务商有(Gmail 是 25MB)。超限会直接断连,不会抛出明确异常,只会卡在 send_message() 或返回模糊错误码。
- 超过 10MB 的附件建议转用云存储链接 + 文本说明,而非硬塞进邮件
- HTML 里引用本地图片?不行 —— 收件方看不到。要转成内联 base64(用
MIMEImage),或托管到公网 URL - 避免在 HTML 中写
<img src="file:///...">,这种路径仅发件人本地有效
边界、编码、头字段、服务商限制——这四点漏掉任何一环,附件就大概率消失或 HTML 变乱码。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










