client: JMAP calendar client#

Synchronous JMAP client.

Wraps session establishment, HTTP communication, and method dispatching into a single object with a clean public API.

Auth note: JMAP has no 401-challenge-retry dance. Credentials are sent upfront on every request. A 401/403 is a hard failure.

class calendaring_jmap.client.JMAPClient(url: str, username: str | None = None, password: str | None = None, auth=None, auth_type: str | None = None, timeout: int = 30)[source]#

Bases: _JMAPClientBase

Synchronous JMAP client for calendar operations.

Usage:

from calendaring_jmap import get_jmap_client
client = get_jmap_client(url="https://jmap.example.com/.well-known/jmap",
                          username="alice", password="secret")
calendars = client.get_calendars()
Parameters:
  • url – URL of the JMAP session endpoint (/.well-known/jmap).

  • username – Username for Basic auth.

  • password – Password for Basic auth, or bearer token if no username.

  • auth – A pre-built requests-compatible auth object. Takes precedence over username/password if provided.

  • auth_type – Force a specific auth type: "basic" or "bearer".

  • timeout – HTTP request timeout in seconds.

Unless a method’s own docstring says otherwise, its account_id parameter is the JMAP account to operate on, defaulting to the authenticated user’s own primary account; pass a different account (typically one shared with you via share_calendar) to operate on that account’s data instead.

accept_invitation(event_id: str, own_email: str, account_id: str | None = None) → None[source]#

Accept a meeting invitation, notifying the organizer.

Finds the participant on event_id matching own_email and sets their participationStatus to accepted. Only that one property is touched; per draft-ietf-jmap-calendars section 5.9, a non-origin account may never modify anything but its own participant properties.

There is no counter-proposal method (iTIP COUNTER): neither RFC 8984 nor draft-ietf-jmap-calendars-29 define one. Confirmed live against Cyrus and Stalwart that a non-origin account can still write start/duration directly through the server’s own rights model, but doing so is a plain update outside the section 5.9 per-user-property restriction above, not a supported scheduling primitive, and is not exposed as a method here.

Parameters:
  • event_id – The JMAP event ID to respond to.

  • own_email – The email address identifying which participant on the event is you.

  • account_id – The account owning event_id.

Raises:

JMAPMethodError – If no participant matches own_email, or if the server rejects the update.

attach_to_event(event_id: str, blob_id: str, name: str, content_type: str, account_id: str | None = None) → None[source]#

Attach an uploaded blob to a calendar event.

Adds one Link to the event’s links property (RFC 8984 Section 4.2.7) with rel: "enclosure". The Link’s href is set to blob_id’s own download URL, not blobId: draft-ietf-jmap-calendars section 5.3 allows a Link to carry blobId directly, but confirmed live against Cyrus and Stalwart that neither server persists it (Cyrus rejects the update; Stalwart accepts it and silently drops the Link). href round-trips correctly on both.

Not safe against a concurrent write to this event’s links: reads the current value, then replaces the whole property with the merged result, with no ifInState guard. Whichever write lands second wins silently. Serialize calls for a given event_id if that matters to your use case.

Known Cyrus limitation: Cyrus accepts a Link with rel: "enclosure" but does not reliably persist that property, so get_event_attachments() may not find what this method just wrote. See JMAPAttachment.is_attachment for the full finding. Stalwart is unaffected.

Parameters:
  • event_id – The JMAP event ID to attach to.

  • blob_id – The blob id, as returned by upload_attachment().

  • name – Human-readable title for the attachment.

  • content_type – Media type of the blob.

  • account_id – The account owning event_id.

Raises:
close() → None[source]#

Release the persistent HTTP session and its connection pool.

Only needed when the client was not used as a context manager: the documented Quick Start builds one directly, and without this there was no way to hand the sockets back. Idempotent; the session is recreated on the next request.

confirm_push_verification(subscription_id: str, verification_code: str) → None[source]#

Submit the verification code the server POSTed to callback_url.

Required to complete subscribe_push()’s handshake: the server “MUST NOT make any further requests to the URL” until this is called with the matching code (RFC 8620 Section 7.2.2). Stalwart rejects a wrong code with invalidProperties rather than silently ignoring it, verified against a running instance. See subscribe_push() for the account_id parameter note.

