Skip to main content
Les contacts implémentent la RFC 9610 (urn:ietf:params:jmap:contacts), avec des fiches en objets JSContact (RFC 9553). Les mêmes données sont servies en CardDAV.

Objets et méthodes

Comportements à connaître

  • Le serveur possède l’uid. Un uid fourni par le client à la création est rejeté (invalidProperties) : c’est le serveur qui le frappe, ce qui rend la correspondance CardDAV sans ambiguïté.
  • Une propriété inconnue est refusée nommément, à la création comme à la modification. Une fiche envoyée avec un prénom et un nom séparés, ce qu’un logiciel venu d’ailleurs fait naturellement, répondait succès et existait sans nom : seules les propriétés reconnues avaient été retenues, les autres jamais regardées. Les propriétés que le serveur gère lui-même (les statistiques d’échange) restent refusées par leur contrôle dédié, qui dit pourquoi ; elles ne deviennent pas « inconnues ».
  • Une vCard sans UID est dédupliquée par contenu à l’import. Beaucoup de vieux exports et de CSV convertis n’ont pas d’identifiant, et chaque fiche en recevait un neuf, si bien que ré-importer le même fichier dupliquait tout le carnet. Pour ces fiches-là seulement, la même adresse principale et le même nom complet nomment la fiche existante dans la réponse au lieu d’en créer une, corbeille comprise ; une fiche qui porte un identifiant n’est jamais dédupliquée par contenu (deux fiches distinctes partagent légitimement une adresse), et une fiche sans adresse s’importe comme avant. Même règle pour un import vers un annuaire partagé.
  • addressBookIds est optionnel à la création et ne classe la fiche nulle part. Présent, il doit être une table d’identifiants vers true (la colonne est gardée pour un retour arrière) ; absent, il reste vide. C’est la source de la fiche qui dit où elle appartient.
  • Le filtre source compare la source de la fiche, celle que ContactCard/get sert, sur ContactCard/query comme sur /queryChanges : source: "organization" retrouve les projections d’annuaire (il répondait 0 en cherchant la valeur parmi les clés d’addressBookIds), l’alias v1 addressBookId garde le même sens, et une valeur hors de personal, organization, derived, imported est refusée unsupportedFilter au lieu de répondre une liste vide.
  • Un caractère de contrôle est refusé à l’écriture, par son chemin. Un caractère C0 ou DEL dans name, nicknames, emails, phones, organizations ou titles répond invalidProperties en nommant le chemin JSON et le point de code (emails/0/address: control character U+000D (CR) is not allowed) ; rien n’est rogné, ce qui est stocké est ce qui a été envoyé. La garde vit dans le magasin, si bien que Contact/import, Contact/copy, CardDAV (403 avec CARDDAV:valid-address-data) et l’outil de migration y passent aussi. notes et les adresses postales sont multilignes par nature et ne sont pas jugées. Email/set applique la même règle aux adresses, aux noms d’affichage et aux sujets, si bien que les deux surfaces jugent les mêmes octets de la même façon.
  • Les numéros de téléphone sont rangés sous leur forme internationale. Un numéro que les plans de numérotation reconnaissent est écrit +41 79 222 44 55 par tous les écrivains (Contact/set, Contact/import, Contact/copy, CardDAV, l’outil de migration). Un numéro national est lu dans le pays que l’organisation a déclaré pour elle-même, jamais dans celui de l’instance ; un numéro qui n’est pas valide, qui peut contenir plus d’un numéro, ou qui est disposé comme les numéros d’un autre pays, est gardé tel qu’écrit. Un Contact/set qui a changé un numéro renvoie phones. Le filtre phone trouve un numéro par ses chiffres, quelle que soit la graphie : 079 222, +41 79 222 et 0041 79 222 trouvent la même fiche, et la graphie nationale d’un motif écrit avec son indicatif n’est cherchée qu’au début d’un numéro. Les numéros rangés avant sont convertis par oximail admin contact normalize-phones, et doctor contact_phone_form avertit tant qu’il en reste.
  • Les propriétés en table contiennent des objets. Une entrée de emails, phones, organizations, media ou anniversaries qui n’est pas un objet (une chaîne nue comme ["ada@example.com"]) est refusée en invalidProperties : la RFC 9553 associe chaque identifiant à un objet. Une liste d’objets est acceptée.
  • L’historique de versions est de premier rang. ContactCard/getHistory liste les versions antérieures d’une fiche, restoreVersion en ramène une. Les modifications de n’importe quel protocole (JMAP ou CardDAV) sont versionnées pareil.
  • L’aller-retour est sans perte. Les composantes de nom et d’adresse se transposent complètement entre vCard (3.0/4.0) et JSContact dans les deux sens ; les propriétés vCard sans modèle JSContact sont préservées plutôt qu’abandonnées. Un PUT CardDAV qui dupliquerait un UID existant d’une autre source est refusé, jamais dupliqué en silence.
  • La dépréciation est annoncée honnêtement. L’objet de capacité du compte annonce maxAddressBooksPerCard: 1 et mayCreateAddressBook: false, si bien qu’un client lit les limites au lieu de les découvrir par essais. Les quatre méthodes AddressBook sont des bouchons dépréciés qui répondent forbidden en nommant la dépréciation ; il n’y a pas de successeur, un client abandonne l’appel.
  • La corbeille est en deux temps (ADR-035), et le balayage des corbeilles détruit ce qui est à la corbeille depuis plus longtemps que l’horizon du locataire, sa surcharge contacts.trash_retention_days ou à défaut [contacts] trash_retention_days (30 par défaut, plancher 1 ; une surcharge à 0 ne purge rien), par la sortie du Drive, séquestres respectés et changement Destroyed annoncé (horizons par locataire).
  • « Dernier échange » regarde en arrière. Les compteurs d’échange (interactionCount, lastInteractionAt) n’étaient alimentés que vers l’avant, si bien que des correspondants de longue date affichaient « jamais » ; oximail admin contact backfill-interactions les sème depuis le courrier existant, au même grain que le souscripteur au fil de l’eau (destinataires des messages envoyés, expéditeur des messages reçus, jamais les brouillons), de façon monotone (ne fait jamais reculer un compteur ni une date, donc rejouable), ne touche que les fiches existantes, simulation par défaut et borné en comptes par --limit.

