Python使用SMTPLIB、DKIMPY发邮件偶现550无From头退信问题
问题根因
这个偶发报错的核心原因是邮件头序列化时偶发丢失CRLF换行分隔符,导致接收方MTA无法识别独立的From头,和DKIM签名逻辑本身无关,你贴出的异常邮件源码已经暴露了序列化异常的特征:
- 非参数类的头值末尾出现无意义的分号
MIME-Version: 1.0没有作为独立头存在,直接拼在了上一个Content-Type头的boundary参数同一行
触发这个bug的直接原因是代码逻辑存在顺序问题:
- 第一次调用
msg.as_bytes()生成待签名内容时,Python email模块会触发内部序列化逻辑,生成头折行缓存 - 之后手动给msg对象追加
DKIM-Signature头,再第二次调用msg.as_bytes()生成最终发送内容时,旧版本email模块(尤其是Python3.6及以下使用compat32策略的版本)会偶发丢失头之间的换行符,导致多个头被拼接成同一个头的内容 - 当换行丢失的位置刚好出现在From头附近时,From头会被识别为上一个头的参数/值内容,接收方严格按RFC解析时就会统计到0个From头,触发550报错。因为折行逻辑和头长度、内容长度强相关,所以问题是偶发的,和你观察到的50封出1-2封的概率完全吻合。
另外还有两个潜在风险点:
- 没有对传入头字段的变量做特殊字符清洗,如果个别发件人名称、主题、收件人地址里携带
\r/\n换行字符,会触发头注入,同样会破坏头结构 - DKIM签名覆盖的头列表不全,没有包含Date、Message-ID、Content-Type等标准头,容易被严格反垃圾系统拦截。
修复方案
按以下步骤调整代码即可彻底解决问题:
- 调整序列化和签名顺序,避免二次序列化
先把所有邮件正文(plain、html部分)、所有标准头(To、From、Subject、Date、Message-ID)全部添加到msg对象完成后,只做一次序列化,DKIM签名生成后直接拼接在邮件字节流的头部,不要追加头到msg对象后再二次序列化。
修正后的核心代码参考:msg = MIMEMultipart("alternative") msg.attach(MIMEText(self.email_text, "plain")) msg.attach(MIMEText(self.email_html, "html")) msg["To"] = self.to_email msg["From"] = f"{self.from_name} <{self.from_email}>" if self.from_name else self.from_email msg["Subject"] = self.subject msg['Date'] = formatdate() msg['Message-ID'] = make_msgid(domain=self.sender_domain) # 所有内容、头添加完成后,只做一次序列化拿到待签名原文字节流 msg_bytes = msg.as_bytes() # 补全签名覆盖的头列表 headers = [b'to', b'from', b'subject', b'date', b'message-id', b'mime-version', b'content-type'] # 生成完整的DKIM签名字节串 sig = dkim.sign( msg_bytes, self.dkim_selector.encode(), self.sender_domain.encode(), self.dkim_key.encode(), include_headers=headers ) # 直接把签名拼在邮件字节流最前面,不再走msg对象序列化 self.msg_data = sig + msg_bytes - 增加头字段输入清洗
所有赋值给头字段的变量(to_email、from_name、from_email、subject)提前做清洗,移除其中的\r、\n字符,避免偶发头注入破坏结构。 - 升级email模块策略(可选)
如果使用Python3.7以下版本,建议升级到3.7+,使用默认的EmailPolicy替代旧的compat32策略,旧策略的折行、序列化逻辑存在多个已知的偶发bug。
内容的提问来源于stack exchange,提问作者Mr Growbot
相关产品推荐
相关产品推荐

