Sending Emails with Python

Loading

When I create a new python script for a project, I generally drop in a few prebuilt functions that become the core of the new project. One of these functions is the ability to email. It is a simple task. One of the most important for the script is to communicate, if needed, to the outside with its status and other information required for its purpose.

For this writing, I have taken the functions that are needed to do this task and created a stand-alone script to illustrate its usage.  Upon execution of the script, the main function is called upon and everything proceeds from there.

if __name__ == "__main__":
    main()

The Main Function

The first execution of all of my applications is to setup all of the variables that are needed for the task. This example is no different, the first line is to read the config file that has all of the passwords, usernames, and the like. I generally group similar scripts in the same folder on the server, and they share the same configuration files. The result is a list of variables that will be passed to the email function for its credential usage.

def main():
    email_list = Read_ConfigFile()

    files = ['main.py']

Files is the array of files that could be used as attachments. If the array is left blank, I have defaulted the function to be None. For this case, the attachment section of the email will be skipped. Otherwise, the function will attach all viable files that are in the list.

    email_subject = 'Hey read this email'
    email_body_html = """\
<html>
  <head></head>
  <body>
    <p>Hi!<br>
       How are you?<br>
       Here is the <a href="http://www.python.org">link</a> you wanted.
    </p>
  </body>
</html>
"""


    email_message(email_subject, email_body_html, email_list, files)

    email_body_text = 'Hey read this email.'
    email_message(email_subject, email_body_text, email_list, files)


The Config Function

I started years ago to never to have usernames and passwords embedded into the actual source code. It was easy to do, a simple variable and there it stays. This tactic was the general thing to for many of the different development teams. The issue sprang up when I was working on my first server code back in the 90’s. Thousands of lines long and hundreds of classes and functions. It took time and effort to pull them out and use a centralized file or database to store the objects. It just was sloppy development at the time.

So, from that came this simple configuration file idea. The function reads a config file and loads in a set of variables into a list. Upon completion returns the list to the calling routine.

def Read_ConfigFile():
    config_list = []
    config_parser = configparser.ConfigParser()
    config_parser.sections()
    config_file_path = r'email_settings.cfg'
    config_parser.read(config_file_path)

    # email variables
    config_list.append(config_parser['email_settings']['email_username'])
    config_list.append(config_parser['email_settings']['email_password'])
    config_list.append(config_parser['email_settings']['email_recipients'])
    config_list.append(config_parser['email_settings']['email_email_from'])

    return config_list

The Email Function

Only four objects are passed to the email function. These are the subject, body, email_list, and any files with their paths in a list format. The subject and body are text variables, while the email_list is the list that was passed from the configuration file function.

def email_message(subject, body, email_list, files=None):

I stored the list of recipients in the configuration file. This function was written for a server to use, and the list of people was generally small and not changing. The function could be altered to add the list of recipients as an input and skip this use of the config file. Another tactic that was used was to pass one email address to the function. The email address would be a group address and that list would be administrated by the email server adminstrator.

Using the config file as a source for the recipients, the addresses need to comma joined together.

    recipients = [email_list[2]]
    emailto = ', '.join(recipients)

    emailfrom = email_list[3]
    
    username = email_list[0]
    password = email_list[1]

Now, for the creation of the message object. This function uses the multipart version of the MIME object. Since the body of the email is not known of its type at the calling of this function, a test is done on the contents of the message. This test will determine if the email is just text, or contains HTML. If it falls in the latter, the body attachment is dealt as a HTML body, otherwise, it is considered as plain text.  

The subject line has caused errors before. Not the content, but the length. According to RFC-2822, the length of the subject line should not be longer than 78 characters. This includes any invisible characters such as tabs and carriage returns. This function will test to see if the subject line is longer than 75 characters, and if so, truncates it to fall within the RFC guidelines.

msg = MIMEMultipart('alternative')

    msg["From"] = emailfrom
    msg["To"] = emailto

    email_subject = (subject[:75] + '..') if len(data) > 75 else subject
    msg["Subject"] = email_subject

    if bool(BeautifulSoup(body, "html.parser").find()):
        msg.attach(MIMEText(body, 'html'))
    else:
        msg.attach(MIMEText(body, 'plain'))

Now for any attachments. If the files array is empty, this section will be skipped over. Otherwise, it will parse the array and add the attachment to the message object. The file list must contain the path and name of the object that will be attached to the message. Otherwise, there is no other inputs needed.

for f in files or []:
        with open(f, "rb") as fil:
            part = MIMEApplication(
                fil.read(),
                Name=basename(f)
            )
        part['Content-Disposition'] = 'attachment; filename="%s"' % basename(f)
        msg.attach(part)

