Pages: Welcome | Projects

Mutt, calendars and such

2019/11/9
Tags: [ Hacking ] [ Ideas ] [ tutorial ]

Invites to work meetings and such may be distributed via mail, often by means of a multi-part message enclosing a text/calendar file.

This type of file looks roughly like this:

BEGIN:VCALENDAR
METHOD:REQUEST
PRODID:Microsoft Exchange Server 2010
VERSION:2.0
BEGIN:VTIMEZONE
TZID:W. Europe Standard Time
BEGIN:STANDARD
DTSTART:16010101T030000
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010101T020000
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
ORGANIZER;CN=Sir Reginald:MAILTO:sir.reginald@dukes.org
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Freddy:MAILTO:mercury@queen.io
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Eddie:MAILTO:eddie@ironmaiden.com
DESCRIPTION;LANGUAGE=en-US:yada yada
SUMMARY;LANGUAGE=en-US:Got work to do
DTSTART;TZID=W. Europe Standard Time:20191115T143000
DTEND;TZID=W. Europe Standard Time:20191115T153000
UID:djfklsajfdklasjfklajsdafklsjkslddjfkasljf07C9193DA84D501000000000000000
CLASS:PUBLIC
PRIORITY:5
DTSTAMP:20191017T105343Z
TRANSP:OPAQUE
STATUS:CONFIRMED
SEQUENCE:0
LOCATION;LANGUAGE=en-US:Studios
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:REMINDER
TRIGGER;RELATED=START:-PT15M
END:VALARM
END:VEVENT
END:VCALENDAR

ICalendar is a common format, interpreted transparently by mainstream mail applications that embed a calendar management extension, while it looks close to gibberish to a human eye.

A convenient programmatic way to parse it could be to install a library such as icalendar (Python), and use it as a basis for a simple script like this:

#!/usr/bin/env python3

import icalendar
import sys

def print_cal(input_file, output_file):
    details = icalendar.Calendar.from_ical(input_file.read())
    for component in details.walk():
        if component.name != "VEVENT":
            continue

        summary   = component.get("summary")
        start     = component.get("dtstart").dt
        end       = component.get("dtend").dt
        uid       = component.get("uid")
        location  = component.get("location")
        attendees = component.get("attendee")

        print(
            f"{start.month}/{start.day}\t{summary}",
            f"\tfrom {start.hour:02d}:{start.minute:02d}",
            f"\tto {end.hour:02d}:{end.minute:02d}",
            f"\tlocation {location}",
            sep="\n", file=output_file
        )
        print(
            *(f"\tattendees {a}" for a in (attendees or [])),
            sep="\n", file=output_file
        )
        print(
            f"\t/* {uid} */",
            sep="\n", file=output_file
        )

if len(sys.argv) > 1:
    with open(sys.argv[1], "rt", encoding="UTF-8") as f:
        print_cal(f, sys.stdout)
else:
    print_cal(sys.stdin, sys.stdout)

Feeding the ICalendar file to this script results in a simple and clear representation:

11/15   Got work to do
        from 14:30
        to 15:30
        location Studios
        attendees MAILTO:mercury@queen.io
        attendees MAILTO:eddie@ironmaiden.com
        /* djfklsajfdklasjfklajsdafklsjkslddjfkasljf07C9193DA84D501000000000000000 */

Integration with mutt

Mutt, the mail user agent that sucks less, doesn't have a calendar feature. And why should it anyway? In true Unix fashion it does one thing and it does it well: it handles email messages.

On the other hand Mutt honours the ~/.mailcap file, which determines what specialized viewer to use depending on the file type. Under the default key bindings, attachment can be listed with the v key, and the viewer can be invoked by pressing the m key while the desired attachment is selected.

The Mailcap feature is documented thoroughly, so I'll just show a short excerpt from my own ~/.mailcap file:

$ more ~/.mailcap
image/jpeg; ristretto %s
application/pdf; qpdfview %s
application/rtf; libreoffice %s
--More--

The following is a very useful line to deal with those obnoxious HTML emails (many web pages are devoted to this subject):

text/html; lynx -dump -force_html -image_links -nopause -cfg ~/.config/lynx.cfg %s; copiousoutput;
--More--

And assuming that you saved the previous script as /usr/local/bin/parse-cal (or somewhere in your $PATH), here's how you get mutt to parse automatically the ICalendar file by means of it:

text/calendar; parse-cal %s; copiousoutput;
application/ics; parse-cal %s; copiousoutput;

NOTE: Don't forget the copiousoutput directive, or Mutt won't display the output correctly!

Pipe the file and store the result

Besides being obviously easier to understand at a glance, the output of parse-cal is in a format that can be understood by a classic Unix command named calendar(1).

To briefly quote its man page:

The calendar utility checks the current directory or the directory specified by the CALENDAR_DIR environment variable for a file named calendar and displays lines that begin with either today's date or tomorrow's.

Mutt can pipe the message or attachment to a shell commands: typing | results in a Pipe to: prompt that can be parametrised with a shell command.

Many Unix commands read the input from STDIN if no file name is provided as argument. Our parse-cal script is no exception, so the attachment can be piped to it, and the output can be appended in turn to the ~/.calendar/calendar file:

| parse-cal >> ~/.calendar/calendar

(In reality I actually prefer to just visualise the invitation, and manually annotate it into my calendar: I can often be more concise while mentally acknowledging the scheduled event).

Daily verification of future events

I instructed cron(1) to check my calendar every day at 9:00.

$ crontab -l | grep calendar
0 9 * * * /usr/bin/calendar -A2

If calendar(1) emits some output, cron(1) will turn it into my local mail, and I can get notified in many ways (e.g. check the bash(1) manual for the MAILCHECK environment variable).

And in case you are wondering: yes, I still manage to forget about meeting, sometimes.