Flask 邮件安全不满足 Microsoft Outlook 的安全要求?

2024-01-11

我们有一个向客户端发送电子邮件的 Web 应用程序,该 Web 应用程序使用 Flask 邮件框架来处理该问题。大约两周前,我们的网络应用程序无法向客户和我们自己的团队发送电子邮件。我们使用 Office 365 的 Outlook 作为发件人。

远程服务器返回'554 5.6.0 消息内容损坏; STOREDRV.Deliver.Exception:ConversionFailedException;由于消息内容转换出现永久性异常,无法处理消息:TNEF 内容摘要已损坏。 ConversionFailedException:内容转换:TNEF 内容摘要已损坏。 [阶段:PromoteCreateReplay]' 原始邮件标题:

这是发件人在被指示发送电子邮件后收到的错误消息。我们联系了我们的 Office 365 管理员,微软告诉他我们的 Web 应用程序的安全性不符合微软的要求/协议。

问题是 Flask 邮件是否使用了旧的安全协议或配置,无法与 Microsoft Outlook 很好地配合?


Outlook.com / Office365 错误消息没有什么帮助,因为它可以指示任意数量的问题。它表明 Microsoft 邮件服务器对电子邮件打包的某些方面(标头、附件等)不满意,并且它们的解析器在某处出错。否则,他们的错误消息所提供的详细信息几乎毫无用处。我发现这是一个断言安全问题胡言乱语; Flask-Mail 使用经过充分测试的 Python 标准库email and smtplib通过 TLS 加密连接发送电子邮件的软件包。

对于 Heroku 上的 Flask-Mail,我将问题追溯到 Heroku Dyno 机器上生成的 Message-ID 标头。该问题不仅限于 Heroku,但是,您会在任何具有以下功能的主机上看到此问题长主机名。典型的 Heroku dyno 主机名以完整的 UUID 开头,再加上另外 5 个左右的组件,例如aaf39fce-569e-473a-9453-6862595bd8da.prvt.dyno.rt.heroku.com.

该主机名用在为每封电子邮件生成的 Message-ID 标头中。 Flask-Mail 包使用标准email.utils.make_msgid()功能 https://docs.python.org/3/library/email.utils.html#email.utils.make_msgid生成标头,默认情况下使用当前主机名。这会产生一个 Message-ID 标头,如下所示:

Message-ID: <154810422972.4.16142961424846318784@aaf39fce-569e-473a-9453-6862595bd8da.prvt.dyno.rt.heroku.com>

这是一个 110 个字符长的字符串。这对于电子邮件标头来说是一个小问题,因为电子邮件 RFC 声明标头should限于78人物。然而,有一些方法可以解决这个问题;对于长度超过 77 个字符的标头值,您可以使用以下规定RFC 5322 https://www.rfc-editor.org/rfc/rfc5322 to fold标头。折叠可多用RFC 2047 https://www.rfc-editor.org/rfc/rfc2047 编码词在多行上。这就是这里发生的情况,上面的电子邮件标题变成

Message-ID: =?utf-8?q?=3C154810422972=2E4=2E16142961424846318784=40aaf39fce-?=
 =?utf-8?q?569e-473a-9453-6862595bd8da=2Eprvt=2Edyno=2Ert=2Eheroku=2Ecom=3E?=

其中 78 和 77 个字符现在符合电子邮件 MIME 标准。

所有这一切在我看来都是符合标准以及处理邮件标头的有效方法。或者至少是其他邮件提供商能够容忍并正确处理的东西,但微软的邮件服务器却没有这样。他们确实不喜欢上面的 RFC2047 编码的 Message-ID 标头,并尝试将正文包装在 TNEF winmail.dat 附件中。这并不总是有效,所以你最终会得到非常神秘的结果554 5.6.0 消息内容损坏错误信息。我认为这是微软的一个错误;我不能 100% 确定电子邮件 RFC 允许使用编码字折叠 Message-ID 标头,但 MS 通过向收件人发送无意义的错误而不是在接收时拒绝邮件来处理错误,这实在是太糟糕了。

你可以设置一个替代方案电子邮件政策 https://docs.python.org/3/library/email.policy.html通过设置 Flask-Mail 来使用flask_mail.message_policy模块全局,或者我们可以生成不同的消息 ID。

电子邮件策略仅在您使用 Python 3.3 或更高版本时才可用,但它是处理折叠的策略对象,因此允许我们更改 Message-ID 和其他 RFC 5322 标识符标头的处理方式。这是一个不会折叠 Message-ID 标头的子类;该标准实际上允许单行最多 998 个字符,并且此子类仅对此标头使用该限制:

import flask_mail
from email.policy import EmailPolicy, SMTP

# Headers that contain msg-id values, RFC5322
MSG_ID_HEADERS = {'message-id', 'in-reply-to', 'references', 'resent-msg-id'}

class MsgIdExcemptPolicy(EmailPolicy):
    def _fold(self, name, value, *args, **kwargs):
        if (name.lower() in MSG_ID_HEADERS and
            self.max_line_length < 998 and
            self.max_line_length - len(name) - 2 < len(value)
        ):
            # RFC 5322, section 2.1.1: "Each line of characters MUST be no
            # more than 998 characters, and SHOULD be no more than 78
            # characters, excluding the CRLF.". To avoid msg-id tokens from being folded
            # by means of RFC2047, fold identifier lines to the max length instead.
            return self.clone(max_line_length=998)._fold(name, value, *args, **kwargs)
        return super()._fold(name, value, *args, **kwargs)

flask_mail.message_policy = MsgIdExcemptPolicy() + SMTP

在 Python 2.7 或 Python 3.2 或更早版本上,您必须替换 Message-Id 标头,只需使用硬编码域名重新生成标头:

from flask import current_app
from flask_mail import Message as _Message

# set this to your actual domain name
DOMAIN_NAME = 'example.com'

class Message(_Message):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # work around issues with Microsoft Office365 / Outlook.com email servers
        # and their inability to handle RFC2047 encoded Message-Id headers. The
        # Python email package only uses RFC2047 when encoding *long* message ids,
        # and those happen all the time on Heroku, where the hostname includes a
        # full UUID as well as 5 more components, e.g.
        # aaf39fce-569e-473a-9453-6862595bd8da.prvt.dyno.rt.heroku.com
        # The work-around is to just use our own domain name, hard-coded, but only
        # when the message-id length exceeds 77 characters (MIME allows 78, but one
        # is used for a leading space)
        if len(self.msgId) > 77:
            domain = current_app.config.get('MESSAGE_ID_DOMAIN', DOMAIN_NAME)
            self.msgId = make_msgid(domain=domain)

然后你就可以使用上面的Message类而不是flask_mail.Message()类,并且它将生成一个较短的 Message-ID 标头,不会与 Microsoft 有问题的标头解析器发生冲突。

I filed Python 项目的错误报告 https://bugs.python.org/issue35805跟踪 msg-id 令牌的处理,因为我怀疑这确实应该在那里解决。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Flask 邮件安全不满足 Microsoft Outlook 的安全要求? 的相关文章

随机推荐