Outlook.com / Office365 错误消息没有多大帮助,因为它可以指示任意数量的问题。这表明 Microsoft 邮件服务器对电子邮件打包的某些方面(标题、附件等)不满意,并且它们的解析器在某处出错。他们的错误信息在它提供的细节方面几乎没有用处。我认为这是一个安全问题的断言是无稽之谈。Flask-Mail 使用经过充分测试的 Python 标准库email
和smtplib
包通过 TLS 加密连接发送电子邮件。
对于 Heroku 上的 Flask-Mail,我将问题追溯到 Heroku Dyno 机器上生成的 Message-ID 标头。问题不仅限于 Heroku,但是,您会在任何具有长 hostname的主机上看到此问题。您典型的 Heroku dyno 主机名以完整的 UUID 开头,再加上另外 5 个左右的组件,例如aaf39fce-569e-473a-9453-6862595bd8da.prvt.dyno.rt.heroku.com
.
此主机名用于为每封电子邮件生成的 Message-ID 标头中。Flask-Mail 包使用标准email.utils.make_msgid()
函数来生成标头,并且默认使用当前主机名。这会产生一个 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 使用的替代电子邮件策略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 有问题的标头解析器发生冲突。
我向Python 项目提交了一份错误报告,以跟踪 msg-id 令牌的处理,因为我怀疑这真的应该在那里解决。