Internet-Draft JMAP Contacts April 2024
Jenkins Expires 14 October 2024 [Page]
Workgroup:
JMAP
Internet-Draft:
draft-ietf-jmap-contacts-07
Published:
Intended Status:
Standards Track
Expires:
Author:
N.M. Jenkins, Ed.
Fastmail

JMAP for Contacts

Abstract

This document specifies a data model for synchronising contacts data with a server using JMAP.

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 14 October 2024.

1. Introduction

JMAP ([RFC8620] JSON Meta Application Protocol) is a generic protocol for synchronising data, such as mail, calendars or contacts, between a client and a server. It is optimised for mobile and web environments, and aims to provide a consistent interface to different data types.

This specification defines a data model for synchronising contacts between a client and a server using JMAP.

1.1. Notational conventions

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.

Type signatures, examples and property descriptions in this document follow the conventions established in Section 1.1 of [RFC8620]. The Id and UnsignedInt data types defined in Sections 1.2 and 1.3 of [RFC8620] are also used in this document.

1.2. Terminology

The same terminology is used in this document as in the core JMAP specification, see [RFC8620], Section 1.6.

The terms AddressBook and ContactCard (with these specific capitalizations) are used to refer to the data types defined in this document and instances of those data types.

1.3. Data Model Overview

An Account (see [RFC8620], Section 1.6.2) with support for the contacts data model contains zero or more AddressBook objects, which is a named collection of zero or more ContactCards. A ContactCard is a representation of a person, company, or other entity, or a group of such entities, in JSContact Card format, as defined in Section 2 of [I-D.ietf-calext-jscontact]. Each ContactCard belongs to one or more AddressBooks.

In servers with support for JMAP Sharing [I-D.ietf-jmap-sharing], users may see and configure sharing of contact data with others. Sharing permissions are managed per AddressBook.

1.4. Addition to the Capabilities Object

The capabilities object is returned as part of the JMAP Session object; see [RFC8620], Section 2. This document defines one additional capability URI.

1.4.1. urn:ietf:params:jmap:contacts

This represents support for the AddressBook and ContactCard data types and associated API methods. The value of this property in the JMAP Session capabilities property is an empty object.

The value of this property in an account's accountCapabilities property is an object that MUST contain the following information on server capabilities and permissions for that account:

  • maxAddressBooksPerCard: UnsignedInt|null

    The maximum number of AddressBooks (see Section 2) that can be can assigned to a single ContactCard object (see Section 3). This MUST be an integer >= 1, or null for no limit (or rather, the limit is always the number of AddressBooks in the account).

  • mayCreateAddressBook: Boolean

    If true, the user may create an AddressBook in this account.

2. AddressBooks

An AddressBook is a named collection of ContactCards. All ContactCards are associated with one or more AddressBook.

A AddressBook object has the following properties:

  • id: Id (immutable; server-set)

    The id of the AddressBook.

  • name: String

    The user-visible name of the AddressBook. This MUST NOT be the empty string and MUST NOT be greater than 255 octets in size when encoded as UTF-8.

  • description: String|null (default: null)

    An optional longer-form description of the AddressBook, to provide context in shared environments where users need more than just the name.

  • sortOrder: UnsignedInt (default: 0)

    Defines the sort order of AddressBooks when presented in the client's UI, so it is consistent between devices. The number MUST be an integer in the range 0 <= sortOrder < 231.

    An AddressBook with a lower order is to be displayed before a AddressBook with a higher order in any list of AddressBooks in the client's UI. AddressBooks with equal order should be sorted in alphabetical order by name. The sorting should take into account locale-specific character order convention.

  • isDefault: Boolean (server-set)

    This SHOULD be true for exactly one AddressBook in any account, and MUST NOT be true for more than one AddressBook within an account. The default AddressBook should be used by clients whenever they need to choose an AddressBook for the user within this account, and they do not have any other information on which to make a choice. For example, if the user creates a new contact card, the client may automatically set the card as belonging to the default AddressBook from the user's primary account.

  • isSubscribed: Boolean

    True if the user has indicated they wish to see this AddressBook in their client. This should default to false for AddressBooks in shared accounts the user has access to and true for any new AddressBooks created by the user themself.

    If false, the AddressBook and its contents should only be displayed when the user explicitly requests it or to offer it for the user to subscribe to.

  • shareWith: Id[AddressBookRights]|null (default: null)

    A map of Principal (Section 2 of [I-D.ietf-jmap-sharing]) id to rights for principals this AddressBook is shared with. The principal to which this AddressBook belongs MUST NOT be in this set. This is null if the AddressBook is not shared with anyone, or the server does not support [I-D.ietf-jmap-sharing]. The value may be modified only if the user has the mayAdmin right. The account id for the principals may be found in the urn:ietf:params:jmap:principals:owner capability of the Account to which the AddressBook belongs.

  • myRights: AddressBookRights (server-set)

    The set of access rights the user has in relation to this AddressBook.

