Skip to main content

JMAP Conditional Set
draft-gondwana-jmap-conditional-00

Document Type Active Internet-Draft (individual)
Author Bron Gondwana
Last updated 2026-06-20
RFC stream (None)
Intended RFC status (None)
Formats
Stream Stream state (No stream defined)
Consensus boilerplate Unknown
RFC Editor Note (None)
IESG IESG state I-D Exists
Telechat date (None)
Responsible AD (None)
Send notices to (None)
draft-gondwana-jmap-conditional-00
Network Working Group                                        B. Gondwana
Internet-Draft                                                  Fastmail
Updates: 8620 (if approved)                                 20 June 2026
Intended status: Standards Track                                        
Expires: 22 December 2026

                          JMAP Conditional Set
                   draft-gondwana-jmap-conditional-00

Abstract

   The JMAP base protocol ([JMAP-CORE]) provides the Foo/set method for
   creating, updating, and destroying objects.  It offers a single
   concurrency control, the "ifInState" argument, which guards an entire
   object type: if any object of that type has changed, the whole method
   is rejected.

   This extension adds a finer, per-object conditional mechanism.  A
   client may require that an individual update or destroy proceed only
   if the target object still matches a set of expected property values,
   expressed using the JMAP PatchObject already defined for updates.
   This provides optimistic concurrency control scoped to a single
   object — the equivalent of an HTTP "If-Match" precondition — for any
   JMAP data type.

Status of This Memo

   This Internet-Draft is submitted in full conformance with the
   provisions of BCP 78 and BCP 79.

   Internet-Drafts are working documents of the Internet Engineering
   Task Force (IETF).  Note that other groups may also distribute
   working documents as Internet-Drafts.  The list of current Internet-
   Drafts is at https://datatracker.ietf.org/drafts/current/.

   Internet-Drafts are draft documents valid for a maximum of six months
   and may be updated, replaced, or obsoleted by other documents at any
   time.  It is inappropriate to use Internet-Drafts as reference
   material or to cite them other than as "work in progress."

   This Internet-Draft will expire on 22 December 2026.

Copyright Notice

   Copyright (c) 2026 IETF Trust and the persons identified as the
   document authors.  All rights reserved.

Gondwana                Expires 22 December 2026                [Page 1]
Internet-Draft              JMAP Conditional                   June 2026

   This document is subject to BCP 78 and the IETF Trust's Legal
   Provisions Relating to IETF Documents (https://trustee.ietf.org/
   license-info) in effect on the date of publication of this document.
   Please review these documents carefully, as they describe your rights
   and restrictions with respect to this document.  Code Components
   extracted from this document must include Revised BSD License text as
   described in Section 4.e of the Trust Legal Provisions and are
   provided without warranty as described in the Revised BSD License.

Table of Contents

   1.  Introduction  . . . . . . . . . . . . . . . . . . . . . . . .   2
     1.1.  Conventions Used in This Document . . . . . . . . . . . .   3
   2.  Addition to the Capabilities Object . . . . . . . . . . . . .   3
     2.1.  urn:ietf:params:jmap:conditional  . . . . . . . . . . . .   3
       2.1.1.  Capability Example  . . . . . . . . . . . . . . . . .   4
   3.  Conditional Foo/set . . . . . . . . . . . . . . . . . . . . .   4
     3.1.  Evaluation Rules  . . . . . . . . . . . . . . . . . . . .   4
     3.2.  Effect  . . . . . . . . . . . . . . . . . . . . . . . . .   5
     3.3.  The "stateMismatch" SetError  . . . . . . . . . . . . . .   5
   4.  Examples  . . . . . . . . . . . . . . . . . . . . . . . . . .   6
     4.1.  Force-with-lease on file content  . . . . . . . . . . . .   6
     4.2.  Conditional destroy . . . . . . . . . . . . . . . . . . .   7
     4.3.  A condition that ignores unrelated changes  . . . . . . .   7
     4.4.  Whole-object compare-and-swap . . . . . . . . . . . . . .   7
   5.  IANA Considerations . . . . . . . . . . . . . . . . . . . . .   8
     5.1.  JMAP Capability Registration for "conditional"  . . . . .   8
   6.  Security Considerations . . . . . . . . . . . . . . . . . . .   8
     6.1.  Information Disclosure  . . . . . . . . . . . . . . . . .   8
     6.2.  Resource Consumption  . . . . . . . . . . . . . . . . . .   8
   7.  References  . . . . . . . . . . . . . . . . . . . . . . . . .   8
     7.1.  Normative References  . . . . . . . . . . . . . . . . . .   8
     7.2.  Informative References  . . . . . . . . . . . . . . . . .   9
   Author's Address  . . . . . . . . . . . . . . . . . . . . . . . .   9

