Skip to main content
Alongside the standard JMAP capabilities, OxiMail serves a family of v2 extension capabilities under 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 accountCapabilities entry 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:rules carries the rule editor’s whole vocabulary: supportedFields (18 condition fields) and supportedActions (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:admin is only present in accountCapabilities for admin-role accounts — its absence is how a client knows not to render an admin surface at all. It advertises supportedPrincipalRoles, the retention facts (softDeleteRetentionDays: 30, auditLogRetentionDays: 2557 — seven years), and mfaRequiredForSensitiveOperations: 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 like v2:admin) advertises the tamper-evident audit log: {hashAlgorithm: "sha-256", verificationSupported: true, retentionDays: 2557}. AuditEvent/verify recomputes the hash chain and reports the first altered or missing entry.
  • v2:mail advertises the enforced sending bounds — maxSizeEmail (50 MiB, also pre-announced through the SMTP SIZE extension), maxScheduleDelay (one year on Email/send sendAt) — and emailQuerySortOptions, the exact sort vocabulary Email/query accepts. The v2 Email object also carries expiresAt (auto-trash at a chosen instant) and Email/send accepts requestMdn (RFC 8098 read receipts) and tlsRequired (RFC 8689 REQUIRETLS, including scheduled sends).
  • v2:streaming advertises replayWindowSeconds (the real change-log retention horizon, 90 days — a Push/catchup from any state inside the window replays exactly what was missed), maxQueryStreamChunk: 1000, and the push-subscription verification pacing.
  • v2:push-patches advertises maxPatchSize (1 MiB), maxBatchEntries: 100, maxPatchesPerSecond: 10, supportedTransports: ["websocket"] and maxConcurrentStreams: 0 (no server-pushed query streams — the client-paced Core/queryStream is the streaming mode). A client opts in per WebSocket connection with EnablePushPatches {types, properties}; updated entries then carry exactly the changed properties, and degraded cases fall back to a plain StateChange (reconcile via Push/catchup).
  • v2:resource-limits now only advertises limits something actually enforces, from the same constants the handlers apply — including maxTotalRequestsPerMinute, injected from the operator’s effective rate-limit configuration (absent when no rate limiter runs). maxConcurrentRequests lives on the core capability (RFC 8620) and is enforced per account on both JMAP transports.
Threads and Conversations gained first-class organization surfaces during the same cycle: per-thread 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).