An AddressBookRights object has the following properties:

  • mayRead: Boolean

    The user may fetch the ContactCards in this AddressBook.

  • mayWrite: Boolean

    The user may create, modify or destroy all ContactCards in this AddressBook, or move them to or from this AddressBook.

  • mayAdmin: Boolean

    The user may modify the "shareWith" property for this AddressBook.

  • mayDelete: Boolean

    The user may delete the AddressBook itself.

2.1. AddressBook/get

This is a standard "/get" method as described in [RFC8620], Section 5.1. The ids argument may be null to fetch all at once.

2.2. AddressBook/changes

This is a standard "/changes" method as described in [RFC8620], Section 5.2.

2.3. AddressBook/set

This is a standard "/set" method as described in [RFC8620], Section 5.3 but with the following additional request argument:

  • onDestroyRemoveContents: Boolean (default: false)

    If false, any attempt to destroy an AddressBook that still has ContactCard in it will be rejected with an addressBookHasContents SetError. If true, any ContactCards that were in the AddressBook will be removed from it, and if in no other AddressBooks they will be destroyed.

  • onSuccessSetIsDefault: Id|null

    If an id is given, and all creates, updates and destroys (if any) succeed without error, the server will try to set this AddressBook as the default. (For references to AddressBook creations, this is equivalent to a creation-reference, so the id will be the creation id prefixed with a "#".)

If the id is not found, or the change is not permitted by the server for policy reasons, it MUST be ignored and the currently default AddressBook (if any) will remain as such. No error is returned to the client in this case.

As per [RFC8620], Section 5.3, if the default is successfully changed, any changed objects MUST be reported in either the "created" or "updated" argument in the response as appropriate, with the server-set value included.

The "shareWith" property may only be set by users that have the mayAdmin right. When modifying the shareWith property, the user cannot give a right to a principal if the principal did not already have that right and the user making the change also does not have that right. Any attempt to do so MUST be rejected with a forbidden SetError.

Users can subscribe or unsubscribe to an AddressBook by setting the "isSubscribed" property. The server MAY forbid users from subscribing to certain AddressBooks even though they have permission to see them, rejecting the update with a forbidden SetError.

The following extra SetError type is defined:

For "destroy":

  • addressBookHasContents: The AddressBook has at least one ContactCard assigned to it, and the "onDestroyRemoveContents" argument was false.

3. ContactCards

A ContactCard object contains information about a person, company, or other entity, or represents a group of such entities. It is a JSContact Card object, as defined in Section 2 of [I-D.ietf-calext-jscontact], with the following additional properties:

  • id: Id (immutable; server-set)

    The id of the ContactCard. The id uniquely identifies a ContactCard with a particular "uid" within a particular account.

  • addressBookIds: Id[Boolean]

    The set of AddressBook ids this ContactCard belongs to. A card MUST belong to at least one AddressBook at all times (until it is destroyed). The set is represented as an object, with each key being an AddressBook id. The value for each key in the object MUST be true.

For any Media object in the card (see Section 2.6.4 of [I-D.ietf-calext-jscontact]), a new property is defined:

  • blobId: Id

    An id for the Blob representing the binary contents of the resource.

When returning ContactCards, any Media with a data: URI SHOULD return a blobId property and omit the uri property, as this lets clients load the (potentially large) image file only when needed, and avoids the overhead of Base64 encoding. The "mediaType" property MUST also be set. Similarly, when creating or updating a ContactCard, clients MAY send a blobId instead of the uri property for a Media object.

