5
\$\begingroup\$

The below class implementing a simple way to create emails with attachments, including the option to create in-memory zip files and attach them to the email.

Because I am not a professional programmer but like to write good code I will be glad to get comments. Two main questions do I create proper email headers and proper attachment headers.

import sys,os,smtplib,email,io,zipfile
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email import Encoders

class sendmail(object):
    def __init__(self):
        self.subject=""
        self.body=""
        self.mail_from=""
        self.mail_to=""
        self.attachments=[]
        self.attachments2zip=[]
        
    def add_body_line(self,text):
        self.body="%s\r\n%s"%(self.body,text)
        
    def set_body(self,text):
        self.body=text
        
    def new_mail(self,frm,to,sbj):
        self.subject=sbj
        self.body=""
        self.mail_from=frm
        self.mail_to=to
        self.attachments=[]
        self.attachments2zip=[]
        
    def set_subject(self,text):
        self.subject=text
        
    def set_from(self,text):
        self.mail_from=text
        
    def set_to(self,text):
        self.mail_to=text
        
    def add_attachment(self,file):
        self.attachments.append(file)
        
    def add_attachment_zipped(self,file):
        self.attachments2zip.append(file)
        
    def send(self):
        message = MIMEMultipart()
        message["From"] = self.mail_from
        message["To"] = self.mail_to
        message["Subject"] = self.subject
        #message["Bcc"] = receiver_email  # Recommended for mass emails
        
        message.attach(MIMEText(self.body, "plain"))
        
        if len(self.attachments)>0:#If we have attachments
            for file in self.attachments:#For each attachment
                filename=os.path.basename(file)
                #part = MIMEApplication(f.read(), Name=filename)
                part = MIMEBase('application', "octet-stream")
                part.set_payload(open(file, "rb").read())
                Encoders.encode_base64(part)
                part.add_header('Content-Disposition', 'attachment; filename="%s"'%(filename))
                message.attach(part)
                
        if len(self.attachments2zip)>0:#If we have attachments
            for file in self.attachments2zip:#For each attachment
                filename=os.path.basename(file)
                zipped_buffer = io.BytesIO()
                zf=zipfile.ZipFile(zipped_buffer,"w", zipfile.ZIP_DEFLATED)
                zf.write(file, filename)
                zf.close()
                zipped_buffer.seek(0)
                
                part = MIMEBase('application', "octet-stream")
                part.set_payload(zipped_buffer.read())
                Encoders.encode_base64(part)
                part.add_header('Content-Disposition', 'attachment; filename="%s.zip"'%(filename))
                message.attach(part)
        
        text = message.as_string()
        server = smtplib.SMTP('localhost')
        server.sendmail(self.mail_from, self.mail_to, text)
        server.quit()
\$\endgroup\$
2
  • \$\begingroup\$ I rolled back your last edit. After getting an answer you are not allowed to change your code anymore. This is to ensure that answers do not get invalidated and have to hit a moving target. If you have changed your code you can either post it as an answer (if it would constitute a code review) or ask a new question with your changed code (linking back to this one as reference). See the section What should I not do? on What should I do when someone answers my question? for more information \$\endgroup\$ Commented Jul 13, 2021 at 17:44
  • \$\begingroup\$ Why Python 2? It's been deprecated for a while. \$\endgroup\$
    – Reinderien
    Commented Jul 14, 2021 at 1:50

1 Answer 1

2
\$\begingroup\$

While your sendemail class is small, it does a lot of stuff. It is also not that friendly to use, because you have to create the sendemail object and then set all the parameters with methods. For me an ideal interface would look something like:

Email(
    Subject="sub",
    Body="body", 
    From="from", 
    To=["to"], 
    Attachments=[file1, file2],
)
.send()

You could then have separate file readers which would convert file path to email part:

class Attachment:
    def __init__(self, path):
        self.path = path

    def file(self):
        file = self.__read_file(self.path)
        return convert_to_part(file)

    def zip(self):
        file = self.__read_file(path)
        zip_buffer = self.__zip_file(file)
        return convert_to_part(zip_buffer)

    def __convert_to_attachment(buffer, filename):
        part = MIMEBase('application', "octet-stream")
        part.set_payload(buffer)
        Encoders.encode_base64(part)
        part.add_header('Content-Disposition', 'attachment; filename="%s.zip"'%(filename))
        return part

    def __read_file(self): 
        return open(self.path, "rb").read()

    def __zip_file(self, file): 
        filename=os.path.basename(file)
        zipped_buffer = io.BytesIO()
        zf = zipfile.ZipFile(zipped_buffer,"w", zipfile.ZIP_DEFLATED)
        zf.write(file, filename)
        zf.close()
        zipped_buffer.seek(0)
        return zipped_buffer

I am not sure if I refactored everything correctly, but the idea is to use it like:

Email(
    ...
    Attachments=[Attachment('path').zip()]
)
.send()

You could modify the Email to have a builder style attaching:

class Email:
    ...
    attach(self, attachment):
        self.Attachments.append(attachment)

...

Email(...)
.attach(Attachment('path').zip())
.attach(Attachment('path').file())
.send()

This leaves you with the following Email class:

class Email(object):
    def __init__(self, From, To, Subject, Body, **kwargs):
        self.subject = Subject
        self.body = Body
        self.mail_from = From
        self.mail_to = To
        self.attachments = kwargs.get('Attachments', [])

    def __build_message(self):
        message = MIMEMultipart()
        message["From"] = self.mail_from
        message["To"] = self.mail_to
        message["Subject"] = self.subject
        
        message.attach(MIMEText(self.body, "plain"))
        
        for part in self.attachments:
            message.attach(part)
        
        return message.as_string()
        
    def send(self):
        message = self.__build_message()

        server = smtplib.SMTP('localhost')
        server.sendmail(self.mail_from, self.mail_to, message)
        server.quit()

