Help creating a Delivery Status Notification using email library module

I’m trying to create a DSN (also known as a Non-Delivery Report) using the email library module.

I create a new message and add 3 attachments. When I try to print the resulting email, I get this error:

Traceback (most recent call last):
  File "/home/mgrant/./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'

I’ve narrowed this down to the second attachment which I create as follows:

msg.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 refused
Last-Attempt-Date: {}
""".format(domain, recipient, recipient, received).encode('us-ascii'),
                   maintype="message",
                   subtype="delivery-status",
                   cte=None)

If I change the maintype to “text” and subtype to “plain”, the message prints out, but with the incorrect content-type for that part. The message/delivery-status causes it to call _handle_message_delivery_status() in generator.py. I’m starting to wonder if there’s a bug in there, but equally, I’m not convinced I’m doing this correctly.

In _handle_message_delivery_status() in generator.py, there is this loop:

        for part in msg.get_payload():
            s = self._new_buffer()
            g = self.clone(s)
=>          g.flatten(part, unixfrom=False, linesep=self._NL)
...

msg.get_payload() is returning the entire string which I formatted in my attachment above, for example:

(Pdb) msg.get_payload()
'Reporting-MTA: dns; example.com\n\nOriginal-Recipient: rfc822;joe.user@example.com\nFinal-Recipient: rfc822;joe.user@example.com\nAction: failed\nStatus: 5.7.1\nDiagnostic-Code: smtp; 571 Delivery not authorized, message refused\nLast-Attempt-Date: Thu, 05 May 2022 18:54:51 -0000\n'

Hence the for loop, part gets each character of this in form of a string starting with ‘R’, clearly this isn’t what that loop is expecting!

Is this a bug in _handle_message_delivery_status()? or am I not creating this body part attachment correctly?

Not to detract, but if you can use Sendgrid - I would use it. It has a python library too.

Sendgrid? as in the paid service to send email? I’m completely confused as to why this might be helpful. I’m developing something in Python, it’s sending and receiving emails.

Sendgrid has a free tier. Sendgrid is just a very streamlined solution for sending email via Python and PowerShell. They have a web API and SMTP servers.

I guess I didn’t realize you needed to receive them.

You may check Github Issues page if there is one.

I had the exact same issue, I ended up using the old MIMEMultipart object to create the message.

Here is the code, it is a bit more general than just sending DSN’s.

class Part(BaseModel):
    body: str
    content_type: tuple[str, str]

class Email(BaseModel):
    from_: EmailStr = Field(..., alias="from")
    to: EmailStr | list[EmailStr]
    subject: str
    body: str
    headers: Optional[dict[str, str]]
    parts: Optional[list[Part]]
    html: Optional[bool] = False

def create_message(email: Email):
    msg = MIMEMultipart()
    msg["Subject"] = email.subject
    msg["From"] = email.from_
    msg["To"] = email.to if not isinstance(email.to, list) else ",".join(email.to)
    if email.headers is not None:
        for header_name, header_value in email.headers.items():
            msg[header_name] = header_value

    body = MIMEText(email.body, _subtype="html" if email.html else "plain")

    parts: list = [body]

    if email.parts:
        for part in email.parts:
            m = Message()
            m.set_payload(part.body)
            subpart = MIMEMessage(m, _subtype=part.content_type[1])
            parts.append(subpart)

    msg.set_payload(parts)
    return msg

I managed to get around this by setting the policy and passing a contentmanager arg (I just passed the raw_data_manager from the email.contentmanager package) like this:

from email.policy import default
from email import policy
from email.mime.text import MIMEText
from email.headerregistry import ContentTypeHeader

then, when I create the DSN:

dsn = EmailMessage()
dsn.policy = policy.SMTP
dsn.make_mixed()

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 refused
Last-Attempt-Date: {}
""".format(domain, recipient, recipient, received).encode("ascii"),
                   maintype="report-type",
                   subtype="delivery-status",
                   content_manager=email.contentmanager.raw_data_manager,
                   cte='7bit')

Hi Michael,

I do need something similar. Have you made this library public? if so please share.

Thanks

The email library is part of python. The code here is an example to use it. There is nothing more to make public, just use it like the example given.