Robotic Emails

Build a mass-emailer with easy templating in Python

rohan-bansal@rohan-bansal

Robot holding stack of emails illustration

Overview

In this workshop, you'll be using good ol' Python to send emails using a customizable template! The project should take about 20-30 minutes to complete.

Here are some links to the final code and a live demo (repl.it):

Final Code: GitHub

Demo: Live

Prerequisites

  • A Google account, preferably with 2FA disabled
  • Beginner to intermediate knowledge of Python functions and lists

Initial Setup

SMTP (Simple Mail Transfer Protocol) has been getting more and more secure as of late; and for good reason. To be able to send emails from your Google account, we need to give our Python application access.

Head over to this link and flick the "Allow less secure apps" lever to the on position. Google strictly allows only its own or verified software to interface with the creation/sending of emails; since you're creating a Python app, it's necessary to tell Google that it's acceptable.

less secure app access

You will also need to complete this captcha to enable access for the next application that uses your credentials (this one).

Quick Notes:

If you happen to have 2 Factor Authentication enabled for your account, please follow this article to generate an app password. When we write the program and input our account password, replace that with the app password you generated.

After working on this project, it is recommended to turn the less secure apps lever back to its off position.


File Setup

Head over to repl.it/languages/python3 and create a new Python3 project. In the sidebar, a main.py file should already be created. Go ahead and create two more files:

  • contacts.txt
  • message.txt
  • .env

uploadfiles

The contacts.txt file will contain emails and names that will be substituted into the message body later (can be customized). Feel free to add an email and a name to it, but make sure it's formatted exactly as below:

bobsmith@johndoe.com, Bob

In the message.txt file, add the following template, which we will process in our Python script.

  • The first line contains the subject of the email
  • The rest of the lines contain the body
Test Email

Dear {0},

    Bacon ipsum dolor amet venison ball tip hamburger tenderloin bresaola ribeye pancetta ham chuck rump buffalo. T-bone cow pork, capicola jerky short loin prosciutto picanha porchetta ribeye. Pig ground round shank frankfurter drumstick, pork belly bresaola tongue. Pancetta alcatra bacon ground round kielbasa beef landjaeger cupim prosciutto sirloin tongue jowl.

Sincerely,
<Your Name>

Please substitute <Your Name> with your name, as this won't be substituted automatically in this workshop (feel free to add it in later though)!

The .env file is used to hide your Gmail credentials from the public eye--it's only visible to you! Add the following to that file:

EMAIL=myemail@gmail.com
PASSWORD=baconisyummy55

Quick Note: wondering where I got that spicy placeholder text? Check out Bacon Ipsum.


Writing Code

I'll take this moment to ask you not to copy and paste. It takes away from the learning experience (and in many cases, things need replacing).

At the top of your main.py file, go ahead and add the following imports:

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import sys, os
  • smtplib is a Python module used to send emails, utilizing the Simple Mail Transfer Protocol
  • email is a Python module not used to send emails, but to manage email contents
  • email.mime is a library that uses the global MIME internet standard to format email messages
  • sys is a system library used to deal with file and program operations, and os is going to be our method of loading your email/password from the .env file

Next, lets load our credentials from the .env file!

address = os.getenv("EMAIL")
password = os.getenv("PASSWORD")

For more info about how virtual environments work (and how only you can see this env file), check out repl.it’s docs.

Let’s also initialize a connection to the Gmail SMTP mail server with those credentials.

mail = smtplib.SMTP('smtp.gmail.com', 587)
mail.ehlo()
mail.starttls()
mail.login(address, password)
  • The mail.ehlo() function is the application's way of identifying itself to the server. More info here.
  • The mail.starttls() function ensures that the connection is secure. More info here.

So far, this is what your code should look like:

beforecontacts

Getting Contacts

Now, let's obtain our contacts from the contacts.txt file!

Create a function and initialize a dictionary to hold our emails and names in key:value format:

def get_contacts(filename):
    contacts_list = {}

Next, let's open the contacts file. Python has an easy way of doing this using with open.... This reads the file and closes it immediately after we are finished! We open it in r (reading) mode, to make sure nothing is written to the file on accident, and we also use the utf-8 character encoding. Make sure to place the following code inside the get_contacts() function with an indent.

with open(filename, mode = 'r', encoding = 'utf-8') as f:

Now we have to actually read the contents in the file. To do this, we use a pretty straightforward .read() method, followed by .split('\n'). \n is a newline character in Python; by splitting a file by \n, we are essentially splitting up all the lines. Remember to place the following inside the with statement.

contacts = f.read().split('\n')

We also want to check if the contacts file is empty, and we do that by looking at the beginning of the file and checking if there is a character there. Place the following on the same indent as the previous code.

f.seek(0) # look at the beginning of the file
first = f.read() # read the beginning of the file
if first == '': # if it's empty, there are no contacts, print error
	print('Contacts file is empty.')
	sys.exit() # exit the program