1.  Introduction

   The Foo/set method defined in [JMAP-CORE] is the mechanism by which
   clients create, update, and destroy objects.  It provides one form of
   concurrency control: the optional "ifInState" argument, which aborts
   the entire method if the account's state string for that data type
   does not match the value the client supplies.

Gondwana                Expires 22 December 2026                [Page 2]
Internet-Draft              JMAP Conditional                   June 2026

   This guard is coarse.  The state string changes whenever _any_ object
   of that type changes, by any client.  In a busy account — or one that
   receives server-initiated changes, such as the arrival of new mail —
   the state can change between a client's read and its write for
   reasons entirely unrelated to the object the client is modifying.
   Using "ifInState" to protect a single update therefore leads to
   frequent spurious rejections and retries.

   Clients commonly need a narrower guarantee: "apply this change only
   if the specific object I read still holds the values I depend on".
   This is the same need met by the HTTP "If-Match" precondition
   ([HTTP-SEMANTICS]) and the entity-tag model of WebDAV ([WEBDAV]): a
   conditional write scoped to a single resource.

   This document defines a generic, per-object conditional mechanism for
   Foo/set.  It introduces no new properties and no new version tokens.
   Instead it reuses the PatchObject already defined by [JMAP-CORE] for
   updates: a client states its precondition as a PatchObject describing
   the values it expects the object to currently hold, and the server
   performs the change only if applying that patch would change nothing.

   Because the mechanism is defined entirely in terms of an object's
   properties and the existing PatchObject semantics, it applies
   uniformly to every data type that implements Foo/set, with no per-
   type additions.

1.1.  Conventions Used in This Document

   The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
   "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
   "OPTIONAL" in this document are to be interpreted as described in
   BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
   capitals, as shown here.

   The terms "Id", "PatchObject", "SetError", and "Foo/set" are defined
   in [JMAP-CORE].  A "pointer" is a JSON Pointer as used within a
   PatchObject (see [JMAP-CORE], Section 5.3).

2.  Addition to the Capabilities Object

2.1.  urn:ietf:params:jmap:conditional

   The presence of the "urn:ietf:params:jmap:conditional" property in
   the "capabilities" object of the JMAP Session resource indicates
   support for the conditional Foo/set behaviour defined in this
   document.  The value of this property MUST be an empty object.

Gondwana                Expires 22 December 2026                [Page 3]
Internet-Draft              JMAP Conditional                   June 2026

   A client indicates that it wishes to use this extension by including
   the capability URI in the "using" array of a request.  When a server
   that advertises this capability receives a request whose "using"
   array includes this URI, it MUST honour the "ifUnchangedBy" argument
   (Section 3) on every Foo/set method it implements.

   The capability is generic: it does not depend on any particular data
   type, and its presence applies to all types for which the server
   implements Foo/set.

2.1.1.  Capability Example

   "urn:ietf:params:jmap:conditional": {}

3.  Conditional Foo/set

   This document adds one argument to the standard Foo/set method
   ([JMAP-CORE], Section 5.3):

   *  ifUnchangedBy: "Id[PatchObject]" (default: an empty object)

      A map of object id to a PatchObject expressing a precondition on
      that object.  For each entry, the precondition is satisfied if and
      only if applying the given PatchObject to the current server-side
      object would leave the object unchanged.

      Equivalently: for every pointer in the PatchObject, the object's
      current value at that pointer MUST equal the value given in the
      PatchObject.  A JSON "null" value matches a property that is not
      present (it asserts that removing the property would be a no-op).
      Comparison uses the same representation the server would return
      for that property from Foo/get.

3.1.  Evaluation Rules

   1.  Preconditions are evaluated against the state of each object as
       it exists at the start of the method, before any create, update,
       or destroy in the same method is applied.

   2.  The "ifUnchangedBy" argument applies only to objects identified
       by an existing id.  Each id used as a key in "ifUnchangedBy" MUST
       also appear as a key in the "update" argument or as a member of
       the "destroy" argument of the same method.  If an id appears in
       "ifUnchangedBy" but in neither, the server MUST reject the whole
       method with an "invalidArguments" error.  A creation id (an id
       prefixed with "#") MAY be used as a key in "ifUnchangedBy" to
       reference an object created by an earlier method call in the same
       request; the server resolves it using the creation id mechanism

