Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,38 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0

This library is tightly dependent on the CalDAV-library, particularly the `compatibility_hints.py`-file. This file is not (yet) considered to be part of the "core" business logic in the CalDAV library and can be changed in patch-releases in the CalDAV library - so this library is usually released in lock-steps with the CalDAV-library. I've considered to bump the version number to be follow the CalDAV version number.

## Unreleased

Needs caldav 3.7.0, which has the features below in its `compatibility_hints.py`.

### Changed

- **`save.no-uid-conflict`** is graded by a plain PUT. 403 or 409 with `CALDAV:no-uid-conflict` (or Cyrus' `CALDAV:unique-scheduling-object-resource`) is `full`; another status with it (Xandikos, Stalwart: 412) is `quirk`. A refusal without it is `quirk` too, with the extra keys `status` and `precondition`, if nothing was stored at the URL and the same PUT with another UID goes through (SOGo: 409). A bare 403 stays `ungraceful`.

### Added

- **`supported-components.property-exists`** (the former `get-supported-components`, now probed), **`supported-components.honoured`** and **`create-calendar.with-supported-component-types.multiple`** - whether a calendar reports the component set, whether it takes exactly the types it lists (and every type where it lists none, RFC 4791 §5.2.3), and whether a MKCALENDAR naming several types gives a calendar taking all of them. A calendar that reports `[VTODO]` and still takes a VEVENT is `broken` on `.honoured` (Bedework 3.10, DAViCal, Stalwart); CCS refuses a set of several components the §5.3.1.1 way and is `unsupported` on `.multiple`.
- **`delete.if-match`** - whether a DELETE with an If-Match etag the object no longer has is refused with 412, and one with the current etag goes through. SOGo refuses a stale If-Match on PUT, but deletes the object on DELETE.
- **`save-load.event.floating`** - whether an event with a floating DTSTART and DTEND is stored floating. SOGo gives the times TZID=Etc/UTC (tasks it keeps floating). The probe reads the server's own text: the icalendar library writes a TZID=Etc/UTC time back with a `Z` after it, which RFC 5545 §3.3.5 forbids.
- **`save-load.icalendar.attendee-params`** - whether ATTENDEE parameters are stored as sent. An added RFC 5545 default (PARTSTAT=NEEDS-ACTION and the like) or SCHEDULE-STATUS is fine; SOGo also adds RSVP=TRUE, where the default is FALSE.
- **`save-load.icalendar.participant`** - whether an RFC 9073 PARTICIPANT inside a VEVENT is stored. CCS and Bedework 3 refuse the event with 403, robur with 400, GMX and Ox with 500. See https://github.com/python-caldav/caldav-server-tester/issues/38.
- **`save-load.icalendar.vtimezone`** - whether the VTIMEZONE sent with an event comes back with it. CCS returns the event's TZID without the VTIMEZONE. See https://github.com/python-caldav/caldav-server-tester/issues/39.
- **`save-load.icalendar.x-property`**, **`save-load.icalendar.unknown-property`**, **`save-load.todo.attendee`** and **`save-load.todo.completed-without-status`** - whether an X- property (in an event and in a task), a property RFC 5545 does not define (ESTIMATED-DURATION), an ATTENDEE on a task, and COMPLETED on a task without STATUS are kept. Ox drops all of them (the X- property only from a task); a task with an ATTENDEE is answered 201, then 404. See https://github.com/python-caldav/caldav-server-tester/issues/40.
- **`delete.missing`** and **`delete.missing.if-match`** - whether a DELETE of an object that does not exist answers 404, and one with If-Match answers as the plain one does, as RFC 9110 §13.2.1 has it. Baïkal, Davis, Nextcloud, CCS, OX, Robur, Purelymail and Infomaniak answer 412 with If-Match, so a delete of an object someone else already deleted reads as an etag mismatch.
- **`save.etag.matches-stored`** - whether a strong ETag in a PUT answer belongs to what was stored: RFC 4791 §5.3.4 lets a server rewrite the object, but not answer with a strong ETag then. The behaviour says what the server changed.
- **`sync-token.initial-sync`**, **`sync-token.delete.created-and-deleted`** and **`sync-token.recreated`** - which members a sync-collection report lists: an initial sync with deleted members or the collection itself in it, a member created and deleted between two reports left out, and a re-created member reported twice, or only as removed (Nextcloud, Synology).
- **`multiget.missing-href`** - whether a calendar-multiget answers a missing href with a 404 entry; Baïkal, Nextcloud, DAViCal, Davis and Infomaniak leave it out, Posteo fails the whole multiget with a 404, and an entry claiming the object exists is broken.
- **`multiget.limit`**, **`propfind.limit`** and **`search.limit`** - whether a calendar-multiget, a Depth: 1 PROPFIND or a calendar-query answers every member, however many. Stalwart stops at its `maxResults` with a 507. These store as many objects as `CALDAV_SERVER_TESTER_LIMIT_OBJECTS` says, and are not probed without it.
- **`save.no-uid-conflict.if-none-match`** - the same, for a PUT carrying `If-None-Match: *`, as `save(no_overwrite=True)` sends it. Bedework answers it with a bare 412, but a plain PUT with 403 and the precondition.
- **`save.no-uid-conflict.href-given`** - whether the precondition names the object holding the UID, as RFC 4791 section 5.3.2.1 says it SHOULD.
- **`search.text.uid`** - whether a calendar-query with a UID filter answers the one object with that UID. iCloud answers it with 412 (`ungraceful`), so `get_object_by_uid()` and what is built on it fail there. See https://github.com/python-caldav/caldav/issues/783.

The behaviours came from calendaring-sync's server compatibility notes: https://github.com/PyCalendar/calendaring-sync/blob/c40782bc7afe7b350fe63bf27ef13374c280ea8b/docs/reference/server-compatibility.rst

### Fixed

- **`create-calendar.with-supported-component-types`** - back to asking whether a MKCALENDAR naming `VTODO` gives a calendar with that set. Bedework 5 and SOGo report another set and are `unsupported`; a calendar that reports the set and ignores it is a matter for `.honoured`.

## 1.7.0 - 2026-10-23

This release works with caldav 3.6.0. 1.6.0 was accidentally released without two probes and a grading fix that caldav 3.6.0's server hints were based on.
Expand Down
4 changes: 2 additions & 2 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,12 @@ library *declares* but nothing measures, and quirks that predate the feature
database entirely and still live in the old flat flag list.

At the baseline nine features were defined in `compatibility_hints.py` and probed
by nothing. Eight still are: `get-supported-components`, `save-load.reuse-deleted-uid`,
by nothing. Seven still are: `save-load.reuse-deleted-uid`,
`save-load.todo.recurrences.thisandfuture`, `search.time-range.accurate`,
`search.time-range.journal`, `search.text.category.substring`,
`wrong-password-check` and `http.multiplexing`. Every one of them is currently
reported from its default rather than from evidence - which is exactly the
failure mode constraint 2 is about, at eight known sites. The ninth,
failure mode constraint 2 is about, at seven known sites. The ninth,
`create-calendar.with-supported-component-types`, got its probe on 2026-09-13,
after Bedework 5 turned out to ignore the component set - the first server where
the default was wrong.
Expand Down
56 changes: 54 additions & 2 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,9 @@ cases apart in the probe.

(Clean-context review 2026-09-14, filed rather than fixed.)

`CheckSupportedComponentSet._delete_probe_calendar` deletes
`principal.calendar(cal_id=CAL_ID)`, although the check itself notes that the
`CheckSupportedComponentSet._delete_probe_calendars` deletes
`principal.calendar(cal_id=...)` for both its probe calendars (`CAL_ID` and
`CAL_ID_MULTIPLE`), although the check itself notes that the
`cal_id` is not the collection's address on a server that relocates it. The
probe calendar has no display name either, so a prefix sweep may miss it too.
No server is known to leak one - OX, the relocating server, also answers at the
Expand Down Expand Up @@ -497,3 +498,54 @@ a production Zimbra, not the podman one):
xandikos. Reproduce: is it the number of UIDs, Zimbra's UID search (it
ignores text-match, so each lookup fetches the whole list), or plann's
handling of several UIDs?

