Skip to main content
Le partage implémente la RFC 9670 (urn:ietf:params:jmap:principals) : un annuaire de Principals (les personnes, équipes, ressources et lieux de l’organisation) et un modèle de droits uniforme que chaque type de collection réutilise.

Objets et méthodes

Le modèle de droits

Chaque collection partageable (Mailbox, Calendar, TaskList, FileNode, Label) porte la même carte shareWith : identifiant de principal vers objet de droits (mayRead, mayWrite, mayAdmin, plus des droits de domaine comme mayReadFreeBusy). Les droits sont :
  • Écrits par le propriétaire sur l’objet collection ; chaque changement de droit produit une ShareNotification pour le bénéficiaire et un changement enregistré pour la resynchronisation.
  • Exercés par l’indirection shared: : un bénéficiaire adresse les données du propriétaire avec l’identifiant de compte accordé dans le slot accountId JMAP standard ; l’objet accounts de la session liste ce qui est joignable. Les capacités se résolvent sur l’appelant authentifié, la résidence des données sur le propriétaire : l’usurpation n’est pas représentable dans la forme de la requête.
  • Fermés par défaut. Le dispatch des comptes partagés refuse par défaut ; une erreur de recherche de droits refuse plutôt que d’accorder.
  • Un appel composé est admis pour lui-même. Une méthode qui en exécute une autre en son sein (le /get derrière les fetchProperties de Core/queryStream, les patchs de push, les reports d’alerte) fait passer à cette méthode interne la garde des comptes partagés et son admission propre, exactement comme si le client l’avait envoyée : hériter du contexte de l’appel extérieur, ce n’est pas hériter de son admission. Le bénéficiaire d’un seul dossier partagé lisait les règles ou les contacts du propriétaire par Core/queryStream ; le refus interne revient maintenant en forbidden. Les patchs de push ne servent plus Thread/get, VacationResponse/get, EmailSubmission/get ni CalendarEventNotification/get à un bénéficiaire, qui reçoit à la place un StateChange sans contenu. Chaque appel composé laisse aussi sa propre ligne dans le journal des accès aux données, rangée sous le propriétaire avec le bénéficiaire pour acteur ; dans une organisation en mode strict, une réponse dont la ligne ne peut pas être écrite est retenue (serverFail).
  • Un flux de requête reste sur son compte. Chaque page d’un Core/queryStream ouvert sur shared:<owner>, cadencé par le client ou poussé par le serveur, vient de ce compte sous la même admission que la première : un flux poussé servait la page 1 depuis le propriétaire et les suivantes depuis le compte du bénéficiaire. Le curseur porte le compte où il a été ouvert et il est refusé en invalidArguments sur un autre ; un droit retiré pendant le flux l’arrête à la page suivante par streamFailed, qui nomme l’erreur.

Les octrois de compte, une collection entière

oximail account grant est la délégation de l’exploitant : un compte atteint la collection d’un autre compte (Mailbox, Calendar, TaskList, FileNode, Notebook, ou celle d’une extension), à un niveau, avec une échéance facultative, sous le même identifiant de compte shared:<owner>. C’est le mécanisme derrière l’accès à la boîte lors d’un départ et derrière l’accès d’un connecteur (multi-tenant). Ce qu’il ouvre, sous [rights] enforce (avant cela, un octroi n’est que journalisé) :
  • Chaque objet de la collection, ceux créés après l’octroi compris : chaque dossier, agenda, liste de tâches, carnet de notes, le Drive entier. readOnly lit ; readWrite lit, écrit et détruit les éléments, chaque destruction inscrite dans la chaîne d’audit du propriétaire avec l’octroi, et ne partage, n’administre, n’envoie de courrier ni de message d’agenda (une écriture que seul l’octroi admet et qui enverrait une invitation est refusée). Une ligne au niveau admin, écrite hors du verbe, s’applique comme readWrite, jamais plus.
  • En JMAP seulement. IMAP, CalDAV et CardDAV n’ouvrent aucun objet sur un octroi de compte ; les téléchargements de blobs, les vignettes et les envois suivent l’admission JMAP du type.
  • Jamais Contact. JMAP ne sert aucun contact sur un compte partagé, donc un octroi de contacts annoncerait un accès qui n’ouvre rien : account grant --collection Contact est refusé et dit pourquoi. Une équipe partage un carnet d’adresses par un annuaire de tenant.
  • Ses états lui appartiennent. Chaque état servi sous un octroi porte l’empreinte de l’octroi (réponses des méthodes, appels composés, WebSocket, EventSource et patchs de push) : un état obtenu avant l’octroi, sous un autre octroi ou à un autre niveau répond cannotCalculateChanges, et le client se resynchronise au lieu de lire un delta calculé pour d’autres droits. Sans octroi, les états restent identiques à l’octet près.