Parameters:
  • subscription_id – The JMAP PushSubscription ID returned by subscribe_push().

  • verification_code – The verificationCode from the PushVerification payload the server POSTed to the subscription’s callback URL.

Raises:

JMAPMethodError – If the code doesn’t match (error_type "invalidProperties"), or the subscription doesn’t exist.

create_calendar(name: str, color: str | None = None, timezone: str | None = None) → str[source]#

Create a calendar.

Returns:

The server-assigned JMAP calendar ID.

Raises:

JMAPMethodError – If the server rejects the create request.

create_event(calendar_id: str, ical_str: str, account_id: str | None = None, if_in_state: str | None = None) → str[source]#

Create a calendar event from an iCalendar string.

Parameters:
  • calendar_id – The JMAP calendar ID to create the event in.

  • ical_str – A VCALENDAR string representing the event.

  • account_id – The account owning calendar_id; pass the owner’s account here to add an event to a calendar shared with you (see get_calendars(account_id=...)).

  • if_in_state – A state string from a prior call that read this account’s calendar state. If the account has changed since, the server rejects the create with JMAPMethodError (error_type == "stateMismatch") instead of applying it (RFC 8620 Section 5.3). The state covers every event in the account, not just this one. None applies unconditionally.

Returns:

The server-assigned JMAP event ID.

Raises:
  • JMAPMethodError – If the server rejects the create request.

  • ValueError – If ical_str is malformed in a way ical_to_jscal rejects (missing UID/DTSTART, a negative duration, mismatched DTSTART/DTEND value types).

create_task(task_list_id: str, title: str, **kwargs) → str[source]#

Create a task in a task list.

Parameters:
  • task_list_id – The JMAP task list ID to create the task in.

  • title – Task title (maps to VTODO SUMMARY).

  • **kwargs – Optional JMAP Task fields using wire names: description, due, start, timeZone, estimatedDuration, percentComplete, progress, priority.

Returns:

The server-assigned JMAP task ID.

Raises:

JMAPMethodError – If the server rejects the create request.

decline_invitation(event_id: str, own_email: str, account_id: str | None = None) → None[source]#

Decline a meeting invitation, notifying the organizer.

Same as accept_invitation(), setting participationStatus to declined instead.

delete_calendar(calendar_id: str, on_destroy_remove_events: bool = False, account_id: str | None = None) → None[source]#

Delete a calendar.

Parameters:
  • calendar_id – The JMAP calendar ID to delete.

  • on_destroy_remove_events – If False (the default), deleting a calendar that still has events fails with a calendarHasEvent error instead of deleting anything. Pass True to remove those events along with the calendar.

  • account_id – The account owning calendar_id; pass the owner’s account here to delete a calendar shared with you.

Raises:

JMAPMethodError – If the server rejects the delete. error_type is "calendarHasEvent" when the calendar still has events and on_destroy_remove_events was left False.

delete_event(event_id: str, account_id: str | None = None, send_scheduling_messages: bool = False, if_in_state: str | None = None) → None[source]#

Delete a calendar event.

Parameters:
  • event_id – The JMAP event ID to delete.

  • account_id – The account owning event_id; pass the owner’s account here to delete an event on a calendar shared with you.

  • send_scheduling_messages – If true, and this account is the event’s origin, the server sends an iTIP CANCEL to the event’s participants (draft-ietf-jmap-calendars section 5.9.2.2).

  • if_in_state – A state string from a prior call that read this account’s calendar state. If the account has changed since, the server rejects the delete with JMAPMethodError (error_type == "stateMismatch") instead of applying it (RFC 8620 Section 5.3). The state covers every event in the account, not just this one. None applies unconditionally.

Raises:

JMAPMethodError – If the server rejects the delete.

delete_task(task_id: str) → None[source]#

Delete a task.

Parameters:

task_id – The JMAP task ID to delete.

Raises:

JMAPMethodError – If the server rejects the delete.

download_attachment(blob_id: str, content_type: str | None = None, filename: str | None = None, account_id: str | None = None) → bytes[source]#

Download a JMAP blob by id (RFC 8620 Section 6.2).

Raw HTTP GET to the Session’s downloadUrl; the response body is the raw binary content.

