Python 的电子邮件消息库输出未被 Outlook 365 接受,当我有来自

Python's Email Message library output not getting accepted by Outlook 365 when i have a named attachments from

我创建了一个示例函数来测试发送带有附加 html 文件的电子邮件,我打算将其用于报告未来的自动测试运行(替换现有的外部 powershell 脚本)。请注意,我附加的是 html 文件,而不是使用 html 作为 body 中的内联文本。我正在使用我们公司的mailgun smtp帐户服务来发送邮件。

我似乎遇到了 Outlook 365(网络托管 - 使用 outlook.office.com 域)拒绝或阻止发送的电子邮件的问题,但有趣的是,我的个人 hotmail 地址收到并接受了同一封电子邮件( outlook.live.com 域)。当我尝试在电子邮件 object 中命名文件时,我发现 Outlook 365 阻止或不接受电子邮件。但如果我不给它命名,它就会通过(默认名称为 "ATT00001.htm" )。

我的代码如下,但关键行似乎是

msg.add_attachment(open_file.read(), maintype='text', subtype='html', filename=filename)

如果我删除文件名键,它会起作用(但使用默认分配的文件名)例如

msg.add_attachment(open_file.read(), maintype='text', subtype='html')

我怀疑 Outlook 365 不同意附件的 header 或 Content-disposition 中的某些内容,但我不确定它是什么或如何解决。

我正在使用以下内容(Python 3.6.5,在 Windows 10 机器上,smtplib 和 email.message 似乎是内置的)

代码如下:

import smtplib
from email.message import EmailMessage
import os


def send_mail():
    MAILGUN_SMTP_LOGIN = "<my company's mailgun login>"
    MAILGUN_SMTP_PASSWORD = "<my company's mailgun password>"

    fromaddr = "muppet@sharklasers.com" # the from address seems to be inconsequential 
    toaddr = ['me@mycompanysdomainusingoffice365.com.au', 'me@hotmail.com']

    msg = EmailMessage()
    msg.preamble = 'This is preamble. Not sure where it should show in the email'

    msg['From'] = fromaddr
    msg['To'] = ', '.join(toaddr)
    msg['Subject'] = 'Testing attached html results send'
    msg.set_content(""" This is a test of attached html """)


    filename = 'api_automatedtests_20180903_1341.html'
    filepath = os.path.abspath('D:/work/temp/api_automatedtests_20180903_1341.html')
    open_file = open(filepath, "rb")
    # msg.make_mixed()
    msg.add_attachment(open_file.read(), maintype='text', subtype='html', filename=filename)
    # msg.add_attachment(open_file.read(), maintype='text', subtype='html')


    server = smtplib.SMTP(host="smtp.mailgun.org", port=587)
    server.ehlo()
    server.starttls()
    server.login(MAILGUN_SMTP_LOGIN, MAILGUN_SMTP_PASSWORD)
    server.set_debuglevel(1)
    server.send_message(msg)
    server.quit()


if __name__ == "__main__":
    send_mail()

我试过的

  1. 尝试使用文本文件(具有适当的类型)使用相同的代码发送。例如
    msg.add_attachment(open_file.read(), maintype='text', subtype='plain', filename=filename)
    结果:这按预期工作(通过给定的名称 - 文件名是一个字符串变量,例如 testfile.txt)

  2. 添加 msg.make_mixed() 以确保它被识别为多部分消息。结果:没有效果

  3. 打开 smtp 调试级别 1,结果:Mailgun 说一切正常(邮件确实按预期显示在我的 hotmail 帐户中)

  4. 在 msg.add_attachment 调用中未使用文件名键。 结果:这有效附件通过 ATT00001.htm 有趣的是,默认名称是 *.htm 而我尝试使用的文件名是 *.html

    1. 尝试使用带 *.htm 的文件名和 'htm' 的子类型(而不是 html) 结果:与 html 相同(在 hotmail 上收到,但在 outlook 365 上未收到)

    2. 尝试使用 maintype=''application'、subtype='octet-stream'.
      的泛型类型 例如msg.add_attachment(open_file.read(), maintype='application', subtype='octet-stream', filename=filename)
      结果:与 html 相同(在 hotmail 上收到,但在 outlook 365 上未收到)

    3. 已尝试使用 mimetypes.guess,如此 link
      所示 https://docs.python.org/3.6/library/email.examples.html

    ctype, 编码 = mimetypes.guess_type(路径) 如果 ctype 是 None 或编码不是 None: # 无法猜测,或者文件被编码(压缩),所以 # 使用通用 bag-of-bits 类型。 ctype = 'application/octet-stream' 主类型,子类型 = ctype.split('/', 1) 打开(路径,'rb')作为 fp: msg.add_attachment(fp.read(), 主类型=主类型, 亚型=亚型, 文件名=文件名)
    结果:它被确定为 maintype='text', subtype='html' 并且我得到与我的原始代码相同的结果(即到达 hotmail 但被 365 阻止)。

    1. 检查我的垃圾邮件和杂乱文件夹 - 不存在

关于为什么使用文件名会破坏它的任何建议?

更新 在发送到不同提供商的其他电子邮件地址后,我发现:

1) muppet@sharklasers.com 不是受信任的发件人(可以更改)

2) 我发现附件被标记为不安全。 html 文件来自 pytest 的 html 报告,带有单个文件选项。它包含用于行扩展器的 javascript。 Gmail 警告附件可能不安全(Office 365 直接完全阻止电子邮件)。

不确定如何解决 2)。我可以在 outlook 365 和 gmail 之间通过电子邮件将同一文件发送给自己,反之亦然,并且该文件不会被阻止。只有当我使用 python 的库和 Mailgun SMTP 使用上述脚本时,它才会被阻止。我怀疑我需要在电子邮件 header 中更改一些内容以解决此问题。但是我不知道是什么。

尝试添加文件名和附件被标记为不安全之间似乎存在某种联系

好的,我明白了。问题是需要在其值中包含 "name=filename" 的内容类型。 我还需要使用 maintype='multipart', subtype='mixed'.

我有 2 个解决方案。

解决方案 1

import smtplib
from email.message import EmailMessage
import os

def send_mail(body_text, fromaddr, recipient_list, smtp_login, smtp_pass, file_path):
    msg = EmailMessage()
    msg.preamble = 'This is preamble. Not sure where it should show'

    msg['From'] = fromaddr
    msg['To'] = ', '.join(recipient_list)
    msg['Subject'] = 'API Testing results'
    msg.set_content(body_text)

    filename = os.path.basename(file_path)
    open_file = open(file_path, "rb")
    msg.add_attachment(open_file.read(), maintype='multipart', subtype='mixed; name=%s' % filename, filename=filename)

    server = smtplib.SMTP(host="smtp.mailgun.org", port=587)
    server.ehlo()
    server.starttls()
    server.login(smtp_login, smtp_pass)
    server.send_message(msg)
    server.quit()


if __name__ == "__main__":
    smtp_login = "<my smtp login>"
    smtp_pass = "<my smtp password>"
    recipient_list = ['user1@mycompany.com.au', 'user2@mycompany.com.au']
    file_path = os.path.abspath('D:/work/temp/api_automatedtests_20180903_1341.html')
    body_text = "test results for 03/09/2018 "
    fromaddr = 'autotesting@mycompany.com.au'
    send_mail(body_text=body_text, recipient_list=recipient_list, smtp_login=smtp_login, smtp_pass=smtp_pass,
              file_path=file_path)

解决方案 2(根据文档,使用 email.mime 库是一个遗留解决方案,应该优先使用 EmailMessage 方法。

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
import os

def send_mail(body_text, fromaddr, recipient_list, smtp_login, smtp_pass, file_path):

    msg = MIMEMultipart()
    msg['From'] = fromaddr
    msg['To'] = ', '.join(recipient_list)
    msg['Subject'] = "Sending API test results"
    msg.attach(MIMEText(body_text, 'plain'))

    filename = os.path.basename(file_path)
    attachment = open(file_path, "rb")

    part = MIMEBase('multipart', 'mixed; name=%s' % filename)
    part.set_payload(attachment.read())
    encoders.encode_base64(part)
    part.add_header('Content-Disposition', "attachment; filename= %s" % filename)

    msg.attach(part)

    server = smtplib.SMTP(host="smtp.mailgun.org", port=587)
    server.starttls()
    server.login(smtp_login, smtp_pass)
    text = msg.as_string()
    server.set_debuglevel(1)
    server.sendmail(fromaddr, recipient_list, text)
    server.quit()

if __name__ == '__main__':
    smtp_login = "<my smtp login>"
    smtp_pass = "<my smtp password>"
    recipient_list = ['user1@mycompany.com.au', 'user2@mycompany.com.au']
    file_path = os.path.abspath('D:/work/temp/api_automatedtests_20180903_1341.html')
    body_text = " Api test results for 03/09/2018 "
    fromaddr = "autotest@mycompany.com.au"
    send_mail(body_text=body_text, fromaddr=fromaddr, recipient_list=recipient_list, smtp_login=smtp_login, smtp_pass=smtp_pass,
              file_path=file_path)