La console dit la même chose : account grant affiche ce que l’octroi ouvre et où (opensObjects et faces en --json), rights explain --object <id> --account <email> liste les droits que l’octroi donne sur cet objet, par la traduction même du serveur, rights audit signale une ligne écrite en admin avec le niveau réellement appliqué (appliedLevel), et oximail doctor compte les octrois non expirés qui ouvrent des objets, par niveau (account_grants_open_objects).

Les équipes et leur adresse

Tout principal non individuel (group, resource, location) est provisionné avec un compte partagé d’adossement à la création, pour qu’une équipe possède des boîtes, un agenda et un drive sans qu’une personne en soit propriétaire. Ce qui exige un administrateur n’est pas le compte mais le nom : choisir une adresse prend un local-part dans l’espace de l’organisation et le réserve, pour que personne ne le prenne entre-temps, alors qu’une adresse générée ne prend rien que quiconque veuille, ce qui garde « partager ce dossier avec ces trois personnes » en libre-service. Une équipe naît muette : son adresse est réservée et la porte SMTP refuse le courrier qui lui est adressé (550) tant que sa surface mail n’est pas posée. Principal/enableMail {ids} (capability d’administration, 20 identifiants au plus par appel) la pose : les six boîtes système, l’identité d’envoi, une liste de tâches, le dossier racine et le script de tri. C’est un acte, pas une propriété : il n’existe aucun moyen de dé-provisionner une surface mail, et une bascule à sens unique ne doit pas se dessiner comme un interrupteur, donc la méthode suit l’idiome /set avec des issues par identifiant dans promoted / notPromoted plutôt qu’un booléen sur Principal/set. Quatre issues sont distinctes : promoted (avec l’identityId), alreadyLive (rejouer est sûr), repaired (une activation interrompue complétée, en nommant ce qui a été posé, laid), ou l’erreur mailSurfaceDamaged, un composant à moitié posé et rien de touché, qui demande un humain plutôt qu’une supposition. Les activations d’un même compte sont prises une par une, si bien que deux administrateurs qui cliquent ensemble ne lisent pas l’état intermédiaire de l’autre comme une panne. L’activation inscrit ce qu’elle crée sur le compte de l’équipe et le signale, si bien qu’un client connecté voit la boîte arriver ; elle ne porte pas d’ifInState (le moteur écrit dans le compte d’adossement, pas sur la ligne du principal) et se trace comme son jumeau CLI. L’annuaire dit si une adresse reçoit : Principal.receivesMail, servi quand il est demandé et jamais omis (un client doit distinguer « ne reçoit pas » d’un serveur plus ancien qui ignore la propriété), nomme exactement cette question. Il ne promet pas que ce message sera accepté (une boîte pleine fait patienter, elle ne refuse pas), et il ne dit pas si l’équipe peut répondre, qui est la question de l’exploitant et que doctor sert. Les listes de diffusion comptent comme joignables. L’écrire est refusé nommément, avec la méthode qui rend une équipe joignable.