Gondwana                Expires 22 December 2026                [Page 4]
Internet-Draft              JMAP Conditional                   June 2026

       ([JMAP-CORE], Section 5.3).  This lets a client condition a
       change on an object it created earlier in the request not having
       been changed by another actor in the interval between method
       calls.

   3.  Unlike the PatchObject supplied in the "update" argument, which
       may only reference properties the client is permitted to set, the
       PatchObject in "ifUnchangedBy" MAY reference any property of the
       object, including server-set and otherwise read-only properties
       (for example a content identifier, a size, or a server-maintained
       change timestamp).  This allows a client to condition a change on
       properties it cannot itself modify.

   4.  If a pointer in an "ifUnchangedBy" PatchObject is not a valid
       pointer for the object's type, the server MUST treat the affected
       id as it would an invalid update: the id is added to "notUpdated"
       or "notDestroyed" with an "invalidPatch" SetError.

   5.  The "ifUnchangedBy" argument is independent of, and composes
       with, the method-level "ifInState" argument.  If both are
       supplied, the server first checks "ifInState" (rejecting the
       whole method with a "stateMismatch" error if it does not match),
       and then evaluates the per-object preconditions.

3.2.  Effect

   If the precondition for an id is satisfied, the corresponding update
   or destroy proceeds exactly as it would without this extension.

   If the precondition is not satisfied, the server MUST NOT perform the
   corresponding update or destroy.  It MUST instead add the id to
   "notUpdated" (if the id appeared in "update") or "notDestroyed" (if
   the id appeared in "destroy"), with a SetError of type
   "stateMismatch" (Section 3.3).

   As with all Foo/set processing, each id is handled independently: a
   failed precondition on one id does not prevent other ids in the same
   method, whose preconditions are satisfied and which are otherwise
   valid, from succeeding.

3.3.  The "stateMismatch" SetError

   This document defines a new SetError type, "stateMismatch", for use
   in the "notUpdated" and "notDestroyed" maps of a Foo/set response.
   It indicates that an "ifUnchangedBy" precondition for the id was not
   satisfied: the object's current server-side state differs from the
   state the client asserted.

Gondwana                Expires 22 December 2026                [Page 5]
Internet-Draft              JMAP Conditional                   June 2026

   The SetError object has no properties beyond the common "type"
   property and an optional "description".  In particular it does not
   echo the object's current values; a client that needs them fetches
   the current state of the object (for example via Foo/get or Foo/
   changes) before deciding how to proceed.

   Note: [JMAP-CORE] uses the term "stateMismatch" for the method-level
   error returned when "ifInState" does not match.  The SetError defined
   here is the per-object analogue.  The two are distinguished by their
   position in the response: a method-level error object versus a
   SetError appearing within "notUpdated" or "notDestroyed".

4.  Examples

4.1.  Force-with-lease on file content

   A client last synchronised a file node "f42" with content blob
   "G_old".  It has since produced new content "G_new", and wishes to
   replace the content only if no other client has changed it in the
   meantime.  It conditions on the server-set "blobId"
   ([JMAP-FILENODE]):

   [[ "FileNode/set", {
     "accountId": "u1",
     "ifUnchangedBy": { "f42": { "blobId": "G_old" } },
     "update":       { "f42": { "blobId": "G_new",
                                "modified": "2026-05-01T09:30:00Z" } }
   }, "0" ]]

   If "f42" still references "G_old", the update succeeds.  If another
   client has already replaced the content (so "blobId" is now, say,
   "G_other"), the precondition fails and the server makes no change:

   [[ "FileNode/set", {
     "accountId": "u1",
     "oldState": "f1a2",
     "newState": "f1a2",
     "updated": null,
     "notUpdated": { "f42": { "type": "stateMismatch" } }
   }, "0" ]]

   The client fetches "f42", resolves the conflict (for instance by
   preserving its own version as a separate file), and retries.

Gondwana                Expires 22 December 2026                [Page 6]
Internet-Draft              JMAP Conditional                   June 2026

4.2.  Conditional destroy

   Destroy a message only if it has not been read on another device.  In
   [JMAP-MAIL], an unread message has no "$seen" keyword, so the client
   asserts that "keywords/$seen" is absent:

   [[ "Email/set", {
     "accountId": "u1",
     "ifUnchangedBy": { "M7": { "keywords/$seen": null } },
     "destroy":      [ "M7" ]
   }, "0" ]]

   If the message has since been marked "$seen", "M7" is returned in
   "notDestroyed" with a "stateMismatch" SetError and is not deleted.