Parameters:
  • blob_id – The blob id, as returned by upload_attachment().

  • content_type – Media type of the blob, if known. Both Cyrus and Stalwart look up a blob by blobId alone, tested directly against each: an omitted or wrong content_type/ filename only affects the response’s own Content-Type/Content-Disposition headers, not whether the download succeeds.

  • filename – Filename to suggest for the download, if known.

  • account_id – The account owning blob_id.

Returns:

The blob’s raw bytes.

Raises:
  • JMAPCapabilityError – If the server’s Session object has no downloadUrl.

  • JMAPAuthError – On HTTP 401 or 403.

  • requests.HTTPError – On any other non-2xx HTTP response, including 404 for an unknown blob_id (confirmed live on both Cyrus and Stalwart).

get_address_books(account_id: str | None = None) → list[JMAPAddressBook][source]#

Fetch all address books for an account.

A server that doesn’t advertise RFC 9610 Contacts support at all does not raise here: it logs a warning and returns an empty list instead, regardless of account_id. For a server that does support Contacts in general but a different account_id lacks it specifically, a genuine error surfaces normally instead, the same as search_contacts() always does (see _can_skip_contacts_request for why only the session’s own account gets the graceful behavior).

Parameters:

account_id – Pass a different account here to browse address books another user has shared with you. Defaults to the session’s own (calendar) account.

Returns:

List of JMAPAddressBook objects, or [] if Contacts isn’t supported (see above).

Raises:
  • JMAPMethodError – If a different account’s own lack of Contacts support surfaces as a method-level error (e.g. accountNotSupportedByMethod), or the request otherwise fails.

  • requests.HTTPError – If the request otherwise fails at the HTTP level.

get_availability(account_ids: list[str], start: str, end: str, show_details: bool = False) → dict[str, list[BusyInterval]][source]#

Return busy intervals for each of your own accounts over a time period.

Tries Principal/getAvailability (draft-ietf-jmap-calendars section 2.2) first, using the current user’s own Principal id (from the session, no extra round trip). Falls back per account to a CalendarEvent/query scan of that account’s own calendars when the account doesn’t advertise RFC 9670 Principal support at all, the call fails with unknownMethod/accountNotSupportedByMethod, or account_id isn’t the session’s own account. The check for Principal support itself is the base RFC 9670 Section 1.5.1 Principal capability, not the draft’s own narrower :availability sub-capability: confirmed live that Cyrus implements this method fully without ever advertising that sub-capability, so gating on it would wrongly skip Cyrus every time.

Principal/getAvailability never carries an explicit accountId (confirmed live: Cyrus rejects one in this method’s own args), so it can only ever report on the session’s own account; any other entry in account_ids always goes through the fallback, which does scope to an explicit account. Only ever reports your own availability, never another user’s: checking someone else’s by email would need resolving that email to a Principal id via Principal/query, which this client doesn’t implement yet (the same limitation share_calendar() already has for Principal ids in general).

The fallback path builds each interval from a matching event’s own freeBusyStatus (RFC 8984 Section 4.4.2): an event marked "free" is excluded from the result entirely, and an event with no freeBusyStatus at all is treated as "busy", the spec’s own default.

Parameters:
  • account_ids – JMAP accounts to check, one entry in the result per account. Only the session’s own account can use the primary path; other accounts always use the fallback.

  • start – Start of the period, inclusive (YYYY-MM-DDTHH:MM:SS, treated as UTC; matches search_events()’s convention).

  • end – End of the period, exclusive (YYYY-MM-DDTHH:MM:SS, treated as UTC).

  • show_details – If true, populate each interval’s event where permitted. On Stalwart this needs the account’s server to support returning event details at all, verified directly: omitting eventProperties there returns event: None even with show_details=True. On Cyrus, details are returned by default.

Returns:

Dict mapping each account id to its list of BusyInterval objects.

Raises:

JMAPMethodError – If Principal/getAvailability fails for a reason other than lack of support (e.g. forbidden, tooLarge), or if the fallback’s CalendarEvent/query fails (e.g. expandDurationTooLarge, cannotCalculateOccurrences for a window with too many recurrence instances to expand).

get_calendar_subscriptions() → list[JMAPCalendar[Literal[False]]][source]#

Fetch the calendars the authenticated account is subscribed to.

JMAP Calendars has no separate subscription object; this is get_calendars() filtered to is_subscribed.

Returns:

List of JMAPCalendar objects with is_subscribed set.

