Change log#

Every calendaring-jmap release, newest first: new features, bug fixes, breaking changes, and documentation updates, each with the pull request that shipped it.

2.0.1 (2026-11-03)#

Bug fixes#

  • accept_invitation/decline_invitation/tentatively_accept no longer crash with AttributeError when an event has no participants property at all. They now raise JMAPMethodError with notFound, as documented.

  • create_event/update_event now retry with the other recurrenceRule/recurrenceRules spelling when the server rejects the one they sent. Current Cyrus requires the plural array form. Stalwart still requires the singular form, so no single hardcoded spelling could satisfy both servers.

  • get_address_books now marks an account's sole address book as default when the server reports none as default. Cyrus omits isDefault entirely instead of sending false, so the one address book an account actually has was never flagged default.

  • ical_to_jscal no longer emits a version property on created/updated events. Current Cyrus rejects it outright with invalidProperties. RFC 8984 marks the property optional, so leaving it out is correct.

  • ical_to_jscal now emits a sendTo property on every participant, alongside the existing calendarAddress. Current Cyrus rejects any participant without sendTo, on both create and update, regardless of scheduling messages.

Documentation#

  • Added sphinx-llms-txt for generating LLMS-friendly files. @stevepiercy (#85)

2.0.0 (2026-11-02)#

Breaking changes#

  • create_event, send_invite, and update_event now take a JSCalendar dict instead of an iCalendar string. JMAPCalendar.add_event and JMAPCalendarObject.save are unaffected: they still take and produce iCalendar strings. (#67)

  • create_event and send_invite now return a 3-tuple (event_id, old_state, new_state) instead of just the event id. (#67)

  • update_event now returns (old_state, new_state) instead of None. (#67)

  • get_objects_by_sync_token and get_tasks_by_sync_token now return a 6-tuple (added, modified, deleted, old_state, new_state, has_more_changes) instead of a 4-tuple, and no longer raise when the server truncates a page (hasMoreChanges: true). Call the method again with new_state to fetch the next page. (#69)

  • Dropped Python 3.10 support. requires-python is now >=3.11.

  • delete_event now returns (old_state, new_state) instead of None, matching create_event/update_event. Both Cyrus and Stalwart already send this on every destroy; it was just being discarded.

  • requests is no longer a hard dependency. niquests is used by default and remains required; requests is only needed as a fallback if niquests isn't installed, and is now its own extra: pip install calendaring-jmap[requests].

  • search_events now accepts position, limit, and calculate_total for paging through large result sets, and returns an EventSearchResult instead of a bare list. Use .events for the matching events.

New features#

  • Added calendar management: create_calendar, update_calendar, delete_calendar, share_calendar, get_calendar_subscriptions, and set_default_alerts on both JMAPClient and AsyncJMAPClient. (#11)

  • get_calendars and the event methods now take an account_id parameter to work with a calendar another user has shared with you. (#11)

  • Added scheduling: accept_invitation, decline_invitation, tentatively_accept, and send_invite on both JMAPClient and AsyncJMAPClient. (#12)

  • delete_event can now send an iTIP CANCEL to participants via a new send_scheduling_messages argument. (#12)

  • Added get_availability on both JMAPClient and AsyncJMAPClient, returning busy intervals for one or more accounts over a time period. Falls back to a CalendarEvent/query scan when Principal/getAvailability isn't supported. Checking someone else's availability by email is not supported yet. (#13)

  • Added upload_attachment, download_attachment, attach_to_event, and get_event_attachments on both JMAPClient and AsyncJMAPClient, for uploading binary data as a JMAP blob and attaching it to a calendar event. (#14)

  • Session now parses uploadUrl/downloadUrl from the JMAP session object, resolving a relative or mismatched-scheme URL the same way apiUrl already was. (#14)

  • ical_to_jscal/jscal_to_ical now convert the iCalendar ATTACH property to and from the JSCalendar links property, for both URI and inline binary forms. (#14)

  • Added get_address_books and search_contacts on both JMAPClient and AsyncJMAPClient, for resolving contact information, such as participant emails, via JMAP Contacts. get_address_books returns an empty list with a logged warning, instead of raising, when Contacts isn't supported. (#15)

  • Added subscribe_push, confirm_push_verification, renew_push, and unsubscribe_push on both JMAPClient and AsyncJMAPClient, for JMAP push subscriptions (RFC 8620 Section 7.2). The subscription's callback URL must be HTTPS and publicly reachable by the server; this will not work behind localhost without tunneling. (#16)

  • Added has_attachment, participant_email, participation_status, and participant_role filters to search_events on both JMAPClient and AsyncJMAPClient, plus JMAPCalendar.search. None of these have a matching FilterCondition property, so they filter client-side against the events the server's existing filters already matched, not on the server. (#17)

  • Added get_task_sync_token, get_tasks_by_sync_token, and search_tasks to both JMAPClient and AsyncJMAPClient. search_tasks's due_before, due_after, and progress filters have no matching Task/query filter property (the draft spec never defines one), so they filter client-side against the tasks the server's text filter already matched, not on the server. search_tasks falls back to fetching every task and filtering everything client-side, including text, when the server doesn't support Task/query at all. (#18)

  • create_event, send_invite, update_event, and delete_event now accept an optional if_in_state argument. The server rejects the write with JMAPMethodError (error_type == "stateMismatch") if the account's state has moved since, instead of silently applying over a concurrent change. (#68)

  • Added Python 3.14 support.

  • JMAPClient/AsyncJMAPClient now have a public get_account_id(), returning the session's default account id. Fetches the session on first call if it isn't cached yet, the same as any other method here.

  • JMAPMethodError now has a properties attribute, listing the properties a server's invalidProperties rejection named, when the server included that list. None for any other error.

  • create_event/send_invite now accept calendar_id=None to send an event's own calendarIds unchanged, instead of always overwriting it down to one calendar. Raises ValueError if the event has no calendarIds at all.

  • get_objects_by_sync_token and get_tasks_by_sync_token now accept an optional max_changes to cap how many changes the server returns in one page.

  • get_sync_token, get_objects_by_sync_token, get_task_sync_token, and get_tasks_by_sync_token now accept an optional account_id, for syncing a calendar or task list shared with you instead of only your own account.

Bug fixes#

  • ical_to_jscal now emits a version property on every Event object. Cyrus requires it and rejected every event without one; create_event/update_event failed with invalidProperties: ["version"] on any current Cyrus build. (#11)

  • search_events and Calendar.search now filter with inCalendar (a single calendar id), matching the current JMAP Calendars spec, instead of the non-existent inCalendars (a list). Calendar-scoped search previously failed against spec-compliant servers. (#11)

  • ical_to_jscal no longer emits a sendTo property on participants, and now emits calendarAddress instead. Cyrus rejected any event with an ATTENDEE or ORGANIZER without this fix. (#12)

  • ical_to_jscal and jscal_to_ical now use recurrenceRule/excludedRecurrenceRule (singular) instead of RFC 8984's recurrenceRules/excludedRecurrenceRules (an array), which neither Cyrus nor Stalwart accept. Only the first RRULE/EXRULE is kept if a VEVENT has more than one. (#12)

  • ical_to_jscal no longer writes a non-mailto ORGANIZER/ATTENDEE URI, such as sip:alice@example.com, into the Participant email property, which RFC 8984 defines as an addr-spec, not an arbitrary URI. Such a participant now has calendarAddress only. (#12)

  • jscal_to_ical no longer double-wraps a non-mailto calendarAddress in a spurious mailto: prefix. (#12)

  • Fixed an AttributeError when a participant has an explicitly null calendarAddress. (#12)

  • get_availability no longer returns the wrong account's busy intervals when asked about more than one account. (#13)

  • get_availability's CalendarEvent/query fallback now converts each busy interval to UTC using the event's own time zone, instead of returning an offset time for any zone other than UTC. (#13)

  • attach_to_event sets href on the attachment's Link, not blobId. The calendars draft allows a Link to carry a blob id directly, but neither Cyrus nor Stalwart actually persists it: Cyrus rejects it outright, and Stalwart silently drops it. A direct download URL round-trips correctly on both. (#14)

  • On Cyrus, get_event_attachments may not find an attachment attach_to_event just wrote. Cyrus accepts a Link with rel: "enclosure" without error but does not reliably persist that property, and get_event_attachments filters on it. Stalwart is unaffected. (#14)

  • update_event now clears an event's attachments when they're removed from the iCalendar it's given, instead of silently leaving the server's old links in place. (#14)

  • On Stalwart, search_contacts's text filter does not match against a contact's name, only against its email address. A name-only text search that finds a contact on Cyrus can silently return nothing on Stalwart; use email when searching for an address. (#15)

  • ical_to_jscal and jscal_to_ical now convert the URL property to and from JSCalendar's links, instead of dropping it entirely. (#19)

  • ical_to_jscal and jscal_to_ical now convert a VCONFERENCE component's DESCRIPTION/STYLED-DESCRIPTION to and from VirtualLocation's description/descriptionContentType, instead of dropping it entirely. Cyrus currently rejects description on a VirtualLocation with invalidProperties on CalendarEvent/set; Stalwart round-trips it correctly. (#19)

  • Recurrence override patches now carry participants, locations, alerts, status, keywords, and freeBusyStatus changes in both conversion directions, instead of silently dropping any per-occurrence change beyond title, start, duration, and description. (#20)

  • jscal_to_ical now resolves a recurrence override patch returned as flattened per-key JSON-Pointer entries, such as "keywords/urgent": true as confirmed on Cyrus, in addition to the whole-map replacement form this package itself always sends. (#20)

  • Alert trigger values are now emitted and read as proper OffsetTrigger/AbsoluteTrigger objects per RFC 8984, instead of a bare duration or date-time string that a spec-compliant server rejects. (#20)

  • ical_to_jscal and jscal_to_ical now convert CONFERENCE properties to and from JSCalendar's virtualLocations, at both the master event level and inside recurrence override patches, instead of dropping video/phone conference details entirely. (#20)

  • ical_to_jscal now normalizes a non-IANA TZID value, such as a Windows timezone name or a vendor-prefixed globally-unique TZID, to an IANA name in timeZone, as RFC 8984 requires, instead of passing it through unchanged. (#44)

  • Removed the async extra. It declared niquests[asyncio], an extra niquests itself doesn't define; pip install calendaring-jmap[async] silently installed plain niquests with a pip warning, no different from the base install. No separate install step has ever been needed for async support: niquests is already a required dependency and ships AsyncSession unconditionally.

  • get_jmap_client/get_async_jmap_client now accept a pre-built auth object. The auth keyword argument was silently dropped before reaching the client, so a caller following the documented example got JMAPAuthError instead of using their own auth object.

  • ical_to_jscal now raises ValueError when DTSTART and DTEND have mismatched value types (one DATE, one DATE-TIME, which RFC 5545 forbids), instead of an unexplained TypeError.

  • ical_to_jscal now raises ValueError when a VEVENT's DTEND/DURATION resolves to before its DTSTART, instead of silently producing a negative JSCalendar duration, which RFC 8984 forbids and a server would otherwise reject with an opaque invalidProperties error.

  • ical_to_jscal now raises ValueError when the master VEVENT is missing UID or DTSTART (both mandatory per RFC 5545), instead of a bare, undocumented KeyError.

  • ical_to_jscal now skips a VALARM with no TRIGGER (mandatory per RFC 5545 and RFC 8984) rather than emitting a JSCalendar Alert with no trigger property, which RFC 8984 forbids. Other, well-formed alarms on the same event are unaffected.

  • jscal_to_ical now raises ValueError when the input JSCalendar dict has no uid (mandatory per RFC 8984), instead of silently emitting an iCalendar VEVENT with no UID property, which RFC 5545 forbids.

Documentation#

  • Switched the project to uv for Python and dependency management, and to a Makefile for the common development commands (tests, linting, docs builds). @stevepiercy (#63)

  • The reference pages under docs/reference/ are now generated from source with sphinx-apidoc instead of hand-maintained. @stevepiercy (#63)

  • Fixed broken links found by make linkcheckbroken, and removed unused theme files. (#63)

  • Added SEO metadata to every docs page: a canonical link, an accurate description, and a social preview image using the Python Calendaring Ecosystem's own branding.

  • Added a complete example script that keeps a SQLite cache of a calendar's events in sync.

  • Added a how-to page documenting the calendaring_jmap logger and every case where the library logs a warning instead of raising.

  • Added a how-to page for checking free/busy availability.

  • Added a how-to page for listing address books and searching contacts.

  • Added a how-to page for sending invitations and responding to them (accept, decline, tentatively accept).

  • Added a how-to page for the full push notification lifecycle: subscribe, confirm verification, renew, unsubscribe.

  • Added a how-to page for uploading, attaching, listing, and downloading event attachments.

  • Added a how-to page for using ical_to_jscal/jscal_to_ical directly, without making any JMAP request.

  • Added a maintainer-facing page documenting the release process.

  • Added a reference page collecting every confirmed Cyrus/Stalwart behavioral difference in one table, instead of leaving each one scattered across its own how-to page.

  • Added an explanation-page section for why the error hierarchy is split into a generic base and a JMAP-specific layer, fixing a cross-reference in the errors how-to page that previously pointed at nothing.

  • Added explanation-page coverage for the conversion layer's own design: why CONFERENCE and VCONFERENCE correlate by URI, and why recurrence override patches compare map-shaped properties by value instead of by key.

  • Added explanation-page sections on participant address handling, alert trigger shape, Link/attachment conversion scope, and recurrence rule time zone conversion.

  • Added reference pages for constants, session, and convert, which previously had no documentation of their own despite being part of the public API.

  • Consolidated the account_id explanation, previously repeated across most of JMAPClient/AsyncJMAPClient's own methods, into each class's docstring.

  • Corrected the authenticate how-to's description of configuration precedence: an explicit url argument doesn't merge in username, password, auth type, or timeout from environment variables or a config file.

  • Corrected the contributor guide's title-underline rule: Sphinx only warns about an overline/underline mismatch on a page's own title, not on an ordinary section heading.

  • Cross-linked contribute.rst and release.rst: the change log section had an anchor nothing pointed to, and contribute.rst never mentioned the release page existed.

  • Documented close()/aclose() for releasing the HTTP connection without using the client as a context manager.

  • Documented get_connection_params, a public function that previously had no reference page.

  • Documented the ValueError conversion-layer validation added in an earlier release, which wasn't documented anywhere.

  • Documented the security purpose of the push subscription verification handshake and the callback URL trust requirement.

  • Expanded README's Specs section, which only listed 3 of the 7 specs the client actually implements, missing JSCalendar/iCalendar conversion, Contacts, Sharing/Principals, and Tasks entirely.

  • Fixed docs/how-to/sync.rst and docs/how-to/example-sync-cache.rst, which still unpacked get_objects_by_sync_token's old 4-tuple and described the removed serverPartialFail exception. The sync-cache example also kept its sync token per calendar; fixed to key it per account, since JMAP state covers the whole account, not one calendar.

  • Fixed both Docker test-server READMEs' claim that stopping the container wipes persistent data; neither server's container actually has any to wipe.

  • Fixed several cross-references in the API reference that rendered as plain, unlinked text instead of linking to the method or class they named.

  • Fixed several stale docs claims left over from before the 1.0.0 release: a "not yet published to PyPI" notice, a missing reference entry for BusyInterval, and a couple of outdated code examples.

  • Fixed the Stalwart test-server README's claim that it's used for task integration tests; there are none against either server, and Stalwart's actual exclusive coverage is push notifications.

  • Fixed the link in each page's social card, which left out the docs language and version, such as /en/stable/, and so led to a missing page.

  • Fixed two links that pointed at en/latest instead of en/stable.

  • Removed CalDAV comparisons from the docs and docstrings; JMAP's own behavior is now described on its own terms.

  • Removed the theme/Sphinx version numbers from the docs footer; nobody reading the docs needs to know which Sphinx release built the page.

  • The apidoc make target now actually passes SPHINX_APIDOC_OPTIONS to sphinx-apidoc, via --automodule-options; the Makefile set the variable but never wired it into the command.

  • The calendars how-to page covers the full calendar management surface (create, update, delete, share, default alerts, subscriptions), not just listing.

  • The calendars how-to page now covers reading back a calendar's current sharing configuration from its share_with field, not just writing it with share_calendar.

  • The docs site now uses the Python Calendaring Ecosystem's own colors and fonts, in both light and dark mode, instead of the Sphinx theme's defaults.

  • The events how-to page now covers search_events, the account-wide search with the attachment, participant email, participation status, and role filters, alongside the existing single-calendar cal.search() coverage.

  • The install how-to page now covers the requests fallback extra and clarifies that async support needs no separate install step.

  • The install how-to page now mentions the package's py.typed marker.

  • The one-off Sphinx warning about an unresolvable forward reference in niquests' own type hints is suppressed locally instead of turning off warnings-as-errors project-wide; it's a bug in niquests, not in calendaring-jmap.

  • The scheduling how-to page now covers reading a participant's current status, alongside the existing accept/decline/tentatively-accept coverage.

  • The tasks how-to page now covers incremental sync and search, alongside the existing create/read/update/delete coverage.

Security fixes#

  • Now requires wassima>=2.1.5, which drops the compromised eMudhra emSign root certificates from its embedded fallback bundle. CI also installs pip 26.2.2 in every job and publishes to PyPI with gh-action-pypi-publish v1.14.3, both fixes for the same incident. (#72)

1.1.0 (2026-09-15)#

New features#

  • JMAPError and its subclasses now subclass caldav.lib.error.DAVError if caldav is installed, so code catching DAVError around CalDAV calls also catches JMAP errors. caldav is still not a hard dependency. (#10)

1.0.0 (2026-09-11)#

New features#

  • Extracted the JMAP client from python-caldav into this standalone package. calendaring-jmap no longer depends on caldav: configuration now comes from JMAP_URL/JMAP_USERNAME/JMAP_PASSWORD environment variables or a YAML config file instead of caldav's config system. (#9)

Documentation#

  • Added Sphinx documentation scaffolding with the PyData theme. (#8)

  • Added a security policy: how to report a vulnerability, and known risks specific to JMAP session discovery, credential handling, and untrusted calendar data. (#22)

  • Restructured the docs around the Diátaxis framework (tutorial, how-to, reference, explanation), added a security policy and NLnet funding acknowledgment, and cleaned up release metadata ahead of 1.0. (#42)

Other tasks#

  • Maintenance: type checking, test coverage, Docker test fixtures, packaging fix, contributing docs on Read the Docs. (#40)

Last updated on 2026-11-03.