Sémantique d’annuaire à connaître

  • Les types de principals sont honnêtes. Le principal d’un compte d’équipe partagé est type: "other" (non connectable), jamais individual : les sélecteurs de personnes peuvent se fier au type.
  • Les bénéficiaires doivent être adossés à un humain, ou être un groupe. Un droit accordé à un principal adossé à un compte non connectable (le compte partagé d’une équipe, une ressource, un lieu) est rejeté à l’écriture sur tous les domaines ; un groupe est la seule exception, puisqu’un droit keyé sur un groupe résout vers ses membres au contrôle d’accès, et l’appartenance est réservée à l’administrateur pour exactement cette raison.
  • Détruire un principal purge ses droits. Un destroy de Principal/set retire les clés du bénéficiaire de chaque carte shareWith (pas d’entrées d’ACL pendantes) et avertit ou s’interrompt s’il laisserait un drive partagé sans gardien éligible.
  • L’effacement laisse des pierres tombales. Le principal d’une personne effacée est nettoyé de ses données personnelles mais conservé comme ancre de graphe, et un principal en pierre tombale est refusé comme nouveau bénéficiaire (conformité).
  • Un compte gelé garde les mandats de l’organisation, pas les siens. Un droit porte qui l’a écrit : celui que le propriétaire a écrit est un mandat que le compte a donné, suspendu tant que le compte est gelé (role: disabled) et rétabli avec lui ; celui que l’opérateur a écrit au nom du propriétaire (account offboard --grant-mailbox-to, account grant, ou les clés de départ de Principal/setAdmin, voir l’API d’administration) est la décision de l’organisation et reste. Un droit sans auteur se lit comme celui du propriétaire, le sens fermé sur échec. Le droit d’un départ porte une expiration obligatoire, à un an au plus ; account grant --until est facultatif, et refusé vers un compte de service déclaré (service: true dans l’objet de capacité de compte, v2:account, des capabilities du principal), un connecteur qui ne part jamais (multi-tenancy).
  • Les entités se déclarent, elles ne se déduisent pas. La profileCard d’une personne peut nommer son employeur dans organizations par référence à un groupe (l’identifiant du groupe sous oximail.ch:principalId) ; le nom se lit sur le groupe à chaque fois, si bien que le renommer corrige toutes les cartes. Seul un groupe qu’un administrateur de locataire a marqué isEntity: true peut être cité : Principal/set prend la marque à la création et à la mise à jour, sur un groupe seulement (invalidProperties sinon, forbidden pour un non-administrateur), et une carte qui cite un groupe non marqué est refusée en nommant le groupe. Principal/get sert toujours isEntity, et sur un groupe cardReferenceCount, le nombre de cartes qui le citent, pour qu’un client le sache avant un renommage ou une destruction ; retirer la marque d’une entité que des cartes citent encore, ou la détruire, est forbidden avec le nom et le nombre. Principal/query {filter: {isEntity: true}} alimente le sélecteur d’employeur (false : tout sauf les entités). Le côté console est sur la page multi-tenancy.
  • Détruire un groupe libère son compte partagé, son adresse réservée et son siège, comme détruire une ressource ou un lieu le faisait déjà.
  • Un compte de service agissant sous shared:<propriétaire> ne peut modifier qu’une liste blanche du principal, workingHours, photoBlobId et une clé d’un objet de capability, et seulement quand son droit nomme la collection qui ouvre ce canal, pas n’importe quelle délégation en écriture ; le nom, l’adresse et le fuseau restent fermés. Une photo posée par quelqu’un d’autre que sa personne (ce canal, ou un administrateur) doit être un blob de portée organisation que l’auteur a déposé (/jmap/upload-tenant/{tenantId}) ou peut lire, sinon invalidProperties avec un seul texte pour tous les cas (dire « ce blob est dans le compte de la personne » serait un oracle) ; chaque pose ou retrait de ce genre est une ligne d’audit au nom du compte authentifié.
  • Principal/getAvailability (le libre/occupé) est documenté sur la page agenda.
ShareNotification/query renvoie un ordre total (du plus récent au plus ancien). Les règles de rigueur sont identiques à la surface courrier.

D’une organisation à l’autre : la fédération

