Skip to main content
Sharing implements RFC 9670 (urn:ietf:params:jmap:principals): a Principal directory of the people, teams, resources, and locations of the organization, and a uniform grant model every collection type reuses.

Objects and methods

The grant model

Every shareable collection (Mailbox, Calendar, TaskList, AddressBook, FileNode, Label) carries the same shareWith map: principal id → rights object (mayRead, mayWrite, mayAdmin, plus domain-specific rights like mayReadFreeBusy). Grants are:
  • Written by the owner on the collection object; each grant change produces a ShareNotification for the grantee and a recorded change for resync.
  • Exercised through the shared: indirection: a grantee addresses the owner’s data with the granted account id in the standard JMAP accountId slot; the session’s accounts object lists what is reachable. Capabilities resolve on the authenticated caller, data residency on the owner — usurpation is not representable in the request shape.
  • Fail-closed. Shared-account dispatch denies by default; a rights lookup error refuses rather than granting.

Directory semantics worth knowing

  • Principal types are honest. A shared team account’s principal is type: "other" (non-loginnable), never individual — people-pickers can rely on the type.
  • Grantees must be human-backed. A grant to a principal backed by a non-loginnable account is rejected at write time on every domain.
  • Destroying a principal purges its grants. A Principal/set destroy removes the grantee’s keys from every shareWith map — no dangling ACL entries — and warns or aborts if it would leave a shared drive without an eligible custodian.
  • Erasure leaves tombstones. An erased person’s principal is PII-scrubbed but kept as a graph anchor, and a tombstoned principal is refused as a new grantee (compliance).
  • Principal/getAvailability (free-busy) is documented on the calendar page.
ShareNotification/query returns a total order (newest first). Strictness rules are identical to the mail surface.

Across organizations: federation

Everything above shares data inside one organization. Sharing with a different organization is a separate surface, urn:oximail:params:jmap:federation, and it is deliberately not the same mechanism: a federated grant is neither a shareWith entry nor an account-access row. At v0.30.0 what it carries is availability (free-busy) — the grant widens who may ask, never what they may see. The capability object advertises the vocabulary rather than making clients discover it from rejections:

The grantee is an organization, not a person

supportedGranteeKinds is ["realm"], and the reason is not schedule. The identity a federated request proves today is the sending node, not which of its principals acted — so a grant naming one person at the far end would not actually be narrower than a grant naming their organization; it would only look narrower in an interface. The list exists so that the day a per-principal identity layer lands, "principal" is an addition a client can detect rather than a behaviour change it has to guess. granteeValue must be a realm you have actually paired with. Pairing itself is a bilateral, out-of-band operator ceremony (it is not a JMAP method), which is exactly why FederationPeer exists: without a listing, a client had either a free-text field where almost every entry is rejected, or a selector with nothing to fill it. FederationPeer is tenant-scoped and readable by any authenticated account of the organization, not administrators only — a peering is a relationship of the organization, with no per-subject view. It carries realm (the value to send as granteeValue), jurisdiction, state, and pairedAt. It deliberately does not expose the pinned key, the pairing anchor, the key epoch, or the suspension cause: those are operator bookkeeping, and a public key on every consent screen is security surface bought for no feature.
FederationPeer has no changes method and its state is not a change-log counter — it is a digest of the served list. Pairing and suspension happen in operator ceremonies outside any JMAP request, so nothing writes a change-log row for them; a change-log state here would be a string that never moves while the data under it does.
  • Only the subject may create or modify a grant, acting on their own account. An administrator may revoke — revocation removes access, so an operator who must cut a leak does not wait for the subject — but never grant on someone’s behalf. A grant an administrator can create for you is not consent. The check is stricter than a role test: a request carrying shared access is somebody acting for someone else, whatever level it holds.
  • Each write is one transaction: the grant row, the ShareNotification that tells the subject, and the audit row naming the writer commit together. The audit records who actually wrote it, not the account the request targeted, so an administrator’s revocation is never attributed to the subject.
  • An unsupported granteeKind is refused loudly. "principal" parses as a well-formed request and would look accepted; the row it created would be a realm grant wearing a person’s name.
Most of a grant is server-decided and read-only on the wire — only granteeKind, granteeValue and expiresAt are settable. rights in particular is not: a client that could widen its own consent record past what the resolver honours would be a permission wearing a setting’s clothes. expiresAt: null means the subject explicitly chose no expiry, and jurisdiction is frozen at grant time — consent was given to a counterparty in a place, and a mutable field would let the place change without new consent.

Why a grant may stop working, and how the subject learns it

FederationGrant.state answers “why can that organization no longer see me?” directly: active, unpaired, expired, dormancy_exhausted, dormant, lapsed or reanchored. The store derives it from the very predicate the authorization read applies, so it cannot drift from the decision. FederationPeer.state (active, dormant, lapsed) uses the same words for the same failures, and only active accepts a grant — again derived from the predicate the write refuses on, so a realm the selector offers is a realm the write takes. Two consequences worth designing around:
  • Revocation is unilateral and instant, and forward-only. A peer never reads or writes your grants; it learns of a revocation by being refused. That is what keeps revocation out of distributed-consistency territory — and it is also why revocationIsForwardOnly is advertised as data: revoking removes the read path going forward and undoes nothing already disclosed.
  • The disclosure log is the other half of that sentence. FederationDisclosure is what makes “revoking undoes nothing already disclosed” checkable instead of merely reassuring: it survives the grant, and there is no FederationDisclosure/set at all, not even destroy-only. Retention bounds the log; the subject cannot erase it. It records disclosures, never attempts, so a peer that probes five hundred addresses creates no rows about the four hundred and ninety-nine people who never consented. Each row names the peer realm, the identity layer that was verified, the jurisdiction the grant stood under at that moment, and when it happened.
Filters and sorts on the two query methods are refused (unsupportedFilter / unsupportedSort) rather than silently ignored. A client that filtered peers on state: "active" and quietly received the dormant ones too would offer its user a grantee the write then rejects.