deaddabe

Sending Templated Emails Using Python and msmtp

Sometimes you need to send the same kind of email over and over. Especially in the corporate world, in order to follow processes for various needs. This burden can be automated quite easily by using an email template, filling it with Python and sending it with msmtp.

Finding and configuring servers

The hardest part, as anyone should have experienced in the corporate space, is to find a working IMAP server (for receiving) and SMTP server (for sending). This can be quite a challenge to figure out.

For example, Microsoft's Outlook 365 servers now support to use OAuth 2.0 in order to identify to both IMAP and SMTP servers. In the case of my current company's IT configuration, OAuth 2.0 is the only allowed method to identify to Microsoft's IMAP and SMTP servers (no more passwords).

This new authentication method is supported in the latest Thunderbird release. However, for less advanced email software, like CLI tools such as msmtp, the support involves some manual steps that are too long to describe here.

After everything is configured, I was able to send simple emails to myself (with no subject) using the following command:

$ echo hello | msmtp me@example.com

Writing down a template

Now that we have a working configuration for sending emails, we can etch out an email template that would apply to our needs. My current need is to ask for a room to come to the office as the COVID-19 pandemic is still running on. I need to follow this process by sending the request some days before I wish to come.

As so, here is the template that I came to, with headers in order to set a subject as well as sending a copy to myself to acknowledge that the script worked:

To: %MANAGER_EMAIL%
Cc: %MY_EMAIL%
Subject: Coming physically on %DATE%

Hi %MANAGER%,

Can you please book me a physical room in order to come on %DATE%?

Thanks and have a nice day.

%SIGNATURE%

Printing the date like an human would

In order to not make this email feel like it was automatically sent, the date should be printed in my current locale's native language instead of an ISO date.

>>> from datetime import date
>>> today = date.today()
>>> today.strftime("%A %d %B")
'Wednesday 06 October'
>>> import locale
>>> locale.setlocale(locale.LC_TIME, "")
'fr_FR.UTF-8'
>>> today.strftime("%A %d %B")
'mercredi 06 octobre'

Python will not by default use my system's locale for some reasons, so we need to call locale.setlocale(). By setting LC_TIME to an empty string, it seems to use the system's locale instead of the English default.

Note that the leading zero before the day number may seem suspicious, but at this point I was too lazy to look back in the Python documentation what formatting character to use to get rid of it.

Also note that there is probably a formatting character that could print the three fields in this order, because this is the standard, natural date format that we learn at school and use in everyday conversations.

Putting it together

We can now put everything together inside a single Python file. The template could be in a separate file, but for portability reasons (and sharing it with colleagues!) I wanted to make the script standalone.

Note that the templating system is composed of basic text.replace() calls. Indeed, using Jinja could feel like an overkill solution. Moreover, relying on an external dependency may make the script distribution a lot harder by involving virtualenvs and pip.

Here is how I use the script:

$ ./coming_to_office.py 2021-10-07 | msmtp -t

The most complicated part (email sending) is handled by a simple pipe to msmtp, instead of involving any library. This allows to review the generated content by just removing the pipe and inspecting stdout directly.

The -t option of msmtp allows to read the recipients list directly from the passed input:

-t, --read-recipients
       Read recipient addresses from the To, Cc, and Bcc headers
       of the mail in addition to the recipients  given  on  the
       command  line.   If any Resent- headers are present, then
       the addresses from any Resent-To, Resent-Cc, and  Resent-
       Bcc  headers  in  the  first block of Resent- headers are
       used instead.

Here is the final script that will conclude this article:

#!/bin/env python3

import sys
import datetime


# Personal variables

KEYS = {
    "MY_EMAIL": "me@example.com",
    "MANAGER": "Alice",
    "MANAGER_EMAIL": "alice@example.com",
    "SIGNATURE": """
--
Me
Software Developer
Foobar Corporation
""".strip(),
}


# Template

TEMPLATE = """
To: %MANAGER_EMAIL%
Cc: %MY_EMAIL%
Subject: Coming physically on %DATE%

Hi %MANAGER%,

Can you please book me a room in order to come on %DATE%?

Thanks and have a nice day.

%SIGNATURE%
""".strip()

def usage():
    print("usage: {} ISODATE | msmtp -t".format(sys.argv[0]))
    print("example: {} 2021-10-07 | msmtp -t".format(sys.argv[0]))
    print()
    print("Remember to check the script output before really sending.")
    sys.exit(1)

def main():
    if len(sys.argv) != 2:
        usage()

    date = datetime.date.fromisoformat(sys.argv[1])

    text = TEMPLATE
    text = text.replace("%DATE%", date.strftime("%A %d %B"))
    for k, v in KEYS.items():
        text = text.replace(f"%{k}%", v)

    print(text)

if __name__ == "__main__":
    from locale import setlocale, LC_TIME
    setlocale(LC_TIME, "")
    main()