Back to writing

Sending emails to myself (¶)

In April, I wrote operatornotify.py, a module which allows my programs to notify me of important information or errors. I was immediately very proud of it. It felt awesome to get so much impact out of a sub-100 line module as I went around hooking it up to my existing programs and getting the tracebacks from my horribly broken code delivered right to me.

Since then, I have figured out which parts of the interface are most important, and boiled them down into a single decorator, so that adding @operatornotify.main_decorator is all it takes to get emails from a program. It's probably the best bang-for-buck code I've written in a while.

@operatornotify.main_decorator(subject='myprogram.py')
def main(argv):
    ...

if __name__ == '__main__':
    raise SystemExit(main(sys.argv[1:]))

The most important part of operatornotify's design is that it takes advantage of your existing logging infrastructure. Although you can call operatornotify.notify whenever you want, it's much easier to add a log handler that captures WARNING level messages automatically. This has encouraged me to revisit some older programs that just used print statements and replace them with proper logging, which makes them easier to use and debug in general.

There are a few pain points that arise from using operatornotify as a log handler. For example:

But these inconveniences are vastly outweighed by the ability to add a single decorator function that immediately supercharges all of the existing log lines and encourages me to add better logging where I skimped on it earlier.

Then I took the next step towards this summit of email enlightenment: running the mail server myself. I figure there are three reasons I should self-host the mail for operatornotify:

  1. The notifications usually contain private information or filepaths that I don't like to have sitting on a third-party server. I don't think these worries are justified since my email provider does at-rest encryption and uses SSL, but I just don't like it.

  2. My code might wind up sending hundreds of emails in a row, and I don't want to do that to a third-party host.

  3. It's a good chance for me to play with stuff I haven't tried before.

I'm using the Maddy email server, which is an all-in-one solution for both SMTP and IMAP. I also considered Mail-in-a-Box, but their documents specifically say that it's designed to have the whole machine to itself, which I don't want to do. The most widely known software is Postfix for SMTP and Dovecot for IMAP, but I started drowning in the configuration of dovecot before even touching postfix, and gave up. I'm just a single user emailing myself. I like that maddy is just a single binary. So now I'm free to send myself as many thousands of emails as I want, containing whatever I want, using my own code and connecting to my own server.

FairEmail on Android supports IMAP IDLE, so I can get tracebacks anytime, anywhere. It's great.

Here are some ways I'm using operatornotify:

For the record, the code that actually sends the emails is in my my_operatornotify.py. The basic code you need is:

import email.message
import smtplib

def send_email(subject, body=''):
    sender = 'xxx'
    recipient = 'xxx'

    subject = str(subject)
    body = str(body)

    message = email.message.EmailMessage()
    message.add_header('From', sender)
    message.add_header('Subject', subject)
    message.add_header('To', recipient)
    message.set_payload(body, charset='utf-8')

    server = smtplib.SMTP_SSL('xxx', 465)
    server.login('username', 'password')
    server.send_message(message)
    server.quit()

def notify(subject, body=''):
    ...
    send_email(subject, body)
    ...

But I have some additional stuff in there to deal with retrying and eventually writing the message to my desktop if the mail server is unreachable.

To write the system heartbeat, I'm parsing the output of schtasks with the subprocess module:

import csv
import subprocess
from voussoirkit import winwhich

command = [
    winwhich.which('schtasks'),
    '/query',
    '/tn', '\\voussoir\\',
    '/fo', 'csv',
    '/v'
]
output = subprocess.check_output(
    command,
    stderr=subprocess.STDOUT,
    creationflags=subprocess.CREATE_NO_WINDOW,
)
output = output.decode('utf-8')
lines = output.splitlines()
reader = csv.DictReader(lines)
for line in reader:
    ...

[1] I wanted to use a checkmark, but the fonts on Android make all of the unicode check marks look stupendously ugly. And Android doesn't let me change the system font outside of Daddy Google's approved list because I'm a big stupid dummy baby who can't be trusted with a ttf because I might get a boo-boo.

[2] I am using a stopgap solution in my_operatornotify that detects the phrase "Program finished, returned 0" to add the ⭕︎.


View this document's history