get_calendars(account_id: str | None = None) → list[JMAPCalendar[Literal[False]]][source]#

Fetch all calendars for an account.

Parameters:

account_id – Pass a different account here to browse calendars another user has shared with you, once share_calendar has granted access; the session only lists accounts you can actually reach (see Session.raw["accounts"]).

Returns:

List of JMAPCalendar objects.

get_event(event_id: str, account_id: str | None = None) → JMAPCalendarObject[source]#

Fetch a calendar event by JMAP event ID.

Parameters:
  • event_id – The JMAP event ID to retrieve.

  • account_id – The account owning event_id; pass the owner’s account here to fetch an event on a calendar shared with you.

Returns:

A JMAPCalendarObject wrapping the raw JSCalendar dict. parent is None since no JMAPCalendar is available at the client level.

Raises:

JMAPMethodError – If the event is not found.

get_event_attachments(event_id: str, account_id: str | None = None) → list[JMAPAttachment][source]#

Return the attachments on a calendar event.

Parameters:
  • event_id – The JMAP event ID to inspect.

  • account_id – The account owning event_id.

Returns:

List of JMAPAttachment, one per links entry whose rel is "enclosure" (see JMAPAttachment.is_attachment for the known Cyrus limitation). Other links entries (e.g. a conference URL) are not included.

Raises:

JMAPMethodError – If the event is not found.

get_objects_by_sync_token(sync_token: str, account_id: str | None = None, max_changes: int | None = None) → tuple[list[JMAPCalendarObject], list[JMAPCalendarObject], list[str], str, str, bool][source]#

Fetch one page of events changed since a previous sync token.

Calls CalendarEvent/changes to discover which events were created, modified, or destroyed since sync_token was issued. Created and modified events are returned as JMAPCalendarObject instances; destroyed events are returned as IDs (the objects no longer exist on the server).

Parameters:
  • sync_token – A state string previously returned by get_sync_token() or by a prior call to this method.

  • account_id – The account to fetch changes for; pass a different account here to sync a calendar shared with you. Must match the account sync_token was issued for.

  • max_changes – Optional upper bound on the number of changes the server returns in this page. The server may return fewer than this regardless; None leaves the page size up to the server. Passing a small value forces a large change set to page across multiple calls, confirmed live against both Cyrus and Stalwart to produce has_more_changes=True.

Returns:

  • added: objects for newly created events (parent is None).

  • modified: objects for updated events (parent is None).

  • deleted: Event IDs that were destroyed.

  • old_state: The sync_token argument echoed back by the server. Compare this against sync_token to detect a server that silently accepted an invalid or stale state instead of reporting an error (RFC 8620 Section 5.2 does not require a server to validate sinceState against a real prior state; confirmed live that Cyrus echoes back whatever value it was given, including one that was never actually issued, rather than rejecting it).

  • new_state: Pass to the next call to this method as sync_token, whether or not has_more_changes is set. Named to match oldState/newState as a pair (RFC 8620 Section 5.2), not sync_token: both names refer to the same kind of value, a state string previously returned by this method or get_sync_token().

  • has_more_changes: If true, the server capped this page and more changes remain; call this method again with new_state to fetch the next page (RFC 8620 Section 5.2).

Return type:

A 6-tuple (added, modified, deleted, old_state, new_state, has_more_changes)

get_sync_token(account_id: str | None = None) → str[source]#

Return the current CalendarEvent state string for use as a sync token.

Calls CalendarEvent/get with an empty ID list, so no event data is transferred, only the state field from the response.

Parameters:

account_id – The account to read the state of; pass a different account here to sync a calendar shared with you, since a JMAP state string is scoped to one account, not the whole session.

Returns:

Opaque state string. Pass to get_objects_by_sync_token() to retrieve only what changed since this point.

get_task(task_id: str) → dict[source]#

Fetch a task by ID.

Parameters:

task_id – The JMAP task ID to retrieve.

Returns:

Raw JMAP Task dict as returned by the server.

Raises:

JMAPMethodError – If the task is not found.

get_task_lists() → list[dict][source]#

Fetch all task lists for the authenticated account.

Returns:

List of raw JMAP TaskList dicts as returned by the server.

get_task_sync_token(account_id: str | None = None) → str[source]#

Return the current Task state string for use as a sync token.