Last but not least, we want to pair the substitutions with the emails (bobjoe@gmail.com with Bob). To do that, we will add the email and all the substitutions associated with that email to the contacts_list dictionary we initialized. The format is key: [value, another_value, etc.]. Add the following outside of the with statement in the previous code chunk since we no longer need the file.

for item in contacts:
	contacts_list[item.split(', ')[0]] = item.split(', ')[1:]
return contacts_list # return contacts dictionary

Explanation of the second line:

  • As we go through each line, we separate the emails from the substitutions (the ", " character)
  • We then pair the email (first part of the line) with the substitutions (second part) in the contacts_list dictionary

Here's what the code should look like so far:

Screenshot of Python file so far

Reading the Message

Great! You're basically 1/3 of the way there!

Woo-hoo GIF

Add another function called read_message to the main.py file at the bottom with a with open statement as explained in the previous section:

def read_message(filename):
    with open(filename, 'r', encoding = 'utf-8') as template:

We're also going to check if this file is empty exactly as we did in the previous section, so add the following inside the with statement.

template_content = template.read() # read the contents of the file to a variable
template.seek(0) # look at the beginning of the file
if template_content == '': # if empty, message is empty
	print('Message file is empty.')
	sys.exit() # quit program

Next, we need to get the subject of the email! If you look in the message.txt file, the subject is the very first line. To get this in Python, write the following outside the with statement in the previous code chunk.

subject = template_content.splitlines()[0].rstrip()

What does rstrip() do? It removes whitespace from the string, in case there was any. More info here.

Last, we need to return two things.

  • the subject
  • the email body

Add the following to return the subject and every line in the file except the first one:

return subject, '\n'.join(template_content.split('\n')[1:])

In the above snippet, since the text is read from the message file and the subject has already been parsed, it's not a good idea to send the subject again in the body. We split the content by newlines and keep everything after the first line; [1:] is the list operation that does this. More info here!

Let's test the functions we just made! At the end of your main.py file, add:

contacts = get_contacts('contacts.txt')
subject, template_content = read_message('message.txt')
print(contacts)
print(subject, template_content)

Go ahead and run the program! You can tell it succeeds when the contacts, subject, and body of the email print.

If you get an SMTP error, you need to go back to this link and complete the captcha again.

Formatting and Sending the Email

You can format and send this email in only 12 lines of code that you add to the end of the main.py file:

for contact_mail in list(contacts):
    msg = MIMEMultipart()
    msg_body = template_content.format(*tuple(contacts[contact_mail]))

    msg['From'] = address
    msg['To'] = contact_mail
    msg['Subject'] = subject

    msg.attach(MIMEText(msg_body, 'plain'))
    print(msg)
    mail.sendmail(address, contact_mail, msg.as_string())
    print("Sent Successfully!")

mail.quit()

Let's go through this line by line!

Line 1: We iterate through the emails in the contacts dictionary.

Line 2: Here's our first occurrence of MIMEMultipart(). This, as explained at the top, manages the content of the email according to a universal internet standard. There's more in-depth info here.

The following is an explanation of Line 3:

If you noticed in the contacts.txt file, the substitution is represented by a {0} . If you've done Python string formatting before, you may realize that character sequence allows for replacement with the .format() function!

  • We first obtain the list of substitutions with contacts[contact_mail] - this returns a list, as explained when we wrote the get_contacts() function
  • tuple is an immutable data type in Python. When we write *tuple(), we unpack the list of substitutions as arguments for the format() function
  • template_content.format() - since template_content is a string, we can use the format function to replace the {0} with custom arguments!

Lines 5-7: We set the message From, To, and Subject fields.

Lines 9-11: This code attaches the body text to the email in MIME format, and sends the email!

At this point, go ahead and run the program. If you're having problems, go check the final code up at the top of this page and make sure you didn't make any errors. Assuming your email and password are correct, and you let your application access your Google account, the program should run successfully! Go check the emails of the contacts you wrote down.

The result should be similar to this:

Screenshot of terminal output


Further Hacking

Congratulations GIF

There are many things that can be changed in this program. Go back and see what you can modify to make it your own! Because of the modularity of the project, to add another substitution, it's as simple as adding a {1} or a {2} in message.txt and adding more commas and arguments in contacts.txt. Try to think of ways to make the project even better, and please share your creations with me or others in the Hack Club community!

Here are some branches of the project that build upon the code (can also be found on GitHub)

  • Switching Mail Servers: demo and code
    • Easily send templated emails from multiple accounts and smtp servers!
  • Company Sponsor Email: demo and code
    • An example of how far you can go with this project! In retrospect, it has hundreds of usages.
  • Multiple Messages: demo and code
    • Multiple presets for emails, select one and send!

We'd love to see what you've made!

Share a link to your project (through Replit, GitHub etc.)