First published 2025-11-02.
I’ve been working for over a year now on improving Emacs’ iCalendar support. It’s been a while since I posted (Part 1, Part 2, Part 3) and in the meantime I’ve turned my parser into a full implementation that handles not just diary import and export, but can be reused by any Emacs Lisp code that needs to handle iCalendar data. The library has grown quite a lot, but I am now confident that it has everything it needs to be useful as a library, and that the code will be a lot more maintainable in …
First published 2025-11-02.
I’ve been working for over a year now on improving Emacs’ iCalendar support. It’s been a while since I posted (Part 1, Part 2, Part 3) and in the meantime I’ve turned my parser into a full implementation that handles not just diary import and export, but can be reused by any Emacs Lisp code that needs to handle iCalendar data. The library has grown quite a lot, but I am now confident that it has everything it needs to be useful as a library, and that the code will be a lot more maintainable in the long term. I’m thus preparing a patch that I hope will be merged into the next major release of Emacs.
This has turned out to be a bigger project than I thought it would be, but I’m proud of the results. Here’s an overview of what I’ve done since Part 3:
Parsing real world data
Once I had the parser compliant with RFC5545, I also needed to make it handle data from the real world. To do this, I tested the parser against all the real iCalendar data I could get my hands on: email attachments from my own email archive.
Since I have a local copy of all my mail and I use Notmuch to index it, I wrote a quick Python script to grab all these attachments and save them as individual files:
import os, hashlib, notmuch
def main():
ics_dir = "/some/output/directory/"
ics_files = []
db = notmuch.Database()
query = db.create_query("tag:attachment and ('.ics' or 'text/calendar')")
for mail in query.search_messages():
for part in mail.get_message_parts():
ct = part.get_content_type()
fn = part.get_filename()
if (ct == 'text/calendar' or str(fn).endswith(".ics")):
bytes = part.get_payload(decode=True)
file = hashlib.sha1(bytes).hexdigest() + ".ics"
with open(ics_dir + file, "w+b") as sink:
sink.write(bytes)
if __name__ == '__main__':
main()
I then ran the parser over all these files to find out where it would have problems.
I found that the most common error was improper escaping in text values. Unescaped commas in addresses were particularly prominent. (Commas need to be escaped in text because they are sometimes used as separators in lists of text values.) So I added a special case to the parser to loosen up text parsing when unescaped text creates no ambiguities.
Other than that, my personal data showed that different implementations make different mistakes; there were no errors that were so common that they were worth adding further exceptions to the parser. Instead, I added some hooks to the parser so users can write their own functions to clean up bad data before it’s parsed, and an option to parse strictly according to 5545, mostly for debugging purposes.
Dealing with real world data also requires signaling errors in the right place in the parser, and handling them appropriately. I added a simple error handling framework to icalendar.el based on compilation-mode, and added error handling to the parser so that parsing can continue at the next place that makes sense. A bad parameter value won’t prevent the parser from parsing the rest of a property; a bad property won’t prevent parsing the rest of a component; and so on.
All of this should make it pretty easy for Emacs users to make use of iCalendar data that isn’t perfectly RFC-compliant. It will also, I hope, provide enough quality-of-life improvements for developers that they can improve both Emacs’ and other implementations. Combined with the syntax highlighting in icalendar-mode (see Part 1), the debugging features I’ve added to the parser should make Emacs a great choice for anyone working on any iCalendar implementation.
Diary import and export
Once I had all that working, I was finally ready to tackle the job that icalendar.el does: converting between iCalendar data and Emacs diary entries. I decided to reimplement this from the ground up, because in my opinion the code in icalendar.el is just too difficult to work with and not worth saving.
My main goal here was to make both import and export more flexible. I also fixed a number of inconsistencies and gotchas along the way, like the fact that there was previously no way to control the date format used for importing to the diary, even though the diary itself allows great flexibility in date formats.
I decided to switch to skeleton.el templates to make it easier for users to customize the import format. Now, instead of customizing a hierarchy of inflexible format strings, you can write an import template like this:
(define-skeleton my-event-skeleton
"Just the date, time, summary, and location on one line"
nil
date & " " & start-to-end & " " & summary & (when location " @ ") & location
"\n")
(setq diary-icalendar-vevent-skeleton-command #'my-event-skeleton)
which will import events to your diary like:
2025/11/2 10:30AM Brunch @ The Restaurant
Since a skeleton is just an Elisp function, you have all the power of Elisp available in such templates. You could e.g. dispatch to other skeletons depending on the event data, or apply filters to certain properties based on the sender.
I’ve added a lot more customization variables in the diary-icalendar group that provide control over import and export, and I’ve added support for iCalendar’s VTODO and VJOURNAL components. I’ve also made it possible to #include an iCalendar file from your diary file, so that you can mark and display its events in the calendar on the fly, without having to import them to the diary first. This will be useful if e.g. you want to mainly keep your calendar data on a server somewhere, but download it to a local file periodically for display in Emacs.
I decided that idempotent export and reimport of diary entries was not one of my design goals. The diary format is far too flexible and underspecified to make this a reasonable goal. You can pretty much write diary entries however you like, in free form text, as long as there is some kind of date at the beginning. The diary also has no syntax for time zones. It’s thus basically impossible to roundtrip text from diary to iCalendar without either imposing further constraints on the user or losing data.
That means one cannot expect to use the diary export and import to sync with an external calendar server. But being able to #include an iCalendar file in the diary ameliorates this: you don’t have to sync between the formats; you can view the iCalendar data in the diary and calendar in Emacs without syncing.
Despite these changes, I was careful to prioritize backward compatibility. Users who are already happy with their icalendar.el setup can continue using it, and they should also be able to switch to the new implementation without changing their setup (just use the new diary-icalendar-* commands corresponding to the old icalendar-* commands, e.g. diary-icalendar-import-buffer). I also ported the existing icalendar.el tests to the new implementation, so the new implementation should handle all edge cases that icalendar.el already did, and a lot more.
What’s next
I still have to rebase against master and prepare a patch, which hopefully I’ll submit that to the original bug report sometime later today. I’m planning to submit the code for feedback from the maintainers, and work on updating the manual and the NEWS file while I’m waiting.