Generating birthday calendars

I recently switched from using Nextcloud for managing contacts and calendars to Radicale. Radicale is much easier to configure and maintain than a Nextcloud instance but there was one feature missing: automatically generating birthday calendar events from the contants. Since both the vCard and iCalendar formats are plain-text it shouldn't be too difficult to generate them using a script.

Reading through the Wikipedia article for vCard, I saw a very useful table showing all available vCard properties. For vCard 3+ the formatted name (FN) property is required so that can be reliably used as the name of the calendar event. The other property of interest is of course the birthday date (BDAY). Looking at some of the vCard files for my contacts I noticed that the date of the BDAY property occasionally has dash separating its components. Something interesting I noticed is that for contacts whose birth year isn't known, it's set to 1604. It would be interesting to learn why.

For the iCalendar format the Wikipedia article wasn't that helpful but the RFC was. The online validator was invaluable in testing the resulting files.

After a little trial and error I came up with the following shell script:

#!/bin/sh
# SPDX-FileCopyrightText: 2022 Sotiris Papatheodorou
# SPDX-License-Identifier: CC0-1.0
set -eu

# Usage: vcard_to_icalendar FILE DIR
# Create an iCalendar file from the vCard file FILE and
# save it in DIR.
vcard_to_icalendar() {
	# Print the formatted name and birthday in the YYYYMMDD format
	# separated by a tab.
	data=$(awk '
		BEGIN { FS = ":" }
		/^FN/ { name = $2 }
		/^BDAY/ { bday = $2; gsub("-", "", bday) }
		END { if (name && bday) printf "%s\t%s\n", name, bday }
		' < "$1")
	# Skip vCard files without a birthday and/or name.
	if [ -z "$data" ]
	then
		return
	fi
	name=$(printf '%s\n' "$data" | cut -f 1)
	bday=$(printf '%s\n' "$data" | cut -f 2)
	# Create a unique ID that remains the same between runs.
	progname=${0##*/}
	uid=$(uuidgen --md5 --namespace '@oid' --name "$progname$name$bday")
	{
		printf 'BEGIN:VCALENDAR\n'
		printf 'VERSION:2.0\n'
		printf 'PRODID:-//Sotiris Papatheodorou//%s//EN\n' "$progname"
		printf 'BEGIN:VEVENT\n'
		printf 'UID:%s\n' "$uid"
		printf 'DTSTART;VALUE=DATE:%s\n' "$bday"
		printf 'DURATION:P1D\n'
		printf 'DTSTAMP:%s\n' "$(date -u '+%Y%m%dT%H%M%SZ')"
		printf 'RRULE:FREQ=YEARLY\n'
		printf 'SUMMARY:%s 🎂\n' "$name"
		printf 'BEGIN:VALARM\n'
		printf 'ACTION:DISPLAY\n'
		printf 'TRIGGER:P0D\n'
		printf 'DESCRIPTION:%s 🎂\n' "$name"
		printf 'END:VALARM\n'
		printf 'END:VEVENT\n'
		printf 'END:VCALENDAR\n'
	} > "$2/$uid.ics"
}

if [ "$#" -ne 2 ]
then
	printf 'Usage: %s INDIR OUTDIR\n' "${0##*/}"
	printf 'Generate birthday calendars inside OUTDIR for\n'
	printf 'all contacts inside INDIR and its subdirectories.\n'
	exit 2
fi

mkdir -p "$2"
# Find all vCard (.vcf) files in INDIR and its subdirectories.
find "$1" -type f -name '*.vcf' | while read -r file
do
	vcard_to_icalendar "$file" "$2"
done

I created a new calendar from the Radicale web interface, set the script to run on a daily cron job and that was it!

Sotiris 2022/04/04

Nextcloud

Radicale

vCard

iCalendar RFC

iCalendar validator

Updates