Skip to content

Add calendar (CalDAV) and contacts (CardDAV) support (#59) - #133

Open
a-schild wants to merge 6 commits into
mainfrom
feature/59-caldav-carddav
Open

Add calendar (CalDAV) and contacts (CardDAV) support (#59)#133
a-schild wants to merge 6 commits into
mainfrom
feature/59-caldav-carddav

Conversation

@a-schild

@a-schild a-schild commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Implements the long-standing request in #59 for calendar access, and the address book support asked for in its comments.

Try it without merging: this branch is published as 14.3.0-SNAPSHOT to the Central Portal snapshot repository — see Trying a pre-release snapshot in the README.

API

All methods are exposed on NextcloudConnector:

Calendars Address books
listCalendars() listAddressBooks()
getCalendarEntries(cal) getContacts(book)
getCalendarEntriesInRange(cal, from, to)
getCalendarEntriesInRange(cal, from, to, expand)
getCalendarEntry(cal, name) getContact(book, name)
putCalendarEntry(cal, name, ics[, etag]) putContact(book, name, vcf[, etag])
deleteCalendarEntry(cal, name) deleteContact(book, name)
createCalendar(name, displayName, colour) createAddressBook(name, displayName, description)
deleteCalendar(name) deleteAddressBook(name)

Design notes

  • Raw payloads, no new dependencies. CalendarEntry.getData() / Contact.getData() return the iCalendar/vCard text verbatim. Parsing would have meant adding ical4j and ez-vcard as compile-scope dependencies for every consumer and tying the public API to their model classes. pom.xml gains no dependency.
  • Shared plumbing. CalDAV and CardDAV are structurally identical, so collection listing, entry CRUD and collection creation live in ADavCollectionHandler; Calendars and AddressBooks only supply the namespaces, element names and content types that differ.
  • Recurrence expansion is opt-in. getCalendarEntriesInRange(..., true) adds the CalDAV <C:expand> element, so the server resolves a recurring event into one VEVENT per occurrence in the range (no RRULE, each with its own RECURRENCE-ID, overrides applied, times in UTC). The javadoc warns that an expanded result is a computed view of that range and must not be written back. The 3-argument overload is unchanged and returns events as stored.
  • REPORT handling. Sardine models plain WebDAV properties and drops the calendar-data/address-data payloads, so REPORT responses are read by a small StAX MultistatusParser, hardened against XXE the same way XMLAnswerParser is. MKCALENDAR and extended MKCOL use a DavMethod on raw HttpClient, following the SystemTags precedent.
  • One round trip per collection. Listing a collection is a PROPFIND to enumerate it plus a single multiget REPORT for the payloads, not one GET per entry.
  • Correct user id. DAV paths use the internal user id from the provisioning API, not the login name — the mismatch flagged in the 2021 discussion on access to ics files. #59.
  • Concurrency. The put* overloads taking an etag send If-Match, so a change made in the meantime fails loudly instead of being overwritten.
  • Path safety. Collection and entry names go through requireValidPathSegment and are percent-encoded, consistent with the 14.2.0 hardening.

Incidental changes

  • authorizationHeader(), buildSyncClient() and shutdownSardine() moved from SystemTags up into AWebdavHandler rather than being duplicated by the new connectors.
  • NextcloudApiException gained a (String message, Throwable cause) constructor, so failures can carry both context and cause.

Tests

152 tests pass in CI against the Testcontainers Nextcloud server, 24 of them new:

  • MultistatusParserTest — 8 unit tests, no server needed (payload extraction, weak etags, 404 propstats, XXE, malformed XML).
  • TestCalendars — 9 integration tests: create calendar → put → get → list → range query → recurrence expansion (a FREQ=DAILY;COUNT=3 event asserted as 1 VEVENT with RRULE unexpanded, and 3 VEVENTs with RECURRENCE-ID and no RRULE expanded) → etag precondition → delete → delete calendar.
  • TestAddressBooks — 7 integration tests over the same lifecycle.

Two bugs were caught by these tests rather than by users: an empty <calendar-data/> in a 404 propstat created phantom entries, and collection listing returned nothing because a targeted PROPFIND does not return resourcetype unless it is explicitly requested.

Version bumped to 14.3.0-SNAPSHOT (new feature, backwards compatible).

Closes #59

🤖 Generated with Claude Code

a-schild and others added 6 commits August 11, 2026 10:28
Calendars and address books are reachable below remote.php/dav/, but the
library had no way to talk to them: the path resolver carried CALDAV and
VCARD types that nothing used.

Both protocols are structurally identical - a per-user home collection of
collections holding text resources - so the shared plumbing lives in
ADavCollectionHandler and the two connectors only supply the namespaces
and element names that differ.

Entries are exchanged as raw iCalendar/vCard text. Parsing them would mean
taking on ical4j and ez-vcard as compile dependencies and tying our public
API to their model classes, so that is left to the caller.

Sardine models plain WebDAV properties and drops the calendar-data and
address-data payloads, so the REPORT responses are read with a small StAX
reader, hardened against XXE like XMLAnswerParser. Listing an entire
collection is a PROPFIND to enumerate it plus one multiget REPORT for the
payloads, rather than one GET per entry.

DAV paths use the internal user id from the provisioning API rather than
the login name, which the 2021 discussion on the issue flagged as a problem
when the two differ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A PROPFIND naming specific properties returns only those, so resourcetype
was never sent back, DavResource.getResourceTypes() was empty and the
filter dropped every calendar and address book from the listing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getCalendarEntriesInRange gained a boolean overload that adds the CalDAV
expand element to the requested calendar-data, so the server resolves a
recurring event into one VEVENT per occurrence in the range instead of
returning the stored event with its RRULE.

The expanded form is a computed view of that range and not the stored
resource, so the javadoc warns against writing it back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

access to ics files.

1 participant