Server compatibility#
Each table lists what the tested servers did where sync depends on it, and what the adapter does about it. The CalDAV adapter was checked against Radicale 3.8.3, Xandikos 0.4.8, Cyrus 3.13.7, Stalwart 0.16.21 and Baïkal 0.10.1, and the JMAP adapter against Cyrus 3.13.7 and Stalwart 0.16.21, between 2026-10-10 and 2026-10-12. The CalDAV adapter’s integration tests ran against all five on 2026-10-23, with python-caldav 3.6.0 and Xandikos 0.4.10, and the rows they added are marked with that date. Entries marked (tester) come from caldav-server-tester, which ran these checks against 22 servers in October 2026; python-caldav publishes its results as compatibility hints (python-caldav/caldav#822). A later release may behave differently. The probes and logs behind every row are on the evidence branch, with the steps to reproduce them. Adapters explains each rule, and the Glossary defines the sync terms in the Adapter column.
CalDAV#
Behavior |
Tested servers |
Adapter |
|---|---|---|
|
All: yes |
Checked before the first sync |
|
All: defined |
Read before every full listing |
Report with an empty token, after deletions (RFC 6578 Section 3.4: no removed members) |
|
Lists with |
Member created and deleted between two reports (RFC 6578 Section 3.5.2: reported as removed) |
|
A create of yours that’s never mentioned stays pending |
Member deleted and re-created at one href (RFC 6578 Section 3.5.1, RFC 6578 Section 3.2: reported once, as changed) |
|
Fetches removed members too: one that comes back with data counts as changed |
|
All: answered as with |
Relies on python-caldav’s report |
Token the server never issued (RFC 6578 Section 3.2: |
|
Resets the scope when the error names |
Token whose history the server has discarded |
|
Resets the scope; flags Stalwart for regular full listings, which recover what it doesn’t report |
Report with |
|
Never sends |
Full listing of a calendar larger than the server’s result limit |
|
Lists with |
PUT the server stores differently (RFC 4791 Section 5.3.4: no strong ETag in the answer) |
|
Never keeps the ETag a write returns; its fingerprint ignores the rewrite |
Character set of calendar data (RFC 5545 Section 3.1.4: UTF-8 by default) |
|
Reads calendar data as UTF-8 unless the server names another charset |
Identical PUT |
All: same ETag |
A version-only change |
Create with a UID already in the calendar (RFC 4791 Section 5.3.2.1) |
|
Recognizes both elements; finding objects by UID returns every match. Without the server’s profile on the client, python-caldav’s search for the UID refuses the create first, on Baïkal too |
|
|
Nothing it can do: there, a delete can remove a change made since your last sync |
|
|
The next ingest settles it |
|
|
A missing member, or one without data, waits for the next report; after a 404 for the whole request, checks that the calendar is there and fetches each member on its own |
Removed member in a sync report |
|
Syncs Infomaniak from python-caldav’s next release |
Conditional writes behaved as RFC 9110 Section 13.1.1 and RFC 9110 Section 13.1.2 specify on every server in the table: a wrong If-Match or an If-None-Match: * on an existing resource gets 412.
JMAP#
Behavior |
Tested servers |
Adapter |
|---|---|---|
|
Both: pages correctly, |
Follows |
Event created and destroyed between two states (RFC 8620 Section 5.2: should be left out) |
|
A create of yours that’s never mentioned stays pending |
|
|
Resets the scope on either |
|
|
Resets the scope when |
State whose history the server has discarded |
Both: |
Resets the scope |
|
Both: |
Keeps the edits pending |
Properties the server adds to a stored event |
|
Stores the event as the server returns it; its fingerprint leaves those properties out or fills in their defaults |
Create without |
|
Include |
Create with a |
Both: |
Stored as a conflict record, refused for its UID |
Update or destroy of an event that’s gone |
Both: |
The next ingest settles it |
JMAP Tasks ( |
Both: not advertised, Stalwart also not on its latest image |
Events only for now |
No tested server reports a per-event version, so every event a /changes call names is fetched and compared by fingerprint.