Les annuaires de tenant

Une organisation peut publier des jeux de contacts partagés (« Clients », « Personnel ») tenus sur le plan d’administration. Le choix de conception qui structure toute la surface : un annuaire n’est pas un conteneur dans lequel les membres classent des fiches. Les membres ne voient aucun conteneur. Ce qu’un membre voit, c’est son propre carnet d’adresses, où les entrées arrivent en fiches lisibles seulement, avec source: "organization" et directorySource: "tenant:<directoryId>" : le plan même qu’alimente un connecteur LDAP ou SCIM externe. Un client qui affiche déjà les contacts d’origine organisationnelle n’a aucun écran à ajouter. Qui écrit quoi. Les objets annuaire sont réservés au rôle d’administrateur de tenant. Les entrées sont réservées aux editorIds de leur annuaire, qui sont des identifiants de principal : y nommer un groupe est valide et courant, et l’appartenance est résolue par l’expansion des bénéficiaires au moment du contrôle d’accès, jamais stockée déjà expansée. Ajouter quelqu’un à un groupe lui donne donc le droit d’éditer sans toucher à l’annuaire. Deux comportements à connaître avant de bâtir dessus :
  • mayEdit est calculé pour vous, et vous ne pouvez pas le calculer vous-même. Il suit le rôle de tenant de l’appelant et l’expansion de groupes côté serveur : deux comptes lisent donc légitimement des valeurs différentes pour le même annuaire, et le dériver depuis editorIds est impossible, cette liste étant aveugle à l’appartenance aux groupes par conception. C’est exactement pour cela qu’il est déclaré dans le canon publié au lieu d’être laissé à la déduction depuis le type TypeScript.
  • La comptabilité du serveur ne vous appartient pas. La propriété card porte la fiche JSContact et rien d’autre : les clés de projection comme source ou directoryId posées à l’intérieur sont refusées, pas écrasées en silence.
Le contenu d’un annuaire est une donnée de l’organisation, pas une donnée personnelle détenue pour le compte d’un membre : il se purge en supprimant des entrées, et il ne suit pas l’effacement d’un compte individuel. Les règles de rigueur sont identiques à la surface courrier : identifiants inconnus dans notFound, invalidProperties plutôt qu’une coercition, accountNotFound, stateMismatch.