Tout ce qui précède partage des données à l’intérieur d’une organisation. Partager avec une organisation différente est une surface distincte, urn:oximail:params:jmap:federation, et ce n’est volontairement pas le même mécanisme : un grant fédéré n’est ni une entrée shareWith, ni une ligne d’accès au compte. Ce qu’il porte, c’est la disponibilité (le libre/occupé) : le grant élargit qui peut demander, jamais ce qui est visible. Depuis octobre 2026, la lecture inter-nœuds est servie sur un serveur dont l’exploitant a ouvert la porte de fédération : un serveur appairé relaie Principal/getAvailability pour 50 adresses au plus sur 62 jours au plus, ne reçoit que du temps occupé fusionné, et chaque personne dont une réponse porte le temps occupé reçoit une ligne FederationDisclosure, écrite avant que la réponse parte. Dans l’autre sens, Event/proposeTimes interroge le serveur appairé qui sert le domaine d’un participant (agenda). Porte fermée, un grant n’autorise rien et aucune divulgation n’est enregistrée. L’appairage, l’enregistrement DNS et le côté exploitant sont décrits sur la page de la fédération. L’objet de capacité annonce le vocabulaire au lieu de laisser les clients le découvrir par des refus :

Le bénéficiaire est une organisation, pas une personne

supportedGranteeKinds vaut ["realm"], et ce n’est pas une question de calendrier. L’identité qu’une requête fédérée prouve aujourd’hui est le nœud émetteur, pas lequel de ses principals a agi. Un grant nommant une seule personne à l’autre bout ne serait donc pas réellement plus étroit qu’un grant nommant son organisation : il en aurait seulement l’air dans une interface. La liste existe pour que, le jour où une couche d’identité par principal arrivera, "principal" soit un ajout que le client peut détecter, et non un changement de comportement à deviner. granteeValue doit être un realm avec lequel vous avez réellement été appairé. L’appairage lui-même est une cérémonie d’opérateur bilatérale et hors bande : ce n’est pas une méthode JMAP, et c’est précisément pourquoi FederationPeer existe. Sans cette liste, un client n’avait que deux options, mauvaises toutes les deux : un champ libre où presque toute saisie est rejetée, ou un sélecteur que rien ne remplit. FederationPeer est borné à l’organisation et lisible par tout compte authentifié de celle-ci, et pas seulement par les administrateurs : un appairage est une relation de l’organisation, sans vue par sujet. L’objet porte realm (la valeur à envoyer en granteeValue), jurisdiction (une forme fermée, ISO 3166-1 alpha-2 en majuscules, CH, DE, FR : le champ était du texte libre, si bien que « Suisse », « Switzerland », « CH » et « ch » étaient quatre juridictions pour toute requête, figées dans chaque grant ; c’est la forme qui est vérifiée, jamais une liste de pays vivants qui se périmerait), state et pairedAt. Il n’expose volontairement ni la clé épinglée, ni l’ancre d’appairage, ni l’époque de clé, ni la cause de suspension : ce sont des écritures d’opérateur, et une clé publique sur chaque écran de consentement serait de la surface de sécurité achetée sans aucune fonctionnalité en échange.
FederationPeer n’a pas de méthode changes, et son state n’est pas un compteur de journal de changements : c’est une empreinte de la liste servie. L’appairage et la suspension se font dans des cérémonies d’opérateur, hors de toute requête JMAP, donc rien n’écrit de ligne de journal pour elles ; un état de journal serait ici une chaîne qui ne bouge jamais alors que les données dessous changent.

Le consentement ne se délègue pas

  • Seul le sujet peut créer ou modifier un grant, en agissant sur son propre compte. Un administrateur peut révoquer, car la révocation retire un accès et un opérateur qui doit couper une fuite n’a pas à attendre le sujet, mais jamais accorder à la place de quelqu’un. Un grant qu’un administrateur peut créer pour vous n’est pas un consentement. Le contrôle est plus strict qu’un test de rôle : une requête qui porte un accès partagé, c’est quelqu’un qui agit pour un autre, quel que soit son niveau.
  • Chaque écriture est une seule transaction : la ligne de grant, la ShareNotification qui prévient le sujet et la ligne d’audit qui nomme l’auteur de l’écriture valident ensemble. L’audit enregistre qui a réellement écrit, pas le compte que la requête visait : la révocation d’un administrateur n’est donc jamais attribuée au sujet.
  • Un granteeKind non pris en charge est refusé bruyamment. "principal" s’analyse comme une requête bien formée et aurait l’air accepté ; la ligne créée serait un grant de realm portant le nom d’une personne.
