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_acceptno longer crash withAttributeErrorwhen an event has noparticipantsproperty at all. They now raiseJMAPMethodErrorwithnotFound, as documented.create_event/update_eventnow retry with the otherrecurrenceRule/recurrenceRulesspelling 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_booksnow marks an account's sole address book as default when the server reports none as default. Cyrus omitsisDefaultentirely instead of sendingfalse, so the one address book an account actually has was never flagged default.ical_to_jscalno longer emits aversionproperty on created/updated events. Current Cyrus rejects it outright withinvalidProperties. RFC 8984 marks the property optional, so leaving it out is correct.ical_to_jscalnow emits asendToproperty on every participant, alongside the existingcalendarAddress. Current Cyrus rejects any participant withoutsendTo, 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, andupdate_eventnow take a JSCalendar dict instead of an iCalendar string.JMAPCalendar.add_eventandJMAPCalendarObject.saveare unaffected: they still take and produce iCalendar strings. (#67)create_eventandsend_invitenow return a 3-tuple(event_id, old_state, new_state)instead of just the event id. (#67)update_eventnow returns(old_state, new_state)instead ofNone. (#67)get_objects_by_sync_tokenandget_tasks_by_sync_tokennow 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 withnew_stateto fetch the next page. (#69)Dropped Python 3.10 support.
requires-pythonis now>=3.11.delete_eventnow returns(old_state, new_state)instead ofNone, matchingcreate_event/update_event. Both Cyrus and Stalwart already send this on every destroy; it was just being discarded.requestsis no longer a hard dependency.niquestsis used by default and remains required;requestsis only needed as a fallback ifniquestsisn't installed, and is now its own extra:pip install calendaring-jmap[requests].search_eventsnow acceptsposition,limit, andcalculate_totalfor paging through large result sets, and returns anEventSearchResultinstead of a bare list. Use.eventsfor the matching events.
New features#
Added calendar management:
create_calendar,update_calendar,delete_calendar,share_calendar,get_calendar_subscriptions, andset_default_alertson bothJMAPClientandAsyncJMAPClient. (#11)get_calendarsand the event methods now take anaccount_idparameter to work with a calendar another user has shared with you. (#11)Added scheduling:
accept_invitation,decline_invitation,tentatively_accept, andsend_inviteon bothJMAPClientandAsyncJMAPClient. (#12)delete_eventcan now send an iTIP CANCEL to participants via a newsend_scheduling_messagesargument. (#12)Added
get_availabilityon bothJMAPClientandAsyncJMAPClient, returning busy intervals for one or more accounts over a time period. Falls back to aCalendarEvent/queryscan whenPrincipal/getAvailabilityisn't supported. Checking someone else's availability by email is not supported yet. (#13)Added
upload_attachment,download_attachment,attach_to_event, andget_event_attachmentson bothJMAPClientandAsyncJMAPClient, for uploading binary data as a JMAP blob and attaching it to a calendar event. (#14)Sessionnow parsesuploadUrl/downloadUrlfrom the JMAP session object, resolving a relative or mismatched-scheme URL the same wayapiUrlalready was. (#14)ical_to_jscal/jscal_to_icalnow convert the iCalendarATTACHproperty to and from the JSCalendarlinksproperty, for both URI and inline binary forms. (#14)Added
get_address_booksandsearch_contactson bothJMAPClientandAsyncJMAPClient, for resolving contact information, such as participant emails, via JMAP Contacts.get_address_booksreturns an empty list with a logged warning, instead of raising, when Contacts isn't supported. (#15)Added
subscribe_push,confirm_push_verification,renew_push, andunsubscribe_pushon bothJMAPClientandAsyncJMAPClient, 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, andparticipant_rolefilters tosearch_eventson bothJMAPClientandAsyncJMAPClient, plusJMAPCalendar.search. None of these have a matchingFilterConditionproperty, 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, andsearch_tasksto bothJMAPClientandAsyncJMAPClient.search_tasks'sdue_before,due_after, andprogressfilters have no matchingTask/queryfilter property (the draft spec never defines one), so they filter client-side against the tasks the server'stextfilter already matched, not on the server.search_tasksfalls back to fetching every task and filtering everything client-side, includingtext, when the server doesn't supportTask/queryat all. (#18)create_event,send_invite,update_event, anddelete_eventnow accept an optionalif_in_stateargument. The server rejects the write withJMAPMethodError(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/AsyncJMAPClientnow have a publicget_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.JMAPMethodErrornow has apropertiesattribute, listing the properties a server'sinvalidPropertiesrejection named, when the server included that list.Nonefor any other error.create_event/send_invitenow acceptcalendar_id=Noneto send an event's owncalendarIdsunchanged, instead of always overwriting it down to one calendar. RaisesValueErrorif the event has nocalendarIdsat all.get_objects_by_sync_tokenandget_tasks_by_sync_tokennow accept an optionalmax_changesto cap how many changes the server returns in one page.get_sync_token,get_objects_by_sync_token,get_task_sync_token, andget_tasks_by_sync_tokennow accept an optionalaccount_id, for syncing a calendar or task list shared with you instead of only your own account.
Bug fixes#
ical_to_jscalnow emits aversionproperty on everyEventobject. Cyrus requires it and rejected every event without one;create_event/update_eventfailed withinvalidProperties: ["version"]on any current Cyrus build. (#11)search_eventsandCalendar.searchnow filter withinCalendar(a single calendar id), matching the current JMAP Calendars spec, instead of the non-existentinCalendars(a list). Calendar-scoped search previously failed against spec-compliant servers. (#11)ical_to_jscalno longer emits asendToproperty on participants, and now emitscalendarAddressinstead. Cyrus rejected any event with an ATTENDEE or ORGANIZER without this fix. (#12)ical_to_jscalandjscal_to_icalnow userecurrenceRule/excludedRecurrenceRule(singular) instead of RFC 8984'srecurrenceRules/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_jscalno longer writes a non-mailto ORGANIZER/ATTENDEE URI, such assip:alice@example.com, into the Participantemailproperty, which RFC 8984 defines as an addr-spec, not an arbitrary URI. Such a participant now hascalendarAddressonly. (#12)jscal_to_icalno longer double-wraps a non-mailtocalendarAddressin a spuriousmailto:prefix. (#12)Fixed an
AttributeErrorwhen a participant has an explicitly nullcalendarAddress. (#12)get_availabilityno longer returns the wrong account's busy intervals when asked about more than one account. (#13)get_availability'sCalendarEvent/queryfallback 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_eventsetshrefon the attachment's Link, notblobId. 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_attachmentsmay not find an attachmentattach_to_eventjust wrote. Cyrus accepts a Link withrel: "enclosure"without error but does not reliably persist that property, andget_event_attachmentsfilters on it. Stalwart is unaffected. (#14)update_eventnow clears an event's attachments when they're removed from the iCalendar it's given, instead of silently leaving the server's oldlinksin place. (#14)On Stalwart,
search_contacts'stextfilter does not match against a contact's name, only against its email address. A name-onlytextsearch that finds a contact on Cyrus can silently return nothing on Stalwart; useemailwhen searching for an address. (#15)ical_to_jscalandjscal_to_icalnow convert theURLproperty to and from JSCalendar'slinks, instead of dropping it entirely. (#19)ical_to_jscalandjscal_to_icalnow convert aVCONFERENCEcomponent'sDESCRIPTION/STYLED-DESCRIPTIONto and from VirtualLocation'sdescription/descriptionContentType, instead of dropping it entirely. Cyrus currently rejectsdescriptionon aVirtualLocationwithinvalidPropertiesonCalendarEvent/set; Stalwart round-trips it correctly. (#19)Recurrence override patches now carry
participants,locations,alerts,status,keywords, andfreeBusyStatuschanges in both conversion directions, instead of silently dropping any per-occurrence change beyond title, start, duration, and description. (#20)jscal_to_icalnow resolves a recurrence override patch returned as flattened per-key JSON-Pointer entries, such as"keywords/urgent": trueas confirmed on Cyrus, in addition to the whole-map replacement form this package itself always sends. (#20)Alert
triggervalues are now emitted and read as properOffsetTrigger/AbsoluteTriggerobjects per RFC 8984, instead of a bare duration or date-time string that a spec-compliant server rejects. (#20)ical_to_jscalandjscal_to_icalnow convertCONFERENCEproperties to and from JSCalendar'svirtualLocations, at both the master event level and inside recurrence override patches, instead of dropping video/phone conference details entirely. (#20)ical_to_jscalnow normalizes a non-IANATZIDvalue, such as a Windows timezone name or a vendor-prefixed globally-uniqueTZID, to an IANA name intimeZone, as RFC 8984 requires, instead of passing it through unchanged. (#44)Removed the
asyncextra. It declaredniquests[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 shipsAsyncSessionunconditionally.get_jmap_client/get_async_jmap_clientnow accept a pre-builtauthobject. Theauthkeyword argument was silently dropped before reaching the client, so a caller following the documented example gotJMAPAuthErrorinstead of using their own auth object.ical_to_jscalnow raisesValueErrorwhenDTSTARTandDTENDhave mismatched value types (oneDATE, oneDATE-TIME, which RFC 5545 forbids), instead of an unexplainedTypeError.ical_to_jscalnow raisesValueErrorwhen a VEVENT'sDTEND/DURATIONresolves to before itsDTSTART, instead of silently producing a negative JSCalendarduration, which RFC 8984 forbids and a server would otherwise reject with an opaqueinvalidPropertieserror.ical_to_jscalnow raisesValueErrorwhen the master VEVENT is missingUIDorDTSTART(both mandatory per RFC 5545), instead of a bare, undocumentedKeyError.ical_to_jscalnow skips a VALARM with noTRIGGER(mandatory per RFC 5545 and RFC 8984) rather than emitting a JSCalendar Alert with notriggerproperty, which RFC 8984 forbids. Other, well-formed alarms on the same event are unaffected.jscal_to_icalnow raisesValueErrorwhen the input JSCalendar dict has nouid(mandatory per RFC 8984), instead of silently emitting an iCalendar VEVENT with noUIDproperty, which RFC 5545 forbids.
Documentation#
Switched the project to uv for Python and dependency management, and to a
Makefilefor 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_icaldirectly, 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, andconvert, which previously had no documentation of their own despite being part of the public API.Consolidated the
account_idexplanation, previously repeated across most ofJMAPClient/AsyncJMAPClient's own methods, into each class's docstring.Corrected the authenticate how-to's description of configuration precedence: an explicit
urlargument 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
ValueErrorconversion-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.rstanddocs/how-to/example-sync-cache.rst, which still unpackedget_objects_by_sync_token's old 4-tuple and described the removedserverPartialFailexception. 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/latestinstead ofen/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
apidocmake target now actually passesSPHINX_APIDOC_OPTIONSto sphinx-apidoc, via--automodule-options; theMakefileset 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_withfield, not just writing it withshare_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-calendarcal.search()coverage.The install how-to page now covers the
requestsfallback 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 inniquests, 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 withgh-action-pypi-publishv1.14.3, both fixes for the same incident. (#72)
1.1.0 (2026-09-15)#
New features#
JMAPErrorand its subclasses now subclasscaldav.lib.error.DAVErrorifcaldavis installed, so code catchingDAVErroraround CalDAV calls also catches JMAP errors.caldavis 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 fromJMAP_URL/JMAP_USERNAME/JMAP_PASSWORDenvironment variables or a YAML config file instead ofcaldav'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)