Search code examples
pythonpandasemailsmtpmime

Sending email with dataframe as an attached file


I am trying to create a function that can send an email with a dataframe attached as a csv file. Attaching files normally requires the file to be saved to disk first, so I don't know if there is any straight forward way to go about this?

I have created a function that can attach the dataframe as HTML, and also a function that can send attachments as email, but nothing that can directly send the dataframe as an attached file

General setup

from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
import os
_server = 'MY_SERVER'
_port = 9999
_sender = 'my.email@domain.com'

def create_simple_mail(to, title):
    mail = MIMEMultipart()
    mail['Subject'] = title
    mail['To'] = to
    mail['From'] = _sender

Sending dataframe as html

def mail_dataframe_as_html(to, msg, title, df):
    mail = create_simple_mail(to, msg, title)
    html_str = msg
    html_str += '<tr></tr><tr></tr>'
    html_str += df.to_html()
    mail.attach(MIMEText(html_str, 'html'))
    smtp_connection = smtplib.SMTP(_server, _port, timeout=120)
    smtp_connection.sendmail(_sender, to, mail.as_string())

Sending attachments

def attach_file_to_mail(mail,f):
    with open(f, "rb") as fil:
        part = MIMEApplication(fil.read(), Name=os.path.basename(f))
        part['Content-Disposition'] = 'attachment; filename="%s"' % os.path.basename(f)
        mail.attach(part)
    return mail

def mail_html(to, title, html, attachments=None):
    mail = create_simple_mail(to=to, msg=None, title=title)
    mail.attach(MIMEText(html, 'html'))
    if attachments is not None:
        for f in attachments:
            mail = attach_file_to_mail(mail,f)
    smtp_connection = smtplib.SMTP(_server, _port, timeout=120)
    smtp_connection.sendmail(_sender, to, mail.as_string())

Solution

  • I figured out a quite neat solution using __enter__ and __exit__ to clean-up a temporary generated file that can be attached to the email.

    import os
    import uuid
    
    class CreateAttachments:
        def __init__(self, dfs, file_names=None):
            self._attachments = dfs
    
            # Create generic file names if none was specified
            if file_names is None:
                self._names = [f'attached_df{i}.csv' for i in range(len(dfs))]
    
            # Ensure all file names ends with the .csv extension
            else:
                self._names = [fn if fn.endswith('.csv') else fn + '.csv' for fn in file_names]
    
                # If less file names than attachments were provided, generic names are appended
                if len(self._names) < len(self._attachments):
                    self._names += [f'attached_df{i}.csv' for i in range(len(self._attachments) - len(self._names))]
    
            # Create a temporary folder for the files
            self._file_location = 'C:/Users/%s/%s/' % (os.getenv('username'), str(uuid.uuid4()))
    
        def __enter__(self):
            # If by the odd chance the temporary folder already exists, we abort the program
            if os.path.isdir(self._file_location):
                raise IsADirectoryError("Directory already exists. Aborting")
    
            # Create temporary folder
            os.makedirs(self._file_location)
    
            # For each attachment, save the dataframe to a .csv in the temporary folder
            attach_paths = []
            for attach, name in zip(self._attachments, self._names):
                tmp_path = self._file_location + name
                attach.to_csv(tmp_path)
                attach_paths.append(tmp_path)
            
            # Save all paths to class, as they are needed in the __exit__ function
            self.paths = attach_paths
            
            # Return paths to use for attachments
            return self.paths
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            # Remove the .csv files for each attachment
            for path in self.paths:
                os.remove(path)
            
            # Remove the temporary folder
            os.removedirs(self._file_location)
    

    With the above code, I can now create attachments like so:

    import pandas as pd
    
    df1 = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    df2 = pd.DataFrame([[2, 4, 6], [3, 6, 9], [4, 8, 12]])
    
    html_str = '''<table style="width:100%">'''
    html_str += '<tr> <font size="4"><b>Some HTML example</b> </font size> </tr>'
    html_str += '<br> This is a test mail.'
    with CreateAttachments([df1, df2]) as f:
        mail_html(to='receiver@domain.com',
                  html=html_str,
                  title='Test email with attachments',
                  attachments=f)