python – Flask邮件安全性不符合Microsoft Outlook的安全要求?

我们有一个Web应用程序向客户端发送电子邮件,Web应用程序正在使用Flask邮件框架来处理它.大约2周前,我们的网络应用程序无法向客户和我们自己的一群人发送电子邮件.我们使用Office 365的Outlook作为发件人.

Remote Server returned ‘554 5.6.0 Corrupt message content; STOREDRV.Deliver.Exception:ConversionFailedException; Failed to process message due to a permanent exception with message Content conversion: Corrupt summary TNEF content. ConversionFailedException: Content conversion: Corrupt summary TNEF content. [Stage: PromoteCreateReplay]’
Original message headers:

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

问题是Flask邮件使用较旧的安全协议或配置是否与Microsoft Outlook不兼容?

最佳答案 Outlook.com / Office365错误消息不太有用,因为它可以指示任何数量的问题.它表明Microsoft邮件服务器对电子邮件打包(标题,附件等)的某些方面感到不满,并且他们的解析器在某处出错.否则,它们的错误消息在其提供的详细信息中是无用的.我发现断言这是一个安全问题是无稽之谈; Flask-Mail使用经过充分测试的
Python标准库电子邮件和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() function生成标头,默认情况下使用当前主机名.然后,这会产生一个Message-ID标头,如:

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

这是一个长110个字符的字符串.这是电子邮件标题的一个小问题,因为电子邮件RFC声明标题应限制为78个字符.然而,有办法解决这个问题;对于超过77个字符的标头值,您可以使用RFC 5322中的规定折叠标题.折叠可以在多行上使用多个RFC 2047编码的单词.这就是这里发生的事情,上面的电子邮件标题变为

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 Corrupt message content错误消息.我认为这是微软的错误;我并非100%确定电子邮件RFC允许使用编码字折叠Message-ID标头,但MS通过向接收方发送无意义错误而不是在接收时拒绝该消息来处理错误是非常糟糕的.

您可以通过将flask_mail.message_policy模块设置为global来为Flask-Mail设置备用email 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有问题的标头解析器冲突.

我提交了a bug report with the Python project来跟踪msg-id令牌的处理,因为我怀疑这应该在那里得到解决.

点赞