Calls Task/get with an empty ID list, so no task data is transferred, only the state field from the response. Reads the Task type’s own state counter, not TaskList’s: RFC 8620 Section 5.2’s /changes is scoped per type, and get_tasks_by_sync_token() calls Task/changes, which only accepts a state previously returned for Task itself.

Parameters:

account_id – The account to read the state of; pass a different account here to sync a task list shared with you, since a JMAP state string is scoped to one account, not the whole session.

Returns:

Opaque state string. Pass to get_tasks_by_sync_token() to retrieve only what changed since this point.

get_tasks_by_sync_token(sync_token: str, account_id: str | None = None, max_changes: int | None = None) → tuple[list[dict], list[dict], list[str], str, str, bool][source]#

Fetch one page of tasks changed since a previous sync token.

Calls Task/changes to discover which tasks were created, modified, or destroyed since sync_token was issued. Created and modified tasks are returned as raw JMAP Task dicts, matching every other task method’s own return convention; destroyed tasks are returned as IDs.

Parameters:
  • sync_token – A state string previously returned by get_task_sync_token() or by a prior call to this method.

  • account_id – The account to fetch changes for; pass a different account here to sync a task list shared with you. Must match the account sync_token was issued for.

  • max_changes – Optional upper bound on the number of changes the server returns in this page. See get_objects_by_sync_token() for the paging behavior this enables.

Returns:

  • added: raw dicts for newly created tasks.

  • modified: raw dicts for updated tasks.

  • deleted: Task IDs that were destroyed.

  • old_state: The sync_token argument echoed back by the server. See get_objects_by_sync_token() for why this is worth comparing against sync_token yourself.

  • new_state: Pass to the next call to this method as sync_token, whether or not has_more_changes is set. See get_objects_by_sync_token() for why this is named new_state rather than sync_token.

  • has_more_changes: If true, the server capped this page and more changes remain; call this method again with new_state to fetch the next page (RFC 8620 Section 5.2).

Return type:

A 6-tuple (added, modified, deleted, old_state, new_state, has_more_changes)

renew_push(subscription_id: str, expires: str | None = None) → None[source]#

Extend (or shorten) a push subscription’s expiry.

Takes an expires parameter rather than none at all: a renewal with no way to state the new expiry would have nothing to send in the update.

Parameters:
  • subscription_id – The JMAP PushSubscription ID to renew.

  • expires – New UTCDateTime expiry, or None to request no expiration at all (RFC 8620 Section 7.2: null means “no expiration”). The server may cap or otherwise modify the requested value; confirmed live that Stalwart caps a far-future request to its own maximum (around 7 days out).

See subscribe_push() for the account_id parameter note.

Raises:

JMAPMethodError – If the server rejects the update, or the subscription doesn’t exist.

search_contacts(text: str | None = None, email: str | None = None, account_id: str | None = None) → list[JMAPContact][source]#

Search for contact cards.

All parameters are optional; omitting both returns every contact in the account. Results are fetched in a single batched JMAP request using a result reference from ContactCard/query into ContactCard/get, the same pattern search_events() uses.

Unlike get_address_books(), an account that doesn’t advertise RFC 9610 Contacts support raises normally here, not an empty list: this method’s whole purpose is resolving contact information for something the caller is about to act on (e.g. an invitation), so silently returning nothing risks going unnoticed. Tested directly, the actual error is either a request-level requests.HTTPError (HTTP 400, unknownCapability) if the server has no Contacts support at all, or a method-level JMAPMethodError with error_type "accountNotSupportedByMethod" if the server supports Contacts but this particular account doesn’t.

Parameters:
  • text – Free-text search across the whole card. Against a running Cyrus instance, this matches as a substring against the name, not exact-only. Stalwart v0.16.21 behaves differently: it doesn’t match against the card’s name at all, only against the email address (the same reach email already has there), so a name-only search that works on Cyrus can silently return nothing on Stalwart. Use email when the value being searched for might be an email address.

  • email – Match against any address in the card’s emails. Both Cyrus and Stalwart match this as a substring too, verified directly, e.g. "alice" matches "alice@example.com".

  • account_id – Pass a different account here to search an address book shared with you.

Returns:

List of JMAPContact instances.

Raises:
  • JMAPMethodError – If the account doesn’t support Contacts (see above), or the request otherwise fails.

  • requests.HTTPError – If the server has no Contacts support at all (see above).

