Sending Automated Email Alerts with Python and Cron on EC2

Posted on June 05, 2020

What if I told you that you could send yourself spam mail? Wouldn't that be great?!?!

All kidding aside, it is sometimes really helpful to setup email alerts programmatically. In a professional setting, I have used this in situations where you need to alert yourself if something happens in your data warehouse, such as when a store's inventory is equal to zero or your conversion rate falls below a certain percent. The best way to do this is with python's smtplib package and cron.

For this example, I'm going to setup a program that sends me an email any night the Rangers are playing. I'll include some stats for good measure on both teams so I don't have to google that stuff when I get the email.

TL;DR: The code for this program can be read here if you speak python.

For the courageous few of you braving on, let's start by writing functions that scrape the Ranger's schedule, filter that data for today's game, and then scrape's each team's record. I included a global variable _rangers to avoid spelling errors when repeatedly typing the full team's name.

_rangers = 'New York Rangers'

def scrape_rangers_schedule_df():
    url = 'https://www.hockey-reference.com/teams/NYR/2020_games.html'
    df = pd.read_html(url)[0]

    """ filter out rows that are column header rows within the table """
    mask = df.loc[:, 'GP'] != 'GP'
    df = (df.loc[mask, ]
        .assign(Date=lambda x: pd.to_datetime(x.Date))
        .rename(columns={'Unnamed: 3': 'Home/Away'})
    )
    assert(df.shape[0] == 82)

    return df

def filter_game_data(df, testing=False):
    date_val = dt.datetime(2020, 4, 2) if testing else dt.datetime.today()

    dt_mask = df.Date == date_val
    df_date_filtered = df.loc[dt_mask, ]

    if not df_date_filtered.empty:
        game_data = df_date_filtered.to_dict(orient='record')[0]
        return game_data

def scrape_team_data(opponent):
    url = 'https://www.hockey-reference.com/leagues/NHL_2020_standings.html#all_standings'
    df = (pd.read_html(url, attrs = {'id': 'standings'})[0]
        .rename(columns={'Unnamed: 1': 'Team'})
        .loc[:, ['Team', 'Overall']]
        .set_index('Team')
    )
    teams = [_rangers, opponent]
    team_records = (df.loc[teams, ]
        .to_dict()
        ['Overall']
    )

    return team_records

We'll run the first two in our main function like this:

df = scrape_rangers_schedule_df()
game_data = filter_game_data(df, testing=testing)
if game_data is None:
    return

If there's no game data, we hit return, which basically means our program ends since there's no game that night. Next let's setup our email parameters.

port = 465
smtp_server = "smtp.gmail.com"

message = MIMEMultipart("alternative")
message["Subject"] = "Rangers Game Tonight!"
message["From"] = sender_email
message["To"] = receiver_email

I'm using gmail for my smtp server (smtp stands for Simple Mail Transfer protocol btw) and the standard gmail port. We've also set the subject and the sender_email and receiver_email here, which are pulled from a config python file. We're using a MIMEMultipart email format, which basically sends a rich-text (aka an email with rendered html) email if a receiver accepts that type, and a plain text email if not. Think of this instance of the MIMEMultipart object as the draft of our email. We now need to setup both the html and text templates (aka the email body).

For each of these I created seperate files, which can be found on my Github here for the html and here for the text file). I read these into python and then created jinja2 template object from the strings.

text_template_str = open('rangers_email.txt').read()
text_template = Template(text_template_str)
html_template_str = open('rangers_email.html').read()
html_template = Template(html_template_str)

These jinja2 objects allow us to do fancy string interpolation into each template object. Let's scrape data for each teams record and then interpolate that data into both our templates.

opponent = game_data['Opponent']

team_records = scrape_team_data(opponent)

template_kwargs = dict(
    opponent=opponent,
    time=game_data['Time'],
    rangers_record=team_records[_rangers],
    opponent_record=team_records[opponent],
)
text = text_template.render(**template_kwargs)
html = html_template.render(**template_kwargs)

The html and text variables now contain the templates with data for the opponent, time of the game and team record's. Our final step for these two templates is to turn them into MIMEText objects and attach them to our MIMEMultipart object.

part1 = MIMEText(text, "plain")
part2 = MIMEText(html, "html")
message.attach(part1)
message.attach(part2)

At this point we basically have our email drafted. We'll now setup an ssl (Secure Sockets Layer) connection with gmail. This encrypts our email so non-Ranger fan hackers can't see who we're playing. Then we'll use this ssl pipe to connect to the gmail server and send the email.

# Create secure connection with server and send email
context = ssl.create_default_context()

with smtplib.SMTP_SSL(smtp_server, port, context=context) as server:
    server.login(sender_email, sender_email_password)
    server.sendmail(
        sender_email, receiver_email, message.as_string()
    )

Boom! Our program is ready to run. Onto setting it up to run daily on cron.

Before running it on cron we'll need to make it an executable. First, add the shebang for your python interpreter to the program. Then use chmod to make the program executable using the below command (notice the x in the second output of the ll command).

> ll | grep rangers_email.py
-rw-rw-r-- 1 bf2398 bf2398   4682 Jun  5 12:38 rangers_email.py
> chmod u+x rangers_email.py
> ll | grep rangers_email.py
-rwxrw-r-- 1 bf2398 bf2398   4682 Jun  5 12:38 rangers_email.py*

Finally add the following line to your crontab using the crontab -e command.

0 8 * * cd /home/bf2398/Documents/Github/side_projects/Other/ && ./rangers_email.py

This tells cron to run the command on the 8th hour of everyday. The command first switches to our directory and then runs the actual script. It's important to be in the actual directory of the script, otherwise we won't have access to our template objects. The ouptut of the email will look like this:

I added a Rangers logo cause I'm fancy like that. Hopefully this tutorial sparks lots of ideas on how you can use email alerts for your business.

Thanks for reading!