> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oximail.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# JMAP v2 extensions

> OxiMail's modernized v2 surface as it is served today: per-capability negotiation, dual-capability method aliases, and the v2 capability objects (labels, rules, conversations, admin, resource limits…).

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](../reference/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](./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

| Capability (`urn:oximail:params:jmap:v2:…`) | Unlocks                                                                                                                                                                                    |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `mail`                                      | The modernized mail shape on `Email/get` / `Email/set` (served as a dual-capability bridge with RFC 8621).                                                                                 |
| `labels`                                    | `Label/get`, `set`, `query`, `changes` — user labels as first-class objects with `shareWith`, distinct from keywords.                                                                      |
| `rules`                                     | `Rule/get`, `set`, `query`, `changes`, `apply`, `test` — the structured filtering layer ([Rules & Sieve](../operator/sieve-rules)).                                                        |
| `conversation`                              | `Conversation/*` and `ConversationMessage/*` — a channel-agnostic timeline aggregating mail and chat, with `archive`, `link`/`unlink`, `merge`, `markRead`.                                |
| `threads`                                   | Thread-level operations beyond RFC 8621 `Thread/get`.                                                                                                                                      |
| `admin`                                     | The admin-over-JMAP surface (ADR-075): `Tenant/get`/`set`, `AuditLog/query`, `AccountStorage/get`, admin `Principal/*`, `JournalingRule/get`/`set`. Requires an admin session.             |
| `resource-limits`                           | Advertised server limits as data, so clients bound their behaviour before hitting errors.                                                                                                  |
| `idempotency`                               | Persistent `clientId` dedup on replayable creates (safe client retries, offline-first).                                                                                                    |
| `provenance`                                | Build provenance on the session (`{sourceUrl, license, commit}` of the running binary).                                                                                                    |
| `streaming`                                 | `Push/catchup` (one-call reconnect reconciliation) and `Core/queryStream` (cursor-paced walks), both with optional inline `fetchProperties` objects.                                       |
| `push-patches`                              | Server-initiated **PushPatch** frames over the WebSocket: push carries the changed data itself, eliminating the post-push `Foo/changes` + `Foo/get` round trips.                           |
| `projection`                                | Conformance marker: the unified property-selection model (per-type closed vocabulary, `*` wildcard) holds across every v2 object type — see [the projection canon](#the-projection-canon). |
| `e2e`                                       | `{"serverSidePreviewSuppressed": true}` — the server's contract that it never generates previews from end-to-end-encrypted bodies.                                                         |
| `calendar`, `contacts`                      | v2 counterparts of the calendar and contacts surfaces (dual-capability with the standard ones).                                                                                            |

For each capability, the authoritative wire contract is what the session advertises plus the method behaviours — the strictness rules ([mail page](./jmap-mail)) 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:

| Emitted              | What it is                                                                                                                                                                                                            |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PROJECTABLE_<Type>` | The exact vocabulary that type accepts. Emitted **only** for types whose vocabulary is closed: a partial canon would be the same lie it replaces. `Email` has two, one for `properties` and one for `bodyProperties`. |
| `DERIVED_<Type>`     | The wire properties the handler **computes for the caller** and injects after serialization — name, TypeScript type, and the condition under which it appears.                                                        |
| `<Type>Wire`         | The augmented type: the struct plus its derived properties.                                                                                                                                                           |

`DERIVED_<Type>` is the part a client cannot reconstruct on its own. `mayEdit` on a [tenant directory](./jmap-contacts#tenant-contact-directories) 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.

<Note>
  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).
</Note>