search_events(calendar_id: str | None = None, start: str | None = None, end: str | None = None, text: str | None = None, has_attachment: bool | None = None, participant_email: str | None = None, participation_status: str | None = None, participant_role: str | None = None, account_id: str | None = None, position: int = 0, limit: int | None = None, calculate_total: bool = False) → EventSearchResult[source]#

Search for calendar events.

All parameters are optional; omitting all returns every event in the account. Results are fetched in a single batched JMAP request using a result reference from CalendarEvent/query into CalendarEvent/get.

has_attachment, participant_email, participation_status, and participant_role have no corresponding FilterCondition property (draft-ietf-jmap-calendars-29 section 5.11.1); they filter client-side, in Python, against every event the server-side filters above already matched, not on the server itself. All filters combine as AND, matching FilterCondition’s own “all must apply” rule, but the three participant-based filters are each satisfied independently across an event’s participants: an event where one participant matches participant_email and a different participant matches participant_role still matches both. Since these filters run after the server-side page is fetched, limit caps the page the server returns, not the number of events left after this client-side filtering, a page can come back smaller than limit once these are applied.

Cyrus drops a Link’s rel: "enclosure" on read-back, confirmed against a running instance, so has_attachment=True misses real attachments there. Both Cyrus and Stalwart drop the attendee role from a participant’s roles map, so participant_role="attendee" misses matches on both servers.

Parameters:
  • calendar_id – Limit results to this calendar.

  • start – Only events ending after this datetime (YYYY-MM-DDTHH:MM:SS).

  • end – Only events starting before this datetime (YYYY-MM-DDTHH:MM:SS).

  • text – Free-text search across title, description, locations, and participants.

  • has_attachment – Only events with (True) or without (False) at least one attachment Link (is_attachment()).

  • participant_email – Only events with a participant matching this email (case-insensitive, falling back to calendarAddress; see _participant_matches_email for the matching rule). Matches against a participant’s email/calendarAddress property, never participantId (a server-assigned id with no email semantics), so no separate contacts lookup is needed to resolve a name to an email before calling this.

  • participation_status – Only events with a participant whose participationStatus equals this value, defaulting an absent value to "needs-action" per RFC 8984 Section 4.4.6.

  • participant_role – Only events with a participant whose roles map has this key set to True.

  • account_id – Pass a different account here to search a calendar shared with you.

  • position – Zero-based index of the first result to return (RFC 8620 Section 5.5). Use with limit to page through a full listing.

  • limit – Maximum number of events the server returns for this page. None means no limit, letting the server return everything (or whatever its own server-side cap is, which this client cannot detect unless calculate_total is also passed).

  • calculate_total – If true, the server includes the total number of matching events in the result, letting a caller detect when it’s seen every page. This count is server-side only; it does not account for has_attachment/participant_email/ participation_status/participant_role, which filter after the server’s own count is taken. See EventSearchResult for how total behaves when this is false.

Returns:

An EventSearchResult. Its events carry parent=None since no JMAPCalendar is available at the client level; use search() if you need parent set.

search_tasks(text: str | None = None, due_before: str | None = None, due_after: str | None = None, progress: str | None = None) → list[dict][source]#

Search for tasks.

All parameters are optional; omitting all returns every task in the account. Tries Task/query first, sending text server-side via its filter. Falls back to fetching every task with Task/get and filtering everything, including text, client-side when the server rejects the request specifically for lacking Task/query, the same fallback shape get_availability() already uses for Principal/getAvailability.

Neither server makes this fallback worth reaching today, tested directly against both: Cyrus and Stalwart reject the entire urn:ietf:params:jmap:tasks capability, not Task/query specifically, so the fallback’s own Task/get call (which needs that same capability) fails identically and the error still propagates. It only helps a future server that implements the tasks capability well enough for Task/get to work but specifically lacks Task/query, a plausible partial-implementation gap given draft-ietf-jmap-tasks section 4.13 itself never defines that method’s filter shape.

due_before, due_after, and progress have no corresponding Task/query filter property (draft-ietf-jmap-tasks section 4.13 never defines one), so they always filter client-side, in Python, against whatever the primary or fallback path already returned. All filters combine as AND.

