Read iCloud subscribed calendars from their source feed - #146
Open
cosmosified wants to merge 2 commits into
Open
Read iCloud subscribed calendars from their source feed#146cosmosified wants to merge 2 commits into
cosmosified wants to merge 2 commits into
Conversation
added 2 commits
August 28, 2026 11:08
Subscribed iCloud calendars (webcal feeds added to iCloud) are pointers, not event containers: the resourcetype is <cs:subscribed/> and the events live at the <cs:source> URL. A calendar-query REPORT against one succeeds but returns an empty multistatus, so a sync reported success with zero events while Calendar.app showed the events fine. - Request <cs:source/> during calendar discovery - Expose subscribed/sourceUrl from parseCalendars - Probe the collection with a Depth:0 PROPFIND in fetchCalendarEvents and read the upstream ICS feed when it is a subscription - Normalize webcal:// -> https:// and reject non-http(s) source URLs - Fetch the feed unauthenticated so the iCloud app password is never sent to the third-party feed host Probe failures fall back to the existing REPORT path, so calendars that already worked cannot regress.
The first pass only unit-tested the parsing helpers, which would not have caught the actual bug: fetchCalendarEvents issuing a calendar-query REPORT against a subscription. These tests pin the behaviour instead. Regression tests (local http server, matching the approach already used by homeAssistant.test.js and outboundTls.test.js, since fetchCalendarEvents takes the collection URL as an argument): - a subscribed calendar reads its source feed and issues NO REPORT - iCloud credentials are never sent to the third-party feed host - a regular calendar still uses REPORT and fetches no feed - a failing subscription probe falls back to REPORT - a subscription with no source URL fails loudly instead of returning [] - a non-http(s) source is refused rather than fetched - the REPORT body carries a well-formed calendar-query with a time-range Also move every outbound CalDAV body into a frozen ICLOUD_XML_MESSAGE class with static fields, so the wire format lives in one place instead of being scattered through the request functions. CALENDAR_QUERY is a builder because a calendar-query must carry a concrete time-range; the other four are constants. Verified byte-identical to the previously inlined bodies. Covered by tests asserting each body is well-formed XML, that CALENDAR_LIST still requests cs:source (dropping it is what made subscriptions look empty), and that the class is frozen. 196 server tests pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A subscribed iCloud calendar — a
webcalfeed added to iCloud, such as aTeamSnap team schedule — appears in the calendar picker and syncs with
success, but always imports zero events. The same calendar shows itsevents correctly in Calendar.app.
Root cause
An iCloud subscription is a pointer, not a container of event resources. Its
resourcetype is
<cs:subscribed/>and its events live at the URL in the<cs:source>property (CalendarServer extension).fetchCalendarEvents()issued acalendar-queryREPORT against the collection.Per RFC 4791 §7.8 that report targets a calendar collection holding event
resources; against a subscription it is accepted but returns an empty
multistatus. The sync therefore recorded
successwith0 eventsand no error,which made it look like an empty calendar rather than a bug.
parseCalendars()already intentionally lists subscriptions (they advertiseVEVENT), so they were selectable in the picker — only the fetch path was wrong.The discovery PROPFIND also never requested
<cs:source/>, so the feed URL wasnever available, even though the
csnamespace was already declared.Changes
All in
server/services/appleCalDAV.js:<cs:source />during calendar discovery.parseCalendars()now reportssubscribedandsourceUrlper calendar.parseCollectionSource()reads subscription details from a Depth:0 PROPFIND.describeCollection()probes a single collection before fetching.fetchSubscriptionEvents()reads the upstream ICS feed directly.fetchCalendarEvents()branches: subscriptions read their source feed,everything else keeps the existing REPORT path unchanged.
normalizeFeedUrl()rewriteswebcal:///webcals://tohttps://andrejects anything not
http(s)afterwards.ICLOUD_XML_MESSAGEclass so the wire format lives in one place.CALENDAR_QUERYis a builder (a calendar-query must carry a concretetime-range); the other four are constants. This is a pure extraction — the
generated bodies were verified byte-identical to the previously inlined
literals.
Compatibility and safety
try/catch; any failure logs a warning and falls through to the current REPORT
path. There is a test for this.
unauthenticated: the source is a third-party host (TeamSnap, leagues, school
districts), so the iCloud app-specific password is never sent to it. Tested.
<cs:source>. Non-http(s)schemes are refused, so amalformed or hostile source property cannot trigger a
file://read. Tested.icsToEvents()does not expandRRULE,and neither did the REPORT path. Left as-is deliberately rather than altered
as a side effect of this fix.
Testing
cd server && npm test— 196 pass, 0 fail (184 before this branch).Behavioural tests use a throwaway local
httpserver rather than module mocks,following the existing approach in
homeAssistant.test.jsandoutboundTls.test.js;fetchCalendarEvents()takes the collection URL as anargument, so pointing it at
127.0.0.1is enough to assert which requests itmakes:
[]http(s)source is refused rather than fetchedcalendar-querywith a time-rangeRequest bodies are covered too: each static body is well-formed XML,
CALENDAR_LISTstill requestscs:source(dropping it is exactly what madesubscriptions look empty), and the class is frozen.
Unit tests additionally cover
webcal:///webcals://rewriting,http(s)passthrough, rejection of
file:///ftp:///empty, subscription detection, andparseCalendars()exposingsubscribed+sourceUrl.Verified end to end against a real iCloud-subscribed TeamSnap calendar using
docker-compose-dev.yml: the same source went fromsuccess / 0 eventstosuccess / 59 events in 379ms.Notes for review
CONTRIBUTING.md do not apply — there are no client changes.
17d2220; a test-merge reports no conflicts.