4.3.  A condition that ignores unrelated changes

   Because the precondition references only the pointers the client
   supplies, concurrent changes to other properties of the same object
   do not cause it to fail.  Here a client removes a grantee's access,
   but only if that grantee currently holds exactly the rights the
   client saw:

   [[ "FileNode/set", {
     "accountId": "u1",
     "ifUnchangedBy": { "d3": { "shareWith/bob/mayRead": true } },
     "update":       { "d3": { "shareWith/bob": null } }
   }, "0" ]]

   The change applies only if Bob currently has "mayRead" of "true".  A
   concurrent change to an unrelated property of "d3" (its name, or
   another grantee's rights) does not trigger a "stateMismatch".

4.4.  Whole-object compare-and-swap

   A data type that maintains a server-set change token can be used to
   require that nothing at all has changed.  For example, a type with a
   server-maintained "changed" timestamp that is updated on every
   modification:

   "ifUnchangedBy": { "f42": { "changed": "2026-04-30T12:00:00Z" } }

   Because the server updates "changed" on any modification to the
   object, this asserts that the entire object is unchanged since the
   client last read it, recovering the coarse "If-Match" behaviour as a
   special case of the general mechanism.

Gondwana                Expires 22 December 2026                [Page 7]
Internet-Draft              JMAP Conditional                   June 2026

5.  IANA Considerations

5.1.  JMAP Capability Registration for "conditional"

   IANA is requested to register the "conditional" JMAP Capability as
   follows, in the "JMAP Capabilities" registry established by
   [JMAP-CORE]:

   Capability Name:  urn:ietf:params:jmap:conditional

   Specification document:  this document

   Intended use:  common

   Change Controller:  IETF

   Security and privacy considerations:  this document, Section 6

6.  Security Considerations

6.1.  Information Disclosure

   The conditional mechanism returns no object data.  A failed
   precondition yields only a "stateMismatch" SetError with no payload,
   disclosing nothing beyond the single fact that the asserted state did
   not hold — information the client could already obtain by reading the
   object with Foo/get.

   A precondition does not bypass access control.  A server MUST require
   the same permissions to read the properties referenced in an
   "ifUnchangedBy" PatchObject as it would to return them from Foo/get.
   In particular, a server MUST NOT evaluate a precondition on a
   property the requesting client is not permitted to read; such a
   condition MUST fail with a "forbidden" SetError, so that the
   mechanism cannot be used as an oracle to probe the values of
   properties the client cannot otherwise see.

6.2.  Resource Consumption

   Evaluating a precondition is comparable in cost to validating an
   update PatchObject and imposes no significant additional load.

7.  References

7.1.  Normative References

Gondwana                Expires 22 December 2026                [Page 8]
Internet-Draft              JMAP Conditional                   June 2026

   [JMAP-CORE]
              Jenkins, N. and C. Newman, "The JSON Meta Application
              Protocol (JMAP)", RFC 8620, DOI 10.17487/RFC8620, July
              2019, <https://www.rfc-editor.org/rfc/rfc8620>.

   [RFC2119]  Bradner, S., "Key words for use in RFCs to Indicate
              Requirement Levels", BCP 14, RFC 2119,
              DOI 10.17487/RFC2119, March 1997,
              <https://www.rfc-editor.org/rfc/rfc2119>.

   [RFC8174]  Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
              2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
              May 2017, <https://www.rfc-editor.org/rfc/rfc8174>.

7.2.  Informative References

   [HTTP-SEMANTICS]
              Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke,
              Ed., "HTTP Semantics", STD 97, RFC 9110,
              DOI 10.17487/RFC9110, June 2022,
              <https://www.rfc-editor.org/rfc/rfc9110>.

   [JMAP-FILENODE]
              Gondwana, B., "JMAP File Storage extension", Work in
              Progress, Internet-Draft, draft-ietf-jmap-filenode-14, 15
              May 2026, <https://datatracker.ietf.org/doc/html/draft-
              ietf-jmap-filenode-14>.

   [JMAP-MAIL]
              Jenkins, N. and C. Newman, "The JSON Meta Application
              Protocol (JMAP) for Mail", RFC 8621, DOI 10.17487/RFC8621,
              August 2019, <https://www.rfc-editor.org/rfc/rfc8621>.

   [WEBDAV]   Dusseault, L., Ed., "HTTP Extensions for Web Distributed
              Authoring and Versioning (WebDAV)", RFC 4918,
              DOI 10.17487/RFC4918, June 2007,
              <https://www.rfc-editor.org/rfc/rfc4918>.

Author's Address

   Bron Gondwana
   Fastmail
   Email: brong@fastmailteam.com

Gondwana                Expires 22 December 2026                [Page 9]