Parameters:
  • text – Free-text search. Sent server-side via Task/query on the primary path; matched as a case-insensitive substring of title on the fallback path (see _task_matches_search_filters).

  • due_before – Only tasks whose due sorts before this value (RFC 8984 Section 5.2.1 LocalDateTime, YYYY-MM-DDTHH:MM:SS). A task with no due set never matches.

  • due_after – Only tasks whose due sorts after this value. A task with no due set never matches.

  • progress – Only tasks whose progress equals this value, treating an absent value as "needs-action" (RFC 8984 Section 5.2.5’s real default derives from participant progress values; this client never sets a Task’s participants, so that derivation always lands on "needs-action" here).

Returns:

List of raw JMAP Task dicts.

send_invite(calendar_id: str, ical_str: str, account_id: str | None = None, if_in_state: str | None = None) → str[source]#

Create a calendar event and notify its participants.

Identical to create_event(), except the server actually dispatches iTIP invitations to the event’s participants (an iCalendar string with ATTENDEE/ORGANIZER properties). create_event itself never sends scheduling messages; use this instead when the event has participants who need to be notified.

Parameters:
  • calendar_id – The JMAP calendar ID to create the event in.

  • ical_str – A VCALENDAR string representing the event.

  • account_id – The account owning calendar_id, same as create_event().

  • if_in_state – See create_event().

Returns:

The server-assigned JMAP event ID.

Raises:
  • JMAPMethodError – If the server rejects the create request, or with error_type == "noSupportedScheduleMethods" if a participant has no usable delivery method.

  • ValueError – Same conditions as create_event().

set_default_alerts(calendar_id: str, alerts_with_time: dict | None = None, alerts_without_time: dict | None = None, account_id: str | None = None) → None[source]#

Set a calendar’s default alerts for new events.

alerts_with_time applies to timed events, alerts_without_time to all-day events (draft-ietf-jmap-calendars section 4). Each is a map of alert ID to Alert dict (RFC 8984 Section 4.5.2). Pass None to leave a property unchanged; pass {} to clear it.

Parameters:

account_id – The account owning calendar_id; pass the owner’s account here to set default alerts on a calendar shared with you.

Raises:

JMAPMethodError – If the server rejects the update.

share_calendar(calendar_id: str, account_id: str, rights: dict, owning_account_id: str | None = None) → None[source]#

Share a calendar with another account.

account_id must already be a resolved JMAP Principal ID, not an email address. This client has no Principal/query/Principal/get support yet (RFC 9670), so resolving an email address to a Principal ID is left to the caller.

Replaces account_id’s entry in the calendar’s shareWith map wholesale; other accounts already sharing the calendar are left untouched. rights itself is not merged with any rights that account already had, so granting an additional right to an account that is already shared with needs the full rights dict, not just the new key. Requires the caller to already hold the mayShare right, and cannot grant a right the caller does not themselves hold.

Parameters:
  • account_id – The account to grant rights to.

  • rights – Dict of right names to bool, e.g. {"mayReadItems": True}. Replaces any rights this account already had on this calendar.

  • owning_account_id – The account owning calendar_id; pass the owner’s account here to re-share a calendar that was itself shared with you (if you hold mayShare on it).

Raises:

JMAPMethodError – If the server rejects the update, for example with forbidden when the caller lacks mayShare.

subscribe_push(callback_url: str, device_client_id: str, types: list[str] | None = None) → str[source]#

Register a push subscription (RFC 8620 Section 7.2).

This only creates the subscription; it does not complete the verification handshake. Immediately after this call, the server POSTs a PushVerification object (containing a verificationCode) to callback_url. The server will not push any further notifications until that code is submitted via confirm_push_verification(). This client has no way to receive that POST itself, since it is an API client, not an HTTP server; the caller’s own callback_url endpoint must extract the code from the request it receives and pass it along.

Unlike most create/update/delete methods in this client, none of the push subscription methods (this one, confirm_push_verification(), renew_push(), unsubscribe_push()) take an account_id parameter: PushSubscription is not account-scoped (RFC 8620 Section 7.2.2).

