I'm trying to send a DSN (Delivery Status Notification, also known as a Non-Delivery Report) from python3 using the email module.
A DSN is a MIME message with Content-Type: multipart/report; report-type=delivery-status
The messages contain 2 attachments (and an optional 3rd):
content-type: text/plain a human readable reportcontent-type: message/delivery-status a machine readable reportcontent-type: message/rfc822 optionally the original message#
# Get data from msg
#
headers = Parser(policy=default).parsestr(msg)
recipient = headers['to'].addresses[0].addr_spec
domain = headers['to'].addresses[0].domain
date = email.utils.formatdate(localtime=True)
#
# Create a new email message
#
dsn = EmailMessage()
dsn.policy = policy.SMTP # <-- this didn't help
dsn.make_mixed()
dsn['From'] = headers['to']
dsn['Date'] = email.utils.localtime(dt=None)
dsn['Message-Id'] = email.utils.make_msgid(idstring=None, domain=None)
dsn['Subject'] = 'Returned Mail: Refused'
dsn['To'] = headers['return-path']
#
# The human readable part
#
dsn.add_attachment("""\
----- The following address had delivery problems -----
<{}> (unrecoverable error: Refused)
""".format(recipient).encode(),
maintype="text",
subtype="plain",
cte=None)
#
# The machine readable part
#
dsn.add_attachment("""\
Reporting-MTA: dns; {}
Original-Recipient: rfc822;{}
Final-Recipient: rfc822;{}
Action: failed
Status: 5.7.1
Diagnostic-Code: smtp; 571 Delivery not authorized, message returned
Last-Attempt-Date: {}
""".format(domain, recipient, recipient, date).encode('us-ascii'),
maintype="message", # <--- these 2 lines cause
subtype="delivery-status", # <--- the issue
cte=None)
#
# The original message
#
dsn.add_attachment(msg.encode(),
maintype="message",
subtype="rfc822",
cte=None)
#
# Set the Content-Type header in the message headers
#
dsn.replace_header('Content-Type', 'multipart/report')
dsn.set_param('report-type', 'delivery-status')
print(dsn) # <--- Dies in here
When the DSN is printed, I receive the following traceback:
Traceback (most recent call last):
File "./send-dsn.py", line 97, in <module>
print(dsn)
File "/usr/lib/python3.9/email/message.py", line 971, in __str__
return self.as_string(policy=self.policy.clone(utf8=True))
File "/usr/lib/python3.9/email/message.py", line 968, in as_string
return super().as_string(unixfrom, maxheaderlen, policy)
File "/usr/lib/python3.9/email/message.py", line 158, in as_string
g.flatten(self, unixfrom=unixfrom)
File "/usr/lib/python3.9/email/generator.py", line 116, in flatten
self._write(msg)
File "/usr/lib/python3.9/email/generator.py", line 181, in _write
self._dispatch(msg)
File "/usr/lib/python3.9/email/generator.py", line 218, in _dispatch
meth(msg)
File "/usr/lib/python3.9/email/generator.py", line 276, in _handle_multipart
g.flatten(part, unixfrom=False, linesep=self._NL)
File "/usr/lib/python3.9/email/generator.py", line 116, in flatten
self._write(msg)
File "/usr/lib/python3.9/email/generator.py", line 181, in _write
self._dispatch(msg)
File "/usr/lib/python3.9/email/generator.py", line 218, in _dispatch
meth(msg)
File "/usr/lib/python3.9/email/generator.py", line 335, in _handle_message_delivery_status
g.flatten(part, unixfrom=False, linesep=self._NL)
File "/usr/lib/python3.9/email/generator.py", line 107, in flatten
old_msg_policy = msg.policy
AttributeError: 'str' object has no attribute 'policy'
The problem seems to be the maintype and subtype of the second attachment, the content-type: message/delivery-status attachment. If I change this to text/plain, the DSN prints except that the second attachment has the wrong content-type.
I had to do this too, and it took a bit of figuring out. I'm sure I'll need to refer back to this answer some day...
As @tripleee points out, this uses the older API. However I wasn't able to acheive this with the new EmailMessage API without hacky workarounds, so it's the best "proper" solution I have for the moment until the newer API matures.
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.message import MIMEMessage
from email.message import Message
# Content-Type: multipart/report; report-type=delivery-status
msg = MIMEMultipart('report', report_type= "delivery-status")
msg['Subject'] = 'Delivery Status Notification (Failure)'
msg['From'] = 'Mail Delivery Subsystem <[email protected]>'
msg['To'] = '[email protected]'
msg.attach(MIMEText("Human readable explanation. Mailbox unavailable.", 'plain'))
# message/delivery-status - machine readable rfc3464 report
delivery_status = """
Reporting-MTA: dns; {}
Original-Recipient: rfc822;{}
Final-Recipient: rfc822;{}
Action: failed
Status: 5.5.0
Diagnostic-Code: smtp; 550 Requested action not taken: mailbox unavailable
Last-Attempt-Date: {}
"""
dsn = Message()
dsn.set_payload(delivery_status)
msg.attach(MIMEMessage(dsn, 'delivery-status'))
# message/rfc822 - optionally the original message
original = Message()
original.set_payload("the original message, or a portion thereof, as an entity of type message/rfc822")
msg.attach(MIMEMessage(original, 'rfc822'))
print(msg)
server = smtplib.SMTP(smtp_host, smtp_port)
server.sendmail('', msg['To'], msg.as_string())
server.quit()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With