A contact card with a "kind" property equal to "group" represents a group of contacts. Clients often present these separately from other contact cards. The "members" property, as defined in Section 2.1.6 of [I-D.ietf-calext-jscontact], contains a set of UIDs for other contacts that are the members of this group. Clients should consider the group to contain any ContactCard with a matching UID, from any account they have access to with support for the urn:ietf:params:jmap:contacts capability. UIDs that cannot be found SHOULD be ignored but preserved. For example, suppose a user adds contacts from a shared address book to their private group, then temporarily loses access to this address book. The UIDs cannot be resolved so the contacts will disappear from the group. However, if they are given permission to access the data again the UIDs will be found and the contacts will reappear.

3.1. ContactCard/get

This is a standard "/get" method as described in [RFC8620], Section 5.1.

3.2. ContactCard/changes

This is a standard "/changes" method as described in [RFC8620], Section 5.2.

3.3. ContactCard/query

This is a standard "/query" method as described in [RFC8620], Section 5.5.

3.3.1. Filtering

A FilterCondition object has the following properties, any of which may be omitted:

  • inAddressBook: Id

    An AddressBook id. A card must be in this address book to match the condition.

  • uid: String

    A card must have this string exactly as its uid to match.

  • hasMember: String

    A card must have a "members" property that contains this string as one of the uids in the set to match.

  • kind: String

    A card must have a type property that equals this string exactly to match.

  • createdBefore: UTCDate

    The "created" date-time of the ContactCard must be before this date-time to match the condition.

  • createdAfter: UTCDate

    The "created" date-time of the ContactCard must be the same or after this date-time to match the condition.

  • updatedBefore: UTCDate

    The "updated" date-time of the ContactCard must be before this date-time to match the condition.

  • updatedAfter: UTCDate

    The "updated" date-time of the ContactCard must be the same or after this date-time to match the condition.

  • text: String

    A card matches this condition if the text matches with text in the card.

  • name: String

    A card matches this condition if the value of any NameComponent in the "name" property, or the "full" property in the "name" property of the card matches the value.

  • name/given: String

    A card matches this condition if the value of a NameComponent with kind "given" inside the "name" property of the card matches the value.

  • name/surname: String

    A card matches this condition if the value of a NameComponent with kind "surname" inside the "name" property of the card matches the value.

  • name/surname2: String

    A card matches this condition if the value of a NameComponent with kind "surname2" inside the "name" property of the card matches the value.

  • nickName: String

    A card matches this condition if the "name" of any NickName in the "nickNames" property of the card matches the value.

  • organization: String

    A card matches this condition if the "name" of any Organization in the "organizations" property of the card matches the value.

  • email: String

    A card matches this condition if the "address" or "label" of any EmailAddress in the "emails" property of the card matches the value.

  • phone: String

    A card matches this condition if the "number" or "label" of any Phone in the "phones" property of the card matches the value.

  • onlineService: String

    A card matches this condition if the "service", "uri", "user", or "label" of any OnlineService in the "onlineServices" property of the card matches the value.

  • address: String

    A card matches this condition if the value of any StreetComponent in the "street" property, or the "locality", "region", "country", or "postcode" property in any Address in the "addresses" property of the card matches the value.

  • note: String

    A card matches this condition if the "note" of any Note in the "notes" property of the card matches the value.

If zero properties are specified on the FilterCondition, the condition MUST always evaluate to true. If multiple properties are specified, ALL must apply for the condition to be true (it is equivalent to splitting the object into one-property conditions and making them all the child of an AND filter operator).

The exact semantics for matching String fields is deliberately not defined to allow for flexibility in indexing implementation, subject to the following:

  • Text SHOULD be matched in a case-insensitive manner.
  • Text contained in either (but matched) single or double quotes SHOULD be treated as a phrase search, that is a match is required for that exact sequence of words, excluding the surrounding quotation marks. Use \", \' and \\ to match a literal ", ' and \ respectively in a phrase.
  • Outside of a phrase, white-space SHOULD be treated as dividing separate tokens that may be searched for separately in the contact, but MUST all be present for the contact to match the filter.
  • Tokens MAY be matched on a whole-word basis using stemming (so for example a text search for bus would match "buses" but not "business").

3.3.2. Sorting