Parameters:
  • callback_url – Absolute URL the server will POST notifications to. Must begin with https:// and be publicly reachable by the server: this will not work behind localhost without tunneling.

  • device_client_id – Caller-chosen id identifying the device/app combination creating this subscription, so the caller can recognize its own subscriptions later (e.g. to renew or unsubscribe them) even after losing other local state. Tested against both: neither test server generates or substitutes one, so a fixed value across every install of a library or app would collide across devices; the RFC’s own text requires this to differ per device and per vendor, and to not contain an unobfuscated device id, so it cannot be generated internally on the caller’s behalf.

  • types – List of type names to receive notifications for (e.g. "CalendarEvent"). Defaults to ["CalendarEvent"]. Pass None explicitly for the server behavior of pushing all types (not the default here, since it is a broader behavior than this default).

Returns:

The server-assigned JMAP PushSubscription ID.

Raises:

JMAPMethodError – If the server rejects the create request, or does not implement PushSubscription at all. Cyrus doesn’t implement PushSubscription/set (unknownMethod), confirmed against a running instance; Stalwart does.

tentatively_accept(event_id: str, own_email: str, account_id: str | None = None) → None[source]#

Tentatively accept a meeting invitation, notifying the organizer.

Same as accept_invitation(), setting participationStatus to tentative instead.

unsubscribe_push(subscription_id: str) → None[source]#

Destroy a push subscription.

See subscribe_push() for the account_id parameter note.

Parameters:

subscription_id – The JMAP PushSubscription ID to destroy.

Raises:

JMAPMethodError – If the server rejects the delete, or the subscription doesn’t exist.

update_calendar(calendar_id: str, name: str | None = None, color: str | None = None, timezone: str | None = None, account_id: str | None = None) → None[source]#

Update a calendar’s name, color, or time zone.

name, color, and timeZone are per-user properties (draft-ietf-jmap-calendars section 4.3). Called by the calendar’s owner, this changes the value for everyone until a sharee sets their own override. Called by a sharee, it only ever changes that sharee’s own view; it can never rename or recolor the calendar for the owner or anyone else it is shared with.

Parameters:

account_id – The account owning calendar_id; pass the owner’s account here to update a calendar shared with you.

Raises:

JMAPMethodError – If the server rejects the update.

update_event(event_id: str, ical_str: str, account_id: str | None = None, if_in_state: str | None = None) → None[source]#

Update a calendar event from an iCalendar string.

A property present on the server’s own copy but absent from ical_str is explicitly cleared, not left alone: RFC 8620 Section 5.3 merge semantics mean an omitted property would otherwise be preserved rather than removed, so this nulls it to actually drop it server-side.

Parameters:
  • event_id – The JMAP event ID to update.

  • ical_str – A VCALENDAR string with the updated event data.

  • account_id – The account owning event_id; pass the owner’s account here to update an event on a calendar shared with you.

  • if_in_state – A state string from a prior call that read this account’s calendar state. If the account has changed since, the server rejects the update with JMAPMethodError (error_type == "stateMismatch") instead of applying it (RFC 8620 Section 5.3). The state covers every event in the account, not just this one. None applies unconditionally. Sent unchanged on every retry of the null-key-dropping loop below, since those retries narrow the same patch rather than re-reading the account’s state.

Raises:
  • JMAPMethodError – If the server rejects the update.

  • ValueError – If ical_str is malformed in a way ical_to_jscal rejects (missing UID/DTSTART, a negative duration, mismatched DTSTART/DTEND value types).

update_task(task_id: str, patch: dict) → None[source]#

Update a task with a partial patch.

Parameters:
  • task_id – The JMAP task ID to update.

  • patch – Partial patch dict mapping property names to new values.

Raises:

JMAPMethodError – If the server rejects the update.

upload_attachment(data: bytes, content_type: str, account_id: str | None = None) → str[source]#

Upload binary data as a JMAP blob (RFC 8620 Section 6.1).

Unlike every other method on this client, this is a raw HTTP POST to the Session’s uploadUrl, not a JMAP method call: there is no methodResponses envelope, the server returns a flat JSON object instead.

Parameters:
  • data – The raw bytes to upload.

  • content_type – Media type of data, sent as the request’s Content-Type header.

  • account_id – The account to upload into.

Returns:

The server-assigned blob id, for use with download_attachment() or attach_to_event().

Raises:
  • JMAPCapabilityError – If the server’s Session object has no uploadUrl.

  • JMAPAuthError – On HTTP 401 or 403.

  • requests.HTTPError – On any other non-2xx HTTP response.

Last updated on 2026-10-08.