L’essentiel d’un grant est décidé par le serveur et en lecture seule sur le fil : seuls granteeKind, granteeValue et expiresAt sont réglables. rights en particulier ne l’est pas : un client capable d’élargir son propre enregistrement de consentement au-delà de ce que le résolveur honore serait une permission déguisée en réglage. expiresAt: null signifie que le sujet a explicitement choisi « sans expiration », et jurisdiction est gelée à la création : le consentement a été donné à une contrepartie dans un lieu donné, et un champ modifiable laisserait le lieu changer sans nouveau consentement.

Pourquoi un grant peut cesser d’agir, et comment le sujet l’apprend

FederationGrant.state répond directement à « pourquoi cette organisation ne me voit-elle plus ? » : active, unpaired, expired, dormancy_exhausted, dormant, lapsed ou reanchored. Le stockage le dérive du prédicat même qu’applique la lecture d’autorisation : il ne peut donc pas s’écarter de la décision. FederationPeer.state (active, dormant, lapsed) emploie les mêmes mots pour les mêmes défaillances, et seul active accepte un grant, là encore dérivé du prédicat sur lequel l’écriture refuse : un realm que le sélecteur propose est un realm que l’écriture accepte. Une échéance s’annonce avant de tomber. Le consentement expire par défaut, ce qui est voulu, mais une expiration dont personne n’a été prévenu est une panne que le sujet ne sait pas expliquer. Un passage horaire dépose une ShareNotification pour le sujet quatorze jours avant l’expiresAt d’un grant, sur la surface où le grant a été annoncé (objectType: "FederationGrant", objectSubtype: "expiring", les droits inchangés puisque rien n’a encore changé), une seule fois par grant : la marque s’écrit dans la même transaction que la notification. Un renouvellement qui repousse l’échéance garde la marque, si bien qu’un grant renouvelé puis proche de sa nouvelle échéance n’est pas prévenu une seconde fois aujourd’hui. Le support lit la jauge oximail_federation_grants_expiring{tenant}, les grants sur le point de tomber, prévenus ou non. Deux conséquences valent la peine d’être prises en compte dès la conception :
  • La révocation est unilatérale, immédiate, et ne vaut que vers l’avant. Un pair ne lit ni n’écrit jamais vos grants ; il apprend une révocation en se voyant refusé. C’est ce qui garde la révocation hors du terrain de la cohérence distribuée, et c’est aussi pourquoi revocationIsForwardOnly est annoncé comme donnée : révoquer retire le chemin de lecture pour la suite et ne défait rien de ce qui a déjà été divulgué.
  • Le journal des divulgations est l’autre moitié de cette phrase. FederationDisclosure est ce qui rend « révoquer ne défait rien de déjà divulgué » vérifiable au lieu de simplement rassurant : il survit au grant, et il n’existe aucun FederationDisclosure/set, pas même limité au destroy. Enregistrer une divulgation fait avancer l’état de la collection du sujet dans la même transaction, si bien qu’un client qui surveille son propre journal le voit bouger. C’est la rétention qui borne le journal ; le sujet n’en efface rien. Il enregistre les divulgations, jamais les tentatives : un pair qui sonde cinq cents adresses ne crée aucune ligne au sujet des quatre cent quatre-vingt-dix-neuf personnes qui n’ont jamais consenti. Chaque ligne nomme le realm du pair, la couche d’identité vérifiée, la juridiction sous laquelle le grant se tenait à cet instant, et la date.
Les filtres et les tris sur les deux méthodes query sont refusés (unsupportedFilter / unsupportedSort) plutôt qu’ignorés en silence. Un client qui filtrerait les pairs sur state: "active" et recevrait discrètement aussi les dormants proposerait à son utilisateur un bénéficiaire que l’écriture rejette ensuite.