You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python EmailMessage创建PGP签名邮件时多余MIME头问题

Python新版email API构造PGP签名邮件问题解决

问题背景

重构基于旧版Python email API实现的PGP分离签名多部分邮件脚本时,使用新版API编写的代码出现签名MIME段被额外添加冗余头部的问题。

旧版可正常运行代码

from email.message import Message
from email.mime.multipart import MIMEMultipart
import gnupg

def messageFromSignature(signature):
    message = Message()
    message['Content-Type'] = 'application/pgp-signature; name="signature.asc"'
    message['Content-Description'] = 'OpenPGP digital signature'
    message.set_payload(signature)
    return message

def send_message(subject, body):
    basemsg = Message()
    basemsg.set_payload(body)
    basemsg.set_charset('utf-8')

    gpg = gnupg.GPG()
    basetext = basemsg.as_string().replace('\n', '\r\n')
    signature = str(gpg.sign(basetext, keyid='xxxxxxxxxxxxxxxx', detach=True))
    if signature:
        signmsg = messageFromSignature(signature)
        msg = MIMEMultipart(_subtype="signed", micalg="pgp-sha512", protocol="application/pgp-signature")
        msg.attach(basemsg)
        msg.attach(signmsg)
    else:
        print('Warning: failed to sign the message!')

新版出错代码

from email.message import EmailMessage, MIMEPart
import gnupg

def messageFromSignature(signature):
    message = MIMEPart()
    message.set_content(signature)
    message.replace_header('Content-Type', 'application/pgp-signature; name="signature.asc"')
    message['Content-Description'] = 'OpenPGP digital signature'
    return message

def send_message(subject, body):
    basemsg = EmailMessage()
    basemsg.set_content(body)
    basemsg.set_charset('utf-8')

    gpg = gnupg.GPG()
    basetext = basemsg.as_string().replace('\n', '\r\n')
    signature = str(gpg.sign(basetext, keyid='xxxxxxxxxxxxxxxx', detach=True))
    if signature:
        signmsg = messageFromSignature(signature)
        msg = basemsg
        msg.make_mixed()
        msg.add_attachment(signmsg)
        msg.replace_header('Content-Type', 'multipart/signed; micalg="pgp-sha512"; protocol="application/pgp-signature";')
    else:
        print('Warning: failed to sign the message!')

错误现象

签名段被额外封装了一层message/rfc822头部,自动添加了MIME-Version、Content-Disposition等冗余字段,实际生成的MIME段如下:

--===============0054553979676888393==
Content-Type: message/rfc822
Content-Transfer-Encoding: 8bit
MIME-Version: 1.0
Content-Disposition: attachment

Content-Type: application/pgp-signature; name="signature.asc"
Content-Transfer-Encoding: 7bit
Content-Description: OpenPGP digital signature

-----BEGIN PGP SIGNATURE-----

iHUEABYKAB0WIQQTeptvPezKETGQAId81GVnkrOh+QUCYqWYUAAKCRB81GVnkrOh
+SJSAQCre0cbhMf6frI9jILwc0XXGkByXUgiBkexcpYnXN/HzAD/WaPZA5hTutMg
5lPOQdZGaeVpsApYDL/fmbH6SniTvAg=
=xuOv
-----END PGP SIGNATURE-----

--===============0054553979676888393==--

问题解答

1. 如何实现旧版attach方法的效果,无冗余头部添加签名子段

核心问题是用错了API方法:

  • add_attachment的设计用途是添加文件类附件,传入参数预期是字符串、字节流或者文件对象。当传入MIMEPart/Message实例时,方法会默认把传入对象当成独立的完整嵌套邮件,自动封装message/rfc822外层头、添加附件处置标记,才会出现冗余头部。
  • 新版API完全保留了attach()方法,行为和旧版完全一致:直接将传入的MIME部件作为同级子段追加到当前多部分消息的payload列表中,不会做任何额外封装。

正确实现步骤:

  • 构造签名段时,直接在set_content中指定内容类型,不需要先设置默认文本类型再替换头,避免生成多余的传输编码配置错误。
  • 不要在正文消息对象上直接调用make_mixed()修改类型,单独构造顶层multipart/signed类型消息,再依次attach正文段和签名段即可。

修正后的核心代码:

from email.message import EmailMessage, MIMEPart
import gnupg

def messageFromSignature(signature):
    message = MIMEPart()
    # 直接指定签名段的内容类型、传输编码,避免后续替换头
    message.set_content(
        signature,
        cte='7bit',
        content_type='application/pgp-signature',
        params={'name': 'signature.asc'}
    )
    message['Content-Description'] = 'OpenPGP digital signature'
    return message

def send_message(subject, body):
    # 构造正文段
    basemsg = EmailMessage()
    basemsg.set_content(body, charset='utf-8')

    gpg = gnupg.GPG()
    basetext = basemsg.as_string().replace('\n', '\r\n')
    signature = str(gpg.sign(basetext, keyid='xxxxxxxxxxxxxxxx', detach=True))
    if signature:
        signmsg = messageFromSignature(signature)
        # 构造顶层signed类型多部分消息
        msg = EmailMessage()
        msg['Subject'] = subject
        # 手动设置multipart/signed的Content-Type参数
        msg['Content-Type'] = 'multipart/signed; micalg="pgp-sha512"; protocol="application/pgp-signature"'
        # 用attach方法追加子段,和旧版行为完全一致,无额外封装
        msg.attach(basemsg)
        msg.attach(signmsg)
    else:
        print('Warning: failed to sign the message!')

2. 为什么add_attachment会为MIMEPart添加MIME-Version头部

这是add_attachment方法的预设逻辑,不是bug:
当方法检测到传入的是Message/MIMEPart类实例时,会将其判定为独立的、可单独解析的完整RFC822格式消息。按照MIME规范,独立存在的MIME消息必须携带MIME-Version: 1.0头部,因此方法会自动补全该头部,同时添加对应的嵌套消息封装头。
这个行为和“MIME子部件不需要自带MIME-Version”的规则不冲突——因为add_attachment本来就不是用来添加同级MIME子部件的方法,它处理传入的MIMEPart时,是把它当成一个独立的嵌套消息附件,而非当前多部分消息的直接子段。


内容的提问来源于stack exchange,提问作者janeden

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 19:24:19