需先用email.header.decode_header()解码附件名再拼路径;取最新邮件应优先用UID+SINCE按时间筛选而非ALL切片;附件内容须调用get_payload(decode=True)解码;同名附件应以UID+原名组合避免覆盖。

附件名乱码导致保存失败
Python 的 imaplib 默认不解析邮件头编码,Content-Disposition 里的中文附件名会变成类似 =?UTF-8?B?5byg5LiJLmpwZw==?= 的编码串,直接用它当文件名会触发 OSError: [Errno 22] Invalid argument。
得先用 email.header.decode_header() 解码,再拼出合法路径:
from email.header import decode_header
raw_name = msg.get_filename() or ''
decoded_parts = decode_header(raw_name)
filename = ''.join(
part.decode(enc or 'utf-8') if isinstance(part, bytes) else part
for part, enc in decoded_parts
)
- 注意:
decode_header()返回的是(bytes, encoding)元组列表,不能直接 str() 强转 - 空附件名(如纯文本邮件)时
get_filename()可能返回None,要兜底 - Windows 下还建议用
filename.replace('', '_')过滤非法字符
只取最新 N 封邮件却漏掉带附件的
IMAP4.search() 的 RECENT 和 ALL 不代表“按时间倒序”,服务器返回的 ID 是递增的,但最新邮件 ID 最大 —— 如果你用 search(None, 'ALL') 拿到所有 ID 再切片 [-10:],很可能切到的是旧邮件 ID,因为 ID 不等于收件时间顺序。
正确做法是先按 DATE 排序(需服务器支持 SORT 扩展),或用 UID + FETCH 拉时间戳再本地排序:
typ, data = M.uid('SEARCH', None, 'SINCE', '01-Jan-2024')
uids = data[0].split()
# 按 UID 降序取前 10(UID 通常随时间递增)
latest_uids = uids[-10:] if len(uids) > 10 else uids
- 优先用
UID模式(M = imaplib.IMAP4_SSL(...); M.login(...); M.select(); M.uid(...)),避免邮件移动后 ID 错位 -
SINCE参数格式必须是'DD-Mon-YYYY',不是 ISO 格式,否则返回空 - 某些邮箱(如 QQ 邮箱)不支持
SORT,只能靠UID+ 时间字段二次筛选
附件内容读出来是空或乱码
常见原因是没正确识别 multipart 子部分的 Content-Transfer-Encoding(比如 base64 或 quoted-printable),直接读 get_payload() 返回的是未解码原始字符串。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
要用 email.message.Message.get_payload(decode=True),且确保该 part 是 multipart 的叶子节点:
for part in msg.walk():
if part.get_content_maintype() == 'multipart':
continue
if part.get('Content-Disposition') and 'attachment' in part.get('Content-Disposition'):
payload = part.get_payload(decode=True)
if payload is None:
continue # 可能是空附件或嵌套 multipart
# 此时 payload 是 bytes,可直接写入文件
-
get_payload(decode=True)仅对 base64/qp 编码有效,对 7bit/8bit 等不做处理,但实际邮件中极少出现 - 有些邮件把附件藏在
message/rfc822类型里(即转发的整封邮件),需递归解析 - 别用
part.as_string()提取内容,那会包含头信息,导致二进制损坏
分类保存时同名附件被覆盖
不同邮件可能有相同附件名(如都叫 report.pdf),直接用 open(filename, 'wb') 会覆盖。但加时间戳又难追溯来源,更稳妥的是按邮件 UID + 原始名组合:
safe_name = f"{uid}_{filename}"
# 或去重:检查是否存在,存在则追加序号
counter = 1
final_path = os.path.join(save_dir, safe_name)
while os.path.exists(final_path):
name, ext = os.path.splitext(safe_name)
final_path = os.path.join(save_dir, f"{name}_{counter}{ext}")
counter += 1
- UID 是服务器分配的唯一标识,比时间戳更可靠,也方便后续查哪封邮件带这个附件
- 注意 Windows 路径长度限制(260 字符),过长的 UID+文件名要截断,但保留扩展名和 UID 后几位
- 如果业务要求严格按语义分类(如“发票”“合同”),光靠文件名不够,得结合邮件正文关键词或发件人规则,那是另一层逻辑了
附件提取真正麻烦的从来不是下载本身,而是邮件客户端五花八门的编码习惯、服务器对 IMAP 扩展的支持差异,还有那些看似合理实则失效的“标准假设”——比如认为 get_filename() 总能拿到名字,或 ALL 搜索结果天然有序。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










