Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

send a DSN (Non-delivery report) using the python email module

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

  1. content-type: text/plain a human readable report
  2. content-type: message/delivery-status a machine readable report
  3. content-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.

  1. Is this the correct way to build a DSN using this module?
  2. How can I fix this policy attribute problem?
like image 553
Michael Grant Avatar asked Mar 09 '26 03:03

Michael Grant


1 Answers

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()
like image 94
Moby Duck Avatar answered Mar 10 '26 16:03

Moby Duck



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!