The following value for the "property" field on the Comparator object MUST be supported for sorting:

  • "created" - The "created" date on the ContactCard.
  • "updated" - The "updated" date on the ContactCard.

The following values for the "property" field on the Comparator object SHOULD be supported for sorting:

  • "name/given" - The value of the first NameComponent in the "name" property whose "kind" is "given".
  • "name/surname" - The value of the first NameComponent in the "name" property whose "kind" is "surname".
  • "name/surname2" - The value of the first NameComponent in the "name" property whose "kind" is "surname2".

3.4. ContactCard/queryChanges

This is a standard "/queryChanges" method as described in [RFC8620], Section 5.6.

3.5. ContactCard/set

This is a standard "/set" method as described in [RFC8620], Section 5.3.

To set a new photo, the file must first be uploaded using the upload mechanism as described in [RFC8620], Section 6.1. This will give the client a valid blobId/size/type to use. The server MUST reject attempts to set a file that is not a recognised image type as the photo for a card.

3.6. ContactCard/copy

This is a standard "/copy" method as described in [RFC8620], Section 5.4.

4. Examples

For brevity, in the following examples only the "methodCalls" property of the Request object, and the "methodResponses" property of the Response object is shown.

4.1. Fetching initial data

A user has authenticated and the client has fetched the JMAP Session object. It finds a single Account with the "urn:ietf:params:jmap:contacts" capability, with id "a0x9", and wants to fetch all the address books and contacts. It might make the following request:

[
  ["AddressBook/get", {
    "accountId": "a0x9"
  }, "0"],
  ["ContactCard/get", {
    "accountId": "a0x9"
  }, "1"]
]

The server might respond with something like:

[
  ["AddressBook/get", {
    "accountId": "a0x9",
    "list": [{
        "id": "062adcfa-105d-455c-bc60-6db68b69c3f3",
        "name": "Personal",
        "description": null,
        "sortOrder": 0,
        "isDefault": true,
        "isSubscribed": true,
        "shareWith": null,
        "myRights": {
          "mayRead": true,
          "mayWrite": true,
          "mayAdmin": true,
          "mayDelete": false
        }
      }, {
        "id": "cd40089d-35f9-4fd7-980b-ba3a9f1d74fe",
        "name": "Autosaved",
        "description": null,
        "sortOrder": 1,
        "isDefault": false,
        "isSubscribed": true,
        "shareWith": null,
        "myRights": {
          "mayRead": true,
          "mayWrite": true,
          "mayAdmin": true,
          "mayDelete": false
        }
      }],
      "notFound": [],
      "state": "~4144"
  }, "0"],
  ["Contact/get", {
    "accountId": "a0x9",
    "list": [{
        "id": "3",
        "addressBookIds": {
          "062adcfa-105d-455c-bc60-6db68b69c3f3": true
        },
        "name": {
          "components": [
            { "kind": "given", "value": "Joe" },
            { "kind": "surname", "value": "Bloggs" }
          ],
          "isOrdered": true
        },
        "emails": {
          "0": {
          "contexts": {
            "private": true
          },
          "address": "joe.bloggs@example.com"
          }
        }
      }],
      "notFound": [],
      "state": "ewarbckaqJ::112"
  }, "1"]
]

4.2. Changing the default address book

The client tries to change the default address book from "Personal" to "Autosaved" (and makes no other change):

[
  ["AddressBook/set", {
    "accountId": "a0x9",
    "onSuccessSetIsDefault": "cd40089d-35f9-4fd7-980b-ba3a9f1d74fe"
  }, "0"]
]

The server allows the change, returning the following response:

[
  ["AddressBook/set", {
    "accountId": "a0x9",
    "updated": {
      "cd40089d-35f9-4fd7-980b-ba3a9f1d74fe": {
        "isDefault": true
      },
      "062adcfa-105d-455c-bc60-6db68b69c3f3": {
        "isDefault": false
      }
    }
  }, "0"]
]

5. Internationalisation considerations

Experience has shown that unrestricted use of Unicode can lead to problems such as inconsistent rendering, users reading text and interpreting it differently than intended, and unexpected results when copying text from one location to another. Servers MAY choose to mitigate this by restricting the set of characters allowed in otherwise unconstrained String fields. The FreeformClass, as documented in [RFC7564], Section 4.3 may be a good starting point for this.

