I’ve long wondered how an application that uses email as its interface might be built. I got all the clues I needed after setting up Postfix and Mailman on my VPS.

The magic is in the alias data Postfix uses to map email recipients to users on the destination system. Allow me to disclaim that this is very ad-hoc and I’m sure there are better ways to do this. Think of this as a prototype.

There are four pieces to this:

  1. A Postfix alias for my program.
  2. A program to parse an inbound email and take an appropriate action.
  3. A program to send out an expense report when requested.
  4. A configuration for msmtp which sends out the email.

The implicit rule for any incoming email looks like this:

dave: dave

This is an alias that tells Postfix to deliver emails sent to dave@example.com to the user dave on the local system. I have out-of-the-box aliases set up in my /etc/aliases file:

# /etc/aliases
mailer-daemon: postmaster
postmaster: root
nobody: root
-snip-

When setting up Mailman, I noticed aliases that looked like this:

list-subscribe: "|/var/lib/Mailman/bin/subscribe"

The file at /var/lib/Mailman/bin/subscribe is a binary executable. This alias pipes the content of the inbound email to this program for processing. That’s all there is to it. It’s just a pipe. How Unixy!

Now I just need to create a program that will parse an inbound email and do something with it. I track the expenses that are shared among the members of my household. At the end of the month, I calculate the difference and settle up. I do this by pocketing receipts when I make purchases and entering the amount into a text file along with the date and payee. It would be nice to be able to do this by sending in an email with the pertinent information. It would also be nice if I could request a report of all the expenses recorded so far including a total.

The alias for Postfix looks like this:

expense: "|awk -f /usr/lib/expense/expense.awk -vdate=$(date +'%Y-%m-%d')"

This tells Postfix to pipe the inbound email message to awk, specifies my expense.awk script, and passes in a variable called date set to the current date. Here are the permission settings for the script:

$ls -l /usr/lib/expense

-rw-r--r-- 1 root root 578 Aug 26 16:01 expense.awk

And here is the script itself:

BEGIN {
  boundary_seen = 0 
  boundary = "^$"
  line = ""
}
(boundary_seen == 1) && ($0 !~ /^$/) {
  line = $0
  exit 
}
/^From:/ {
  $1 = ""; from = $0 
  sub(/^.*</,"",from)
  sub(/>.*$/,"",from)
}
/^Content-Type/ {
  boundary = "^Content-Type: text/plain;"
}
$0 ~ boundary {
  boundary_seen = 1 
}
END {
  if (line ~ /^[0-9]/) {
    split(line,values)
    amt = values[1]
    payee = values[2]
    printf "%s %s %s %s\n", date, amt, payee, from >> "/home/dave/expense"
  }
  if (line ~ "[Tt]otal") {
    system ("awk -f/usr/lib/expense/total.awk /home/dave/expense |msmtp " from)
  }
}

This script parses out the relevant line from the inbound email and determines whether it’s an expense that’s being submitted or if it’s a request for an expense report. This is a bit complicated because, depending on the mail client, the inbound message may or may not have a multipart attachment. Mutt produces a normal email message with the body separated from the headers by a blank line. The mobile Gmail client, however, is compelled to provide an HTML version of even the simplest message. The sequence of the patterns and actions is important, too. We don’t want to test (boundary_seen == 1) until the line after we see the boundary. That’s why this test comes first.

In the case that the email body looks like this:

44.23 Whole Foods

expense.awk will create the following line in /home/dave/expense:

2018-08-26 44.23 Whole Foods dave.bucklin@mail.com

After adding a few expense items to the list, I may want to get a report of all the items in the list along with a total. To do this, I send an email to expense@example.com with the word “Total” in the body. The expense.awk script will shell out and run the total.awk script and pipe the output to msmtp. Here’s the total.awk script:

BEGIN {
  total = 0
}
{ total += $2; print }
END {
  print "Total: " total
}

Simple, right? This could almost be a one-liner.

The msmtp configuration gave me the most trouble. Initially, I just copied my ~/.msmtprc to /usr/lib/expense/ and specified this config for msmtp with the C option. I could get everything to work when running it from the console, but I got a permissions error whenever I ran the request through Postfix. It turns out that the right(er) thing to do is to create a system-wide msmtp config in /etc/msmtprc. I copied my personal config to this location and things started working just fine.

With this pattern established, there are a several application patterns that can be applied.

  • Log expenses, calories, life events, to-dos
  • Gateway to gopher, dict, the web, twtxt, RSS
  • Form delivery and processing
  • Email as CMS front end

Happy hacking!