# Expired sync tokens: worth a probe at all?

(Noted 2026-10-24, from calendaring-sync's evidence branch. Undecided.)

RFC 6578 section 3.2 has a server refuse a token whose change history it
discarded, with `DAV:valid-sync-token`. `sync-token.invalid-token` only
covers tokens the server never issued; a token that was valid and expired is
not probed, and a client that keeps tokens for days is the one that meets it.

Stalwart can be made to expire tokens: calendaring-sync shrank
`DataRetention.maxChangesHistory` to 1 and then ran an `AccountMaintenance`
purge (https://github.com/PyCalendar/calendaring-sync/tree/evidence, under
`sync-design/servers/stalwart_admin.py`).

Against it:

* The purge is an admin JMAP call. The checker is a client; it would need a
server-side hook, i.e. a Stalwart special case, and the checker shouldn't
grow many of those.
* A low `maxChangesHistory` in caldav's shared Stalwart container expires
tokens under caldav's own sync tests whenever a purge runs.

If it is done, the generic shape is a hook in the test-server definition
("expire the change history now"), with the probe `unknown` wherever no hook
is defined; Stalwart would be the only server with one.

# Bedework refuses TZIDs that are tzdata link names

Bedework answers 403 `unknownTimezone` for a TZID that is a tzdata link name
(Europe/Oslo, Europe/Stockholm, Europe/Amsterdam, Europe/Kiev), even with a
matching VTIMEZONE in the object. Canonical names (Europe/Berlin,
Europe/Paris, America/New_York) are accepted. Seen 2026-10-26 on the
Bedework3 docker server.

No probe or hint covers it. The `save-load.icalendar.vtimezone` probe
already sidesteps it by using Europe/Berlin, which is deliberate; only
Bedework 3 was tried, Bedework 5 was not. A probe would save an event with a
link-name TZID (with and without VTIMEZONE) next to a canonical-name control,
and grade the refusal `ungraceful`; it belongs with that probe
(https://github.com/python-caldav/caldav-server-tester/issues/39).

# `delete.if-match: broken` has no consumer

The hint is set for Zimbra, SOGo, Bedework 3 and 5, and Posteo, but nothing
acts on it: caldav's `delete(if_match=)` does not read it, and neither, as far
as known, does https://github.com/PyCalendar/calendaring. `broken` means the
server deletes whatever the etag says, so skipping the header would change
nothing. Decide whether `delete(if_match=True)` should warn or raise when the
hint says `broken`, or emulate the check with a GET and an etag compare
before the DELETE (racy, but closer to what was asked for).
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ classifiers = [
]
keywords = ["caldav", "calendar", "testing", "compatibility", "server"]
dependencies = [
"caldav>=3.6.0",
"caldav>=3.7.0",
"click",
"pyyaml",
]
Expand Down
Loading
Loading