Keeping contact dates in my own CalDAV calendar
by Timo FurrerOne thing I missed after moving my calendar and contacts to Migadu was a birthday calendar. Google builds one from your contacts. Migadu runs sabre/dav (see my post on auto-discovery), which doesn't.
So I wrote carddav-events-calendar. It reads the dates from my contacts and keeps a normal CalDAV calendar in sync with them.
What it does
Once a day it fetches all contacts over CardDAV and writes one yearly recurring all-day event per date. That's not only birthdays: anniversaries, dates of death and the "other" or custom-labelled dates Android and DAVx5 store, like a name day, all end up in the calendar.
The description of each event names the contact and the address book it came from, so I know exactly where things came from. The tool only ever touches events it created. Delete a contact and its events are gone on the next run.
From vCard to event
A contact as DAVx5 stores it looks like this:
BEGIN:VCARD
VERSION:3.0
UID:3f6c2a1e-8b4d-4c1a-9e2f-5d7b8a9c0e12
FN:Jane Doe
BDAY:1984-03-12
item1.X-ABDATE:2015-06-20
item1.X-ABLabel:Name day
END:VCARD
BDAY, ANNIVERSARY and
DEATHDATE are standard properties. Everything else
ends up as an X-ABDATE, paired with its label through
the item1. group prefix. Dates without a year
(--03-12) work too.
Groups are separate vCards that list their members by UID:
BEGIN:VCARD
VERSION:3.0
X-ADDRESSBOOKSERVER-KIND:group
FN:Family
X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:3f6c2a1e-8b4d-4c1a-9e2f-5d7b8a9c0e12
END:VCARD
The birthday above becomes this event (shortened):
BEGIN:VEVENT
UID:505044f0…7dc495@carddav-events-calendar
DTSTART;VALUE=DATE:19840312
DTEND;VALUE=DATE:19840313
RRULE:FREQ=YEARLY
SUMMARY:Jane Doe's birthday
DESCRIPTION:Birthday of Jane Doe: 12 March 1984\n\nContact: Jane Doe\nAddress book: Personal
TRANSP:TRANSPARENT
X-CARDDAV-EVENTS-CALENDAR-SYNC:personal
X-CARDDAV-EVENTS-CALENDAR-HASH:777df13d…b9700507
X-CARDDAV-EVENTS-CALENDAR-CONTACT-UID:3f6c2a1e-8b4d-4c1a-9e2f-5d7b8a9c0e12
X-CARDDAV-EVENTS-CALENDAR-ADDRESSBOOK:Personal
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER:-P1W
END:VALARM
END:VEVENT
The UID is a hash of the sync name, the contact UID, the kind, the
label and the date, so the same date always maps to the same event, and renaming
a contact updates it instead of creating a new one.
SYNC marks the events the tool owns. HASH
covers the content, so a run only writes when something actually
changed. Most days nothing does.
A birthday on February 29 gets
RRULE:FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1, which puts
it on the 28th in other years.
Why not a built-in birthday calendar
Some servers generate one for you. Where they do, you get what the server decides: usually only birthdays, read-only, and little say over reminders.
Here it's an ordinary calendar in my own account. I picked its name and colour, and it syncs to every client like any other calendar. I choose which kinds of dates and which address books end up in it, and per contact group which colour and how many reminders the events get. My family's birthdays remind me a week, two days and a day before:
[[syncs]]
name = "personal"
[syncs.carddav]
url = "https://cdav.migadu.com"
username = "timo@furrer.life"
password_file = "$CREDENTIALS_DIRECTORY/personal-carddav"
[syncs.caldav]
url = "https://cdav.migadu.com"
username = "timo@furrer.life"
password_file = "$CREDENTIALS_DIRECTORY/personal-caldav"
calendar = "Contact dates"
[[syncs.rules]]
groups = ["Family"]
kinds = ["birthday"]
alarms = ["-P1W", "-P2D", "-P1D"]
Nothing in it is specific to Migadu. Any CardDAV and CalDAV server should do, and they don't have to be the same one.
Running it
By default it only logs what it would change. -apply
writes, and -purge removes all of its events again,
which is handy when moving to another calendar:
carddav-events-calendar -config config.toml # dry run
carddav-events-calendar -config config.toml -apply # write
I run it on a Raspberry Pi as a systemd timer, with the passwords stored as encrypted systemd credentials. The deploy README has the details.