Attempts to set a value containing code points outside of the permissible set can be handled in a few ways by the server. The first option is to simply strip the forbidden characters and store the resulting string. This is likely to be appropriate for control characters for example, where they can end up in data accidentally due to copy-and-paste issues, and are probably invisible to the end user. JMAP allows the server to transform data on create/update, as long as any changed properties are returned to the client in the /set response, so it knows what has changed, as per [RFC8620], Section 5.3. Alternatively, the server MAY just reject the create/update with an invalidProperties SetError.

6. Security considerations

All security considerations of JMAP ([RFC8620]) apply to this specification. Additional considerations specific to the data types and functionality introduced by this document are described in the following subsection.

Contacts consist almost entirely of private, personally identifiable information, and represent the social connections of users. Privacy leaks can have real world consequences, and contacts servers and clients MUST be mindful of the need to keep all data secure.

Servers MUST enforce the ACLs set on address books to ensure only authorised data is shared.

7. IANA Considerations

7.1. JMAP capability registration for "contacts"

IANA will register the "contacts" JMAP Capability as follows:

Capability Name: urn:ietf:params:jmap:contacts

Specification document: this document

Intended use: common

Change Controller: IETF

Security and privacy considerations: this document, Section 6

7.2. JMAP Data Type Registration for "AddressBook"

IANA will register the "AddressBook" JMAP Data Type as follows:

Type Name: AddressBook

Can reference blobs: no

Can Use for State Change: yes

Capability: urn:ietf:params:jmap:contacts

Specification document: this document

7.3. JMAP Data Type Registration for "ContactCard"

IANA will register the "ContactCard" JMAP Data Type as follows:

Type Name: ContactCard

Can reference blobs: yes

Can Use for State Change: yes

Capability: urn:ietf:params:jmap:contacts

Specification document: this document

7.4. JMAP Error Codes Registry

The following subsection registers a new error code in the "JMAP Error Codes" registry, as defined in [RFC8620].

7.4.1. addressBookHasContents

JMAP Error Code: addressBookHasContents

Intended use: common

Change controller: IETF

Reference: This document, Section 2.3

Description: The AddressBook has at least one ContactCard assigned to it, and the "onDestroyRemoveContents" argument was false.

7.5. JSContact Property Registrations

IANA will register the following additional properties in the JSContact Properties Registry.

7.5.1. id

Property Name: id

Property Type: Not applicable

Property Context: Card

Intended Use: Reserved

Since Version: 1.0

Change Controller: IETF

7.5.2. addressBookIds

Property Name: addressBookIds

Property Type: Not applicable

Property Context: Card

Intended Use: Reserved

Since Version: 1.0

Change Controller: IETF

7.5.3. blobId

Property Name: blobId

Property Type: Not applicable

Property Context: Media

Intended Use: Reserved

Since Version: 1.0

Change Controller: IETF

8. Normative References

[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/info/rfc2119>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/info/rfc8174>.
[RFC8620]
Jenkins, N. and C. Newman, "The JSON Meta Application Protocol (JMAP)", RFC 8620, DOI 10.17487/RFC8620, , <https://www.rfc-editor.org/info/rfc8620>.
[I-D.ietf-jmap-sharing]
Jenkins, N., "JMAP Sharing", Work in Progress, Internet-Draft, draft-ietf-jmap-sharing-08, , <https://datatracker.ietf.org/api/v1/doc/document/draft-ietf-jmap-sharing/>.
[I-D.ietf-calext-jscontact]
Stepanek, R. and M. Loffredo, "JSContact: A JSON representation of contact data", Work in Progress, Internet-Draft, draft-ietf-calext-jscontact-17, , <https://datatracker.ietf.org/doc/html/draft-ietf-calext-jscontact-17>.

9. Informative References

[RFC7564]
Saint-Andre, P. and M. Blanchet, "PRECIS Framework: Preparation, Enforcement, and Comparison of Internationalized Strings in Application Protocols", RFC 7564, DOI 10.17487/RFC7564, , <https://www.rfc-editor.org/info/rfc7564>.

Author's Address

Neil Jenkins (editor)
Fastmail
PO Box 234, Collins St West
Melbourne VIC 8007
Australia