urn:oximail:params:jmap:v2:*. This page describes the surface as built — what the server actually negotiates and serves at v0.30.0. The extensions are being written up as Internet-Drafts for IETF submission; see IETF drafts for the programme.
How v2 is negotiated
v2 is not a version switch — it is per-capability opt-in, exactly like any JMAP capability:- A client requests a v2 capability in
using; the methods it unlocks become callable in that request. Nothing about v1 behaviour changes for clients that never ask. - Method names are dual-capability aliases where a v1 and a v2 capability cover the same handler: the registry maps both spellings onto one implementation, so there is exactly one behaviour per method, whichever capability unlocked it.
- The mutual-exclusion set is empty at v0.30.0: pairing a v1 capability with its v2 counterpart in the same request is allowed (see JMAP Core for the mechanism).
- As with everything else, the session object is the source of truth: each account’s
accountCapabilitiesentry for a v2 URN carries that capability’s advertised limits and options. Feature-detect from the session, never from the server version.
The v2 capability objects
For each capability, the authoritative wire contract is what the session advertises plus the method behaviours — the strictness rules (mail page) apply to v2 methods identically: unknown arguments and properties are errors, never silently ignored.
What the capability objects advertise
The session’s capability objects are self-describing — clients build their UI from them instead of hardcoding server knowledge. The load-bearing entries as served at v0.30.0:v2:rulescarries the rule editor’s whole vocabulary:supportedFields(18 condition fields) andsupportedActions(15 action types) — a client renders its rule builder from these lists — plus the enforced bounds (maxRulesPerAccount: 1000,maxActionsPerRule: 20,maxConditionDepth: 20,maxRuleNameLength: 256,perRuleExecutionTimeoutMs,maxApplyEmailsPerCall).v2:adminis only present inaccountCapabilitiesfor admin-role accounts — its absence is how a client knows not to render an admin surface at all. It advertisessupportedPrincipalRoles, the retention facts (softDeleteRetentionDays: 30,auditLogRetentionDays: 2557— seven years), andmfaRequiredForSensitiveOperations: true: role changes and tenant mutations by an account that has enrolled a passkey require a passkey-authenticated session.urn:ietf:params:jmap:auditlog(admin-gated likev2:admin) advertises the tamper-evident audit log:{hashAlgorithm: "sha-256", verificationSupported: true, retentionDays: 2557}.AuditEvent/verifyrecomputes the hash chain and reports the first altered or missing entry.v2:mailadvertises the enforced sending bounds —maxSizeEmail(50 MiB, also pre-announced through the SMTPSIZEextension),maxScheduleDelay(one year onEmail/sendsendAt) — andemailQuerySortOptions, the exact sort vocabularyEmail/queryaccepts. The v2 Email object also carriesexpiresAt(auto-trash at a chosen instant) andEmail/sendacceptsrequestMdn(RFC 8098 read receipts) andtlsRequired(RFC 8689 REQUIRETLS, including scheduled sends).v2:streamingadvertisesreplayWindowSeconds(the real change-log retention horizon, 90 days — aPush/catchupfrom any state inside the window replays exactly what was missed),maxQueryStreamChunk: 1000, and the push-subscription verification pacing.v2:push-patchesadvertisesmaxPatchSize(1 MiB),maxBatchEntries: 100,maxPatchesPerSecond: 10,supportedTransports: ["websocket"]andmaxConcurrentStreams: 0(no server-pushed query streams — the client-pacedCore/queryStreamis the streaming mode). A client opts in per WebSocket connection withEnablePushPatches {types, properties};updatedentries then carry exactly the changed properties, and degraded cases fall back to a plainStateChange(reconcile viaPush/catchup).v2:resource-limitsnow only advertises limits something actually enforces, from the same constants the handlers apply — includingmaxTotalRequestsPerMinute, injected from the operator’s effective rate-limit configuration (absent when no rate limiter runs).maxConcurrentRequestslives on the core capability (RFC 8620) and is enforced per account on both JMAP transports.
muted (inherited by newly ingested messages) and pinned, Thread/split, Thread/merge, and /queryChanges on every queryable v2 type.
The projection canon
Every/get validates its properties argument against a closed, per-type vocabulary, before it reads the store. An unknown name comes back as invalidArguments naming the offenders; id is always accepted (RFC 8621 makes it implicit), and * selects all top-level properties.
It is an error rather than a shrug for a precise reason. Under the older serialize-then-filter pattern, a misspelled property produced a success with that property absent — indistinguishable from a property the server had chosen not to serve. A client could not tell a typo from a server limitation, and neither could anyone reading the logs.
A closed vocabulary is only useful if you can read it, so the canon is published in machine form next to the generated TypeScript types:
DERIVED_<Type> is the part a client cannot reconstruct on its own. mayEdit on a tenant directory is computed per caller from their tenant role and a server-side group expansion; no struct field carries it, so the TypeScript type alone says nothing about it. The registry lists only genuine out-of-struct injections — a property that is a struct field rewritten per viewer, such as myRights, is already typed and deliberately absent from it.
The published canon is an additive-only contract, the same rule as the --json CLI surface. v2:projection on the session is the conformance marker that the model holds across every type.
Wire stability: within the v0.30 line, v2 shapes evolve additively — new optional properties and new methods, no renames and no semantic changes to served fields. Hardening steps that turn a previously-ignored argument into an error are staged (log first, reject after clients are confirmed clean).