Supported exhaustive AddressBook/Calendar enumeration with capped JMAP get

Your question

We are qualifying a local macOS integration with the unmodified official Stalwart
0.16.23 arm64 binary. Our application uses public JMAP and DAV interfaces only.
We need a reliable full backup of an account, including empty collections and
collections with identical display names, without reading internal store files.

Observed and source-supported behavior

  • Email, Mailbox, SieveScript, ContactCard and CalendarEvent can be enumerated by
    paginated query, then read with explicit ids and state checks. We have tested
    this against the official binary with getMaxResults=3 and queryMaxResults=2.
  • AddressBook/get and Calendar/get with null/omitted ids apply get_max_objects
    in the pinned source. We did not find corresponding query handlers in its tree.
  • Our real-binary test with two collections and getMaxResults=2 returns two rows.
    The same response shape cannot establish whether further collections exist;
    our application therefore refuses to claim a complete inventory at that limit.
  • Starting changes from initial state n takes Query::All over the stored
    changelog. The cannotCalculateChanges check for a truncated log excludes
    State::Initial. We have not tested log purge in this experiment and do not
    claim that this route provides an exhaustive current inventory.
  • DAV discovery supplies collection URLs and display names. We have not found a
    documented, stable mapping from those URLs to the JMAP ids, especially for
    empty or homonymous collections. We will not infer ids by decoding URLs.

Pinned primary references (condensed to respect the forum’s new-user link limit):

Source paths in that tag: crates/jmap/src/addressbook/get.rs; crates/jmap/src/calendar/get.rs; crates/jmap/src/changes/get.rs; crates/store/src/query/log.rs; crates/jmap-proto/src/types/state.rs; crates/dav/src/common/propfind.rs.

Questions

  1. What supported public interface enumerates every current AddressBook and
    Calendar id when the account contains more collections than maxObjectsInGet?
  2. Is initial-state changes intended to support full enumeration after history
    retention/purge? If so, which invariant guarantees that all surviving
    collections appear, and which versions provide it?
  3. Is there a supported DAV property or documented mapping that identifies the
    corresponding JMAP collection id, without relying on display names or contents?
  4. Are there relevant fixes or supported extensions in later official releases,
    and what version and capability checks should an integration use?
  5. If the intended deployment requires increasing getMaxResults, is a complete
    response explicitly distinguishable from a capped one, and what resource
    limits should the application enforce?

A useful answer would include the API/extension name, affected versions,
capability detection, empty/shared/homonymous collection behavior, and a small
reproduction or test reference. No patch or custom engine fork is requested.

Proposed acceptance fixture — not yet executed as a whole

Create seven calendars and seven address books, including empty and homonymous
collections; set getMaxResults=3 and restart only the isolated test engine.
Enumerate by the proposed interface, read all returned ids explicitly and verify
properties and relationships against the known creation responses. Repeat after
collection deletion, log retention/purge and a concurrent change, then restore
from the encrypted backup to a separate account. Incomplete or ambiguous results
must be detectable; no silent omission is acceptable.

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

This confirms AddressBook/query and Calendar/query exist (not just Get). Let’s check QueryResponseBuilder to see if total is reported when capped by query_max_results.

This confirms Calendar/query and AddressBook/query exist. Let’s check the query.rs build/limit logic and whether response indicates truncation.

I now have solid grounding. AddressBook/query and Calendar/query methods do exist (routed through address_book_query / calendar_query, not get.rs) and return total (when calculateTotal:true) and a limit field that is explicitly set only when the result set was truncated (if total_results > limit). This directly answers questions 1 and 5.

Short answer to Q1 (the key one): use AddressBook/query and Calendar/query, not AddressBook/get/Calendar/get with null ids.

Both handlers exist in the pinned tag (crates/jmap/src/contact/query.rs::address_book_query and crates/jmap/src/calendar_event/query.rs::calendar_query), even though they live in the contact/calendar_event query modules rather than addressbook/calendar directories, which is presumably why they weren’t found by directory name alone. They iterate the full container id set from the DAV cache (cache.document_ids(true)), including empty and homonymous collections, and build a response through the shared QueryResponseBuilder, honoring queryMaxResults (default 5000) rather than getMaxObjects/getMaxResults (default 500, used only by /get). So the null-id get path you tested is capped by getMaxResults; the paginated query path is capped by the much larger queryMaxResults, and supports position/limit/anchor for full pagination if a single account ever exceeds that.

Q5 — is a complete response distinguishable from a capped one: Yes, mechanically. QueryResponseBuilder::build() (crates/jmap/src/api/query.rs) only sets response.limit = Some(limit) when total_results > limit, i.e. the limit field in the JMAP response is present if and only if the result was truncated. If you additionally send calculateTotal:true, response.total gives you the real total count, so total compared against the number of returned ids (plus any further pages) tells you unambiguously whether you’ve seen everything. There is no separate “isComplete” flag; presence/absence of limit plus total is the supported signal. We’d suggest your integration always set calculateTotal:true on AddressBook/query/Calendar/query and page with position/limit until ids.len() reaches total.

Q2 — initial-state Changes: In crates/jmap/src/changes/get.rs, AddressBook/Calendar are valid MethodObjects for Changes, and the max window is changesMaxResults. However, note that both address_book_query/calendar_query always report query_state: State::Initial (hardcoded, not a real changelog cursor) — the query methods themselves are not stateful. Changes from State::Initial does take Query::All over the changelog per your reading, but we did not find, in this pass, an explicit invariant statement that changelog retention/purge preserves full-inventory guarantees for containers specifically; we can’t confirm this without testing log purge behavior directly, which the source alone doesn’t settle. Given that, and given AddressBook/query / Calendar/query already give you exhaustive current-state enumeration without depending on changelog retention, we’d recommend treating query (not changes) as your authoritative full-inventory mechanism, and using changes only for incremental sync between two known states.

Q3 — DAV property mapping to JMAP id: No such documented property was found in crates/dav-proto/src/schema/property.rs (the enumerated WebDavProperty/CardDavProperty/CalDavProperty sets have no “resource id” or “JMAP id” property). Internally DAV resource lookups resolve hrefs to document_id via fetch_dav_resources(...).by_path(...) (crates/dav/src/common/uri.rs), but this is not exposed as a documented, stable client-facing property. Your decision not to decode/rely on hrefs for this purpose is correct based on what’s in the current source.

Q4 — later fixes/extensions: We did not find anything in the current docs describing a dedicated extension or capability flag for exhaustive collection enumeration beyond ordinary JMAP query pagination described above.

Reference: Protocol | Stalwart (Object limits: getMaxResults, queryMaxResults, changesMaxResults, setMaxObjects) and the source paths cited above.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

This has been fixed in the v1.0.0 branch but unfortunately the fix cannot be backported to v0.16. v1.0.0 will be released at the end of this month or early November.