Now the message for the email has been created, it is now time to send the message.  To define the server, we have to indicate the email server and what port to use. This example uses Gmail as the server and the standard port 587 to send its emails. While port 465 can be used, Gmail and the other examples below prefer to use Port 587 as the default port for SMTP message submission.

If the username and passwords are correct, the login process is completed, and the email is sent on its way. The server connection is closed by quitting the server.

    server = smtplib.SMTP('smtp.gmail.com', 587)
    server.starttls()
    server.login(username, password)
    server.sendmail(emailfrom, recipients, msg.as_string())
    server.quit()

Yahoo Mail

server = smtplib.SMTP("smtp.mail.yahoo.com",587)

iCloud Mail

server = smtplib.SMTP('smtp.mail.me.com', 587)

Office 365 – Microsoft

server = smtplib.SMTP('smtp.office365.com',587)

AOL mail

server = smtplib.SMTP("smtp.aol.com", 587)

Troubleshooting

One such issue might come up in the Configuration file that you should be warry. If a field has any special characters in the text, the config parser will throw an exception. For this reason, use numbers or characters in the field to get around this error.

Gmail

If you use Google as the mail server, you have to set up the email account to “Less secure app access” Any email that is sent, will cause an email be sent to your sender address warning of a security issue.

To fix this, log into your sender account and access “Manage Your Account”. Click on Security on the menu options, and navigate down to the “Less secure app access”.

Click on “Turn on access (not recommended)” and you be brought to a new screen. Toggle the switch named “Allow less secure apps”.

The Code

Email.py

# *************************************************************************
#
# Copyright (C) 2020 by Keith Foster
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without l> imitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the
# Software, and to permit persons to whom the Software is furnished
# to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
# THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# *************************************************************************
# Email Function
#
# LAST MODIFICATION:       2020-12-15
# ORIGINATION DATE:        n/a
#
# *************************************************************************
#
from bs4 import BeautifulSoup
import configparser
import smtplib
from os.path import basename
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText


def Read_ConfigFile():
    config_list = []
    config_parser = configparser.ConfigParser()
    config_parser.sections()
    config_file_path = r'email_settings.cfg'
    config_parser.read(config_file_path)

    # email variables
    config_list.append(config_parser['email_settings']['email_username'])
    config_list.append(config_parser['email_settings']['email_password'])
    config_list.append(config_parser['email_settings']['email_recipients'])
    config_list.append(config_parser['email_settings']['email_email_from'])

    return config_list


def email_message(subject, body, email_list, files=None):
    recipients = [email_list[2]]
    emailfrom = email_list[3]
    emailto = ', '.join(recipients)
    username = email_list[0]
    password = email_list[1]

    msg = MIMEMultipart('alternative')

    msg["From"] = emailfrom
    msg["To"] = emailto

    # We want to truncate the subject if over the max limit.
    email_subject = (subject[:75] + '..') if len(subject) > 75 else subject
    msg["Subject"] = email_subject

    # what type of body is this? HTML or plain text?
    if bool(BeautifulSoup(body, "html.parser").find()):
        msg.attach(MIMEText(body, 'html'))
    else:
        msg.attach(MIMEText(body, 'plain'))

    # File attachment section
    for f in files or []:
        with open(f, "rb") as fil:
            part = MIMEApplication(
                fil.read(),
                Name=basename(f)
            )
        part['Content-Disposition'] = 'attachment; filename="%s"' % basename(f)
        msg.attach(part)

    # Set up and connect to Gmail using port 587
    server = smtplib.SMTP('smtp.gmail.com', 587)
    server.starttls()
    server.login(username, password)
    server.sendmail(emailfrom, recipients, msg.as_string())
    server.quit()


def main():
    # Time to read the config file
    email_list = Read_ConfigFile()

    # lets attach the source of this file as a test.
    files = ['test email.py']
    email_subject = 'Hey read this email'

    # Email HTML body test
    email_body_html = """\
<html>
  <head></head>
  <body>
    <p>Hi!<br>
       How are you?<br>
       Here is the <a href="http://www.python.org">Python link</a> you wanted.
       <br><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/76/TapetumLucidum.JPG/1280px-TapetumLucidum.JPG" alt="Just a cat">
       
    </p>
  </body>
</html>
"""
    email_message(email_subject, email_body_html, email_list, files)

    # Email Plain Text test
    email_body_text = 'Hey read this email.'
    email_message(email_subject, email_body_text, email_list, files)


if __name__ == "__main__":
    main()

Email_Settings.cfg

[email_settings]

email_username = username
email_password = password
email_recipients = person_to_send_to.com
email_email_from = person_that_sent_email.com