<p>在Outlook.com/Office365错误消息不太有用,因为它可以指示任何数量的问题。这表明微软邮件服务器对邮件打包的某些方面(邮件头、附件等)不满意,它们的解析器在某个地方出错了。他们的错误信息在其他方面几乎毫无用处。我发现这是一个<em>安全问题的断言是无稽之谈;Flask Mail使用经过良好测试的Python标准库<code>email</code>和{<cd2>}包通过TLS加密的连接发送电子邮件。在</p>
<p>对于Heroku上的Flask Mail,我将问题追溯到Heroku Dyno机器上生成的消息ID头。这个问题不仅限于Heroku,而且在任何主机名为<em>长</em>的主机上都会出现这种情况。典型的Heroku dyno主机名以完整的UUID开头,再加上大约5个组件,例如<code>aaf39fce-569e-473a-9453-6862595bd8da.prvt.dyno.rt.heroku.com</code>。在</p>
<p>此主机名用于为每个电子邮件生成的邮件ID标头。Flask邮件包使用标准的<a href="https://docs.python.org/3/library/email.utils.html#email.utils.make_msgid" rel="noreferrer">^{<cd4>} function</a>生成头,默认情况下使用当前主机名。然后会生成一个消息ID头,如:</p>
<pre><code>Message-ID: <154810422972.4.16142961424846318784@aaf39fce-569e-473a-9453-6862595bd8da.prvt.dyno.rt.heroku.com>
</code></pre>
<p>这是一个110个字符长的字符串。这对于电子邮件头来说是个小问题,因为电子邮件rfc声明headers<em>应该限制在<em>78个</em>字符。但是,有一些方法可以解决这个问题;对于长度超过77个字符的头值,可以使用<a href="https://tools.ietf.org/html/rfc5322" rel="noreferrer">RFC 5322</a>中的规定来<em>折叠</em>头。折叠可以在多行上使用多个<a href="https://tools.ietf.org/html/rfc2047" rel="noreferrer">RFC 2047</a><em>编码单词</em>。这里就是这样,上面的邮件头变成</p>
^{pr2}$
<p>这是78和77个字符,现在符合电子邮件MIME标准。在</p>
<p>在我看来,所有这些都是符合<em>标准的</em>和处理邮件头的有效方法。或者至少是其他邮件提供商能够容忍并正确处理的内容,但微软的邮件服务器却没有。他们真的不喜欢上面的rfc247编码的消息ID头,并试图将主体包装在TNEF中winmail.dat版附件。这并不总是有效的,所以您最终得到的是非常神秘的<em>554 5.6.0 Corrupt message content</em>错误消息。我认为这是微软的一个缺陷;我不能百分之百地肯定电子邮件rfc允许使用编码字折叠邮件ID头,但是MS通过向收件人发送一个无意义的错误来处理错误,而不是在接收时拒绝邮件,这真是太糟糕了。在</p>
<p>您可以通过设置<code>flask_mail.message_policy</code>模块global来为Flask Mail设置一个备用的<a href="https://docs.python.org/3/library/email.policy.html" rel="noreferrer">email policy</a>,或者我们可以生成一个不同的message-ID</p>
<p>电子邮件策略仅在使用Python3.3或更高版本时可用,但它是处理折叠的policy对象,因此允许我们更改邮件ID和其他rfc5322标识符头的处理方式。下面是一个子类,它不会折叠消息ID头;标准实际上允许一行最多998个字符,而此子类仅对该头使用该限制:</p>
<pre><code>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
</code></pre>
<p>在Python 2.7或Python 3.2或更高版本上,您将不得不使用一个硬编码的域名重新生成消息头:</p>
<pre><code>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)
</code></pre>
<p>然后使用上面的<code>Message</code>类而不是<code>flask_mail.Message()</code>类,它将生成一个较短的消息ID头,不会与Microsoft有问题的头解析程序冲突。在</p>
<p>我提交了<a href="https://bugs.python.org/issue35805" rel="noreferrer">a bug report with the Python project</a>来跟踪msg id令牌的处理,因为我怀疑这应该真的在那里解决。在</p>