Skip to content

iCloud CardDAV support (on top of #1) - #2

Open
HubEight wants to merge 15 commits into
anatosun:mainfrom
HubEight:fix/carddav-icloud-vcf
Open

HubEight wants to merge 15 commits into
anatosun:mainfrom
HubEight:fix/carddav-icloud-vcf

Conversation

@HubEight

@HubEight HubEight commented Sep 14, 2026

Copy link
Copy Markdown

This is based on #1 (the XML/namespace discovery for servers like SOGo). I kept that and added a few things that showed up when I pointed it at iCloud.

iCloud doesn't send a vcard content-type on the address book members. The files are just named .vcf. With only the content-type check, listing returned zero contacts even though the book was found. I treat .vcf hrefs as contacts as well.

Network to iCloud is a bit flaky from my setup (occasional "network unreachable" on a single GET). Those GETs are retried a couple of times instead of dropping the contact for that run.

bdaysync also never removed calendar events. If I delete the birthday on a contact, the event stayed in the calendar. There is now an opt-in cleanup, BIRTHDAY_DELETE_ORPHANS (default false). It drops birthday-* events whose contact no longer has that name and birthday, so a changed date replaces the old event as well. It only runs after a complete fetch: a failed address book listing, a failed vCard GET, an unparseable birthday or an empty contact list all skip it, so a blip doesn't wipe the calendar. bdaysync/test_sync.py covers these cases (cd bdaysync && python -m unittest test_sync).

I ran this against a real iCloud account: contacts come through, events show up in the birthday calendar, and removing the date on a test contact removed the event.

4350pChris and others added 10 commits August 12, 2026 04:12
PR #1 discovers addressbooks via DAV/CardDAV namespaces, then only
keeps members whose getcontenttype contains "vcard". iCloud omits that
property and names contacts *.vcf, so listing returned zero URLs.

Treat a vcard MIME type or a .vcf href as a contact, and skip collection
hrefs that end with a slash.
Build on push to main and fix/carddav-icloud-vcf, push to
ghcr.io/hubeight/bdaysync. Do not tag latest.
The previous commit replaced the repo workflow that was already
running on this branch via workflow_dispatch.
Trivy failed because it used HubEight (must be lowercase) and
fix/carddav-icloud-vcf (slash is invalid in a Docker tag). Use the
same sanitized tag docker/metadata-action publishes.
@HubEight
HubEight force-pushed the fix/carddav-icloud-vcf branch from 10a1a5c to 7db3ebd Compare September 14, 2026 16:51
iCloud lookups occasionally fail with ENETUNREACH (IPv6 without a
route). Retry the GET up to three times with a short backoff so one
blip does not drop a contact for the whole run.
After a complete CardDAV fetch, remove calendar events whose UID
matches birthday-{slug}-{YYYYMMDD} if that slug is not in the current
set of contacts that still have a birthday. Skip delete when any vCard
GET failed or the calendar listing is empty.
@HubEight HubEight changed the title fix: CardDAV namespace discovery plus iCloud *.vcf hrefs iCloud CardDAV support (on top of #1) Sep 14, 2026
The Trivy image-ref fix is for this fork's GHCR job (HubEight vs
hubeight, slashes in branch tags). It does not belong in a PR to
anatosun/bdaysync.
@HubEight HubEight closed this Sep 15, 2026
@HubEight
HubEight deleted the fix/carddav-icloud-vcf branch September 15, 2026 06:43
@HubEight
HubEight restored the fix/carddav-icloud-vcf branch September 15, 2026 07:12
@HubEight HubEight reopened this Sep 15, 2026
HubEight and others added 2 commits September 15, 2026 07:18
fetch_complete compared fetched vCards with listed ones, so a failed
addressbook listing (HTTP error, exception, broken XML) was never
counted and still yielded complete=True. Unparseable birthdays were
counted as fetched, and an empty contact list with a complete fetch
deleted every birthday event.

- Clear fetch_complete on any listing, download or parse failure;
  _parse_vcard now raises on unreadable data and returns None only
  when there is no BDAY. Listing PROPFIND gets a timeout.
- Restore main_sync returning False when no contacts were found, so
  orphan delete never runs with an empty set.
- Match orphans on name slug plus month/day, so a changed birthday
  replaces the old event instead of leaving a duplicate.
- Share the UID slug between event creation, lookup and orphan delete.
- The retry docstring blamed IPv6: urllib3 tries every resolved
  address and IPv4 sorts first, so ENETUNREACH is only the last error.

Add test_sync.py covering each case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Orphan delete removes calendar data, so it is opt-in. Wired like the
other BIRTHDAY_* settings: config, .env.template, docker-compose and
README, and logged with the event configuration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants