Using python 3.7, how to ensure that a parsed email is re-generated correctly after modifying its body content correctly without losing any content?

Viewed 494

I would like to parse (serialize) a multipart email (or any and all types with multiple email attachments having inline images, attachments etc), set its body content-type with some extra information and re-generate the email before sending it out. How can this be achieved with the most simplest python code? Using python version 3.6. Right now I am using email.walk() and getting the first available text/plain, text/html and assume its the body of the covering mail, Further parsing recursively thru' the different parts of the mail by walk()...this seems cumbersome. Any easy method? Adding a code snippet what I am trying: #!/usr/bin/python3 """ USAGE: From commandline: cat mail | python warning_mail_stdout.py

This script reads mail from stdin and appends mail body with the "warning" and prints out the appended mail on to stdout """

import imaplib
import email
import logging
import smtplib
import re

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# from subprocess import Popen, PIPE
import sys
import email, os

title = ''
logger = logging.getLogger()
handler = logging.StreamHandler()
formatter = logging.Formatter(
    '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)

from email.policy import default

from email import policy
from email.parser import BytesParser
from email.mime.message import MIMEMessage
parser = email.parser.Parser()

def login():
    # Read message from Std Input
    mail = BytesParser(policy=policy.default).parse(open('1email_sample.eml','rb'))
    if mail is None:
        print("No mail in stdin ...")
    else:
        #mail is not None:
        mbody = ''
        mhtml = ''
        text_body = ''
        mail_charset = None
        html_body = ''
        mail_from = email.utils.parseaddr(mail['From'])[1]
        mwarn = ''
        new_message = ''
        attached_bool = False
        mail_attached_bool = False
        h=p=related_body=None
        h_body=plain_body = None
        new_message1 = email.message.EmailMessage()#policy=default)
        warning_txt = "[Caution: This message came from an external domain.]" 
        new_message = email.message.EmailMessage()
        
        # copying header values
        for header_val in mail:
            parser.parsestr(header_val)
            new_message[header_val] = mail[header_val]
            print(new_message[header_val])

        mailbody = mail.get_body(preferencelist=('related', 'html', 'plain'))

        new_message1.attach(mailbody)
        
        new_message.set_type('multipart/related')
        new_message.attach(new_message1)

        for p  in mail.iter_attachments():
            if p.is_attachment(): 
                new_message.attach(p)

        recipients = [
                      "sha73@yahoo.com",
        ]

         # s  ---is the smtp server to send out mail
        #'''
        try:
            print(new_message.get_content_type(), 'vvvvv sent')
            s.sendmail(mail_from, recipients,bytes(new_message))
        except UnicodeEncodeError:
            s.sendmail(mail_from, recipients,
                       new_message.as_string().encode("utf-8"))
        #'''
        s.close()

if __name__ == "__main__":
    login()
2 Answers

If you just want to change something in the body part you don't even need the walk method. get_body can do the work for you, and although it may give unexpected results on some non-compliant messages it is probably far more reliable than anything you or I could come up with. So the flow of your program should be

  • use a parser object to build the tree
  • probably check the defects attribute to make sure the message is ok for you
  • call get_body on your tree
  • do what you need to on the mime part returned by get_body
  • use a generator object to serialize the tree

I'm adding some examples here. Unfortunately the docs are far from clear so maybe this helps:

>>> from email.message import EmailMessage
>>> import email.policy
>>> from email import message_from_binary_file

>>> msgin = open('1email_sample.eml', 'rb')      
>>> msg = message_from_binary_file(msgin, policy=email.policy.default)
>>> for part in msg.walk():
        print(part.get_content_type())       
multipart/related
multipart/alternative
text/plain
text/html
image/png
image/png
image/png
image/png
image/png
image/png
image/png

>>> body = msg.get_body('plain') ##get the plain text, not 'related'
>>> btext = body.get_payload()
>>> type(btext)
<class 'str'>
>>> len(btext)
6926
>>> body.set_payload('this is my message' + btext)
>>> len(body.get_payload())
6944

>>> body2=msg.get_body('html') ##same with html
>>> bhtml=body2.get_payload()
>>> type(bhtml)
<class 'str'>

Multipart emails are separated by a multipart boundary. Details on what this looks like can be found here (https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). You need not modify and try to reconstitute the email from the parsed object. Simply parse the email with the library of your choice (in this case from email.mime.multipart import MIMEMultipart). Once you identify the part that you wish to modify, derive the boundary marker get_boundary(). Now you can take the raw email, split it by the boundaries, go to the section you wish to modify and inject your changes.

Related