As you can see, the class Email is now very simple. So simple in fact that it could easily be converted to a function. I come from Java, so objects do not bother me, but for the sake of being principled:

def build_message(From, To, Subject, Body, **kwargs):
    message = MIMEMultipart()
    message["From"] = From, 
    message["To"] = To
    message["Subject"] = Subject
        
    message.attach(MIMEText(Body, "plain"))
        
    for part in self.attachments:
        message.attach(part)
        
    return message.as_string()

def send_email(From, To, Subject, Body, **kwargs):
    message = build_message(From, To, Subject, Body, **kwargs)

    server = smtplib.SMTP('localhost')
    server.sendmail(self.mail_from, self.mail_to, message)
    server.quit()

You could do a similar thing with the Attachment class. So instead of Attachment.file(path) you would make a file_attachment(path) and zip_attachment(path).

Edit:

To make the code easier to use for the OP, I propose the following utility function(s):

def build_file_attachments(paths):
    return [Attachment(path).file() for path in paths]

def build_zip_attachments(paths):
    return [Attachment(path).zip() for path in paths]

def build_attachments(file_attachment_paths, zip_attachment_paths):
    file_attachments = build_file_attachments(file_attachment_paths)
    zip_attachments = build_zip_attachments(zip_attachment_paths)

    return file_attachments + zip_attachments

def send_email_with_attachments(
    From, 
    To, 
    Subject, 
    Body, 
    file_attachment_paths=[], 
    zip_attachment_paths=[]
):
    attachments = build_attachments(file_attachment_paths, zip_attachment_paths)

    send_email(From, To, Subject, Body, Attachments=attachments)

Now you would use it the same as your original one, but without the setters and the "spaghetti" you had in your send() method before.

send_email_with_attachments(
    From="[email protected]",
    To="[email protected]",
    Subject="Very important!",
    Body="I wanted to show you something",
    file_attachment_paths=["important-thing-1.jpg"],
    zip_attachment_paths=["important-thing-2/dir"]
)

If you still find this confusing and hard to use, you can do something similar to what you had originally:

class Email:
    def __init__(self, from, to, subject):
        self.__from = from
        self.__to = to
        self.__subject = subject
        self.__body = ''
        self.__zips = []
        self.__files = []

    def sender(self, from):
        self.__from = from
        return self

    def recipient(self, to):
        self.__to = to
        return self

    def subject(self, subject):
        self.__subject = subject
        return self

    def message(self, body):
        self.__body = body
        return self

    def add_new_line(self, line=''):
        self.__body = "%s\r\n%s"%(self.body, line)
        return self

    def add_zip(self, path):
        self.__zips.append(path)
        return self

    def add_file(self, path):
        self.__files.append(path)
        return self

    def send():
        send_email_with_attachments(self.from, self.to, self.subject, self.body, self.files, self.zips)
    

You can use this like so:

email = Email("[email protected]", "[email protected]", "Not so important")

email.message('Hi,')
email.add_new_line()
email.add_new_line('actually it is important. Check this out:')
email.add_new_line()

email.add_file('actually-not-important.pdf')
email.add_zip('./important')

email.send()

Which is basically the same as:

Email("[email protected]", "[email protected]", "Not so important")
    .message('Hi,')
    .add_new_line()
    .add_new_line('actually it is important. Check this out:')
    .add_new_line()
    .add_file('actually-not-important.pdf')
    .add_zip('./important')
    .send()

Choose whichever style you prefer :)

Email class is now just there to remember the parameters, it has no idea, what needs to be done to actually send the email. The send method now passes the parameters into send_email_with_attachments(), which handles the sending... Kind of...

It also has no idea how to actually send the email and that is ok, it just delegates the parameters into the functions that know how to do this stuff, which makes it a lot easier to understand. The point of all this is that you want to move lower level stuff (like reading files, zipping, making multipart message, etc.) as far away as possible, so you are able to program at a higher level (closer to natural language).

The side effect of this is also that you will end up with a ton of reusable code, which you will then be able to use to assemble the higher level code from.

\$\endgroup\$
7
  • \$\begingroup\$ Thanks for the review. I took the idea of creating separate functions for attaching data and reading/zipping. And while your code looks much more professional, I think my version is more clear for people who write code occasionally, like myself :) \$\endgroup\$
    – Alex
    Commented Jul 13, 2021 at 17:47
  • \$\begingroup\$ But why? I am not saying that in an arrogant way, I'm genuinely curious what makes you prefer your code (except that you wrote it) :) Also, are you talking about the usability or implementation? \$\endgroup\$
    – Blaž Mrak
    Commented Jul 13, 2021 at 17:55
  • \$\begingroup\$ I am talking mainly about usability. It is not easy to understand how code like Email(...).attach(Attachment('path').zip()).attach(Attachment('path').file()).send() actually works. At least for me. And I didn't know you can break it into multiple lines. For mee adding body line by line more intuitive than writing it single with inline \n or using <<< that may add unwanted tabs \$\endgroup\$
    – Alex
    Commented Jul 13, 2021 at 18:27
  • \$\begingroup\$ @Alex I have made an edit. Hopefully it is closer to something that you tried to achieve. \$\endgroup\$
    – Blaž Mrak
    Commented Jul 13, 2021 at 20:19
  • \$\begingroup\$ It is now very close to what I currently have, but all functions of your Attachment class are inside the email class because I don't see how you can reuse the Attachment class for something different. I also found that usage of MIMEApplication can shorten the code. \$\endgroup\$
    – Alex
    Commented Jul 14, 2021 at 12:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.