Search code examples
python-3.xemail-attachmentssmtplib

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


I've created a sample function to test sending emails with an attached html file, which i intend to use for reporting on automated test runs in the future (replacing an existing external powershell script). Note that I'm attaching the html file, not using the html as inline text in the body. I'm using our company's mailgun smtp account service to send the email.

I seem to have an issue with Outlook 365 (web hosted - uses the outlook.office.com domain) either rejecting or blocking the sent email, but interestingly the same email is received and accepted by my personal hotmail address (outlook.live.com domain). I've found Outlook 365 blocks or does not accept the email when I attempt to name the file in the email message object. But if I don't name it, it will come through (with a default name of "ATT00001.htm" ).

My code for this is below but they key line seems to be

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

If I drop the filename key it works (but with a default assigned filename) e.g.

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

I have a suspicion there is something in the attachment's header or Content-disposition that Outlook 365 doesn't agree with, but i'm not sure what it is or how to work around.

I'm using the following (Python 3.6.5, on Windows 10 machine, smtplib and email.message seem to be built in)

Here is the code:

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()

What I've tried

  1. Tried sending with the same code using a textfile (with appropriate types). e.g.
    msg.add_attachment(open_file.read(), maintype='text', subtype='plain', filename=filename)
    Result: This works as expected (comes through with the given name - the filename is a string variable e.g. testfile.txt)

  2. adding msg.make_mixed() to make sure it is identified as a multipart message. Result: No effect

  3. Turning on the smtp debug level 1, Result: Mailgun says that everything has worked fine (and the messages do appear as expected in my hotmail account)

  4. Not using the filename key in the msg.add_attachment call. Result: This works the attachment comes through at ATT00001.htm Interestingly the default name is *.htm while the filename I'm trying to use is *.html

    1. Tried using a filename with *.htm and a subtype of 'htm' (instead of html) Result: Same as for html (received on hotmail but not on outlook 365)

    2. Tried using the generic types of maintype=''application', subtype='octet-stream'.
      e.g. msg.add_attachment(open_file.read(), maintype='application', subtype='octet-stream', filename=filename)
      Result: Same as for html (received on hotmail but not on outlook 365)

    3. Tried using mimetypes.guess as shown in this link
      https://docs.python.org/3.6/library/email.examples.html

    ctype, encoding = mimetypes.guess_type(path) if ctype is None or encoding is not None: # No guess could be made, or the file is encoded (compressed), so # use a generic bag-of-bits type. ctype = 'application/octet-stream' maintype, subtype = ctype.split('/', 1) with open(path, 'rb') as fp: msg.add_attachment(fp.read(), maintype=maintype, subtype=subtype, filename=filename)
    Result: It's determined as maintype='text', subtype='html' and I get the same result as with my original code (ie arrives in hotmail but blocked by 365).

    1. Checking my spam and clutter folders - was not there

Any suggestions on why the use of filename would be breaking it?

Update After sending to a other email addresses with various providers I discovered:

1) muppet@sharklasers.com was not a trusted sender (can change this)

2) I discovered the attachment was being flagged as unsafe. The html file comes from pytest's html report with the single file option. It contains javascript for row expanders. Gmail warns the attachment may not be safe (office 365 just straight out blocks the email altogether).

Not sure how to work around 2). I can email the same file to myself between outlook 365 and gmail and vice versa and the file doesn't get blocked. It only get's blocked when I use the above script using python's libraries and Mailgun SMTP. I suspect there is something I need to change in the email header to get around this. But I don't know what.

There seems to be some connection between trying to add the filename and the attachment being marked as unsafe


Solution

  • Okay I figured it out. The problem was the content-type needed to include "name=filename" in it's value. Also I needed to use maintype='multipart', subtype='mixed'.

    I have 2 solutions.

    solution 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)
    

    solution 2 (according to the documentation using the email.mime libraries is a legacy solution and the EmailMessage method is supposed to be used in preference.

    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)