Skip to main content
À côté des capacités JMAP standard, OxiMail sert une famille de capacités d’extension v2 sous urn:oximail:params:jmap:v2:*. Cette page décrit la surface telle que construite : ce que le serveur négocie et sert réellement en v0.30.0. Les extensions sont en cours de rédaction comme Internet-Drafts destinés à l’IETF ; voir drafts IETF pour le programme.

Comment v2 se négocie

v2 n’est pas un commutateur de version : c’est un opt-in par capacité, exactement comme n’importe quelle capacité JMAP :
  • Un client demande une capacité v2 dans using ; les méthodes qu’elle déverrouille deviennent appelables dans cette requête. Rien ne change au comportement v1 pour les clients qui ne demandent jamais.
  • Les noms de méthodes sont des alias à double capacité quand une capacité v1 et une v2 couvrent le même handler : le registre fait correspondre les deux écritures à une seule implémentation, il y a donc exactement un comportement par méthode, quelle que soit la capacité qui l’a déverrouillée.
  • L’ensemble d’exclusion mutuelle est vide en v0.30.0 : associer une capacité v1 à son homologue v2 dans la même requête est permis (voir JMAP Core pour le mécanisme).
  • Comme partout ailleurs, l’objet de session fait foi : l’entrée accountCapabilities de chaque compte pour un URN v2 porte les limites et options annoncées de cette capacité. Détectez les fonctionnalités depuis la session, jamais depuis la version du serveur.

Les objets de capacité v2

Pour chaque capacité, le contrat de fil qui fait foi est ce que la session annonce, plus les comportements des méthodes. Les règles de rigueur (page courrier) s’appliquent aux méthodes v2 à l’identique : arguments et propriétés inconnus sont des erreurs, jamais ignorés en silence.

Ce que les objets de capacité annoncent

Les objets de capacité de la session se décrivent eux-mêmes : les clients construisent leur interface à partir d’eux, au lieu de coder en dur une connaissance du serveur. Les entrées porteuses telles que servies en v0.30.0 :
  • v2:rules porte tout le vocabulaire de l’éditeur de règles : supportedFields (18 champs de condition) et supportedActions (15 types d’action), un client construit son éditeur depuis ces listes, plus les bornes appliquées (maxRulesPerAccount: 1000, maxActionsPerRule: 20, maxConditionDepth: 20, maxRuleNameLength: 256, perRuleExecutionTimeoutMs, maxApplyEmailsPerCall, le [rules] max_apply_emails_per_call de l’exploitant, maxNotificationsPerHour: 30), et la forwardPolicy de l’exploitant (any, internal, verified, none), pour qu’un client sache, avant de demander un code de confirmation, si le serveur en enverra un.
  • v2:admin n’apparaît dans accountCapabilities que pour les comptes au rôle administrateur : son absence est ce qui dit au client de ne pas afficher de surface d’administration. Elle annonce supportedPrincipalRoles, les faits de rétention (softDeleteRetentionDays: 30, auditLogRetentionDays: 2557, soit sept ans) et mfaRequiredForSensitiveOperations: true : les changements de rôle et les mutations de tenant par un compte qui a enrôlé une passkey exigent une session authentifiée par passkey.
  • urn:ietf:params:jmap:auditlog (réservée aux administrateurs comme v2:admin) annonce le journal d’audit inviolable : {hashAlgorithm: "sha-256", verificationSupported: true, retentionDays: 2557}. AuditEvent/verify recalcule la chaîne de hachage et signale la première entrée altérée ou manquante.
  • v2:mail annonce les bornes d’envoi appliquées : maxSizeEmail (50 Mio, aussi pré-annoncée par l’extension SMTP SIZE), maxScheduleDelay (un an sur le sendAt d’Email/send) et emailQuerySortOptions, le vocabulaire de tri exact qu’accepte Email/query. L’objet Email v2 porte aussi expiresAt (mise à la corbeille automatique à l’instant choisi), et Email/send accepte requestMdn (accusés de lecture RFC 8098) et tlsRequired (REQUIRETLS RFC 8689, y compris pour les envois programmés). smimeSigner, le signataire d’une signature S/MIME vérifiée (autorité émettrice, validité, adresses, profil de validation, révocation contrôlée ou non), n’est servi qu’avec v2:mail dans using, à côté des propriétés RFC 9219 de smimeverify (les messages signés).
  • v2:streaming annonce replayWindowSeconds (l’horizon réel de rétention du journal de changements, 90 jours : un Push/catchup depuis n’importe quel état dans la fenêtre rejoue exactement ce qui a été manqué), maxQueryStreamChunk: 1000 et la cadence de vérification des abonnements push.
  • v2:push-patches annonce maxPatchSize (1 Mio), maxBatchEntries: 100, maxPatchesPerSecond: 10, supportedTransports: ["websocket", "eventsource"] (le transport EventSource est GET /jmap/push-patches), types, les noms d’émission qu’un abonnement peut suivre, et maxConcurrentStreams: 4. Un client s’abonne par connexion avec EnablePushPatches {types, properties} ; les entrées updated portent alors exactement les propriétés changées, et les cas dégradés retombent sur un simple StateChange (réconciliation par Push/catchup). Une propriété qui devient null est un changement : sous une projection explicite par type, une propriété projetée présente dans la référence et absente maintenant est envoyée à null, une fois ; sans projection, le serveur ne sait pas distinguer une propriété effacée d’une propriété que le client n’a jamais demandée, et cette collection retombe sur StateChange pour la fenêtre au lieu de deviner. Le patch ne disait rien, si bien qu’un client gardait l’ancienne valeur (une liste de salons montrait encore son dernier texte après un message d’appel). Sur un WebSocket où les patchs sont activés, Core/queryStream avec pushChunks: true devient un flux poussé : la réponse porte {accountId, sessionId} et le serveur enchaîne des trames StreamChunk jusqu’à la fin du parcours ou jusqu’à ce que le client l’annule avec ce sessionId ; au-delà de maxConcurrentStreams, l’appel retombe sur la réponse cadencée par le client avec une trame tooManyConcurrentStreams, et un échec en cours de flux envoie streamFailed, qui nomme l’erreur, pour que le client le relance en mode cadencé. Un flux ouvert sur un compte partagé reste sur ce compte (partage).
  • v2:resource-limits n’annonce plus que des limites qu’un mécanisme applique réellement, depuis les mêmes constantes que les gestionnaires : dont maxTotalRequestsPerMinute, injectée depuis la configuration effective de limitation de débit de l’opérateur (absente quand aucun limiteur ne tourne). maxConcurrentRequests vit sur la capacité core (RFC 8620) et s’applique par compte sur les deux transports JMAP.
Les fils et les conversations ont gagné des surfaces d’organisation de premier rang dans le même cycle : muted par fil (hérité par les messages nouvellement ingérés) et pinned, Thread/split, Thread/merge, et /queryChanges sur chaque type v2 interrogeable.

Le canon de projection

Chaque /get valide son argument properties contre un vocabulaire fermé, propre au type, et avant toute lecture du magasin. Un nom inconnu revient en invalidArguments qui nomme les fautifs ; id est toujours accepté (la RFC 8621 le rend implicite) et * sélectionne toutes les propriétés de premier niveau. C’est une erreur, et non un haussement d’épaules, pour une raison précise. Avec l’ancien motif « sérialiser puis filtrer », une propriété mal orthographiée produisait un succès avec la propriété absente : indiscernable d’une propriété que le serveur aurait choisi de ne pas servir. Un client ne pouvait pas distinguer sa faute de frappe d’une limite du serveur, et personne ne le pouvait davantage en lisant les journaux. Un vocabulaire fermé ne sert que si on peut le lire : le canon est donc publié en forme machine, à côté des types TypeScript générés. DERIVED_<Type> est la partie qu’un client ne peut pas reconstituer seul. Le mayEdit d’un annuaire de tenant est calculé par appelant depuis son rôle de tenant et une expansion de groupes côté serveur ; aucun champ de structure ne le porte, et le type TypeScript seul n’en dit donc rien. Le registre ne liste que de vraies injections hors structure : une propriété qui est un champ de structure réécrit par lecteur, comme myRights, est déjà typée et en est délibérément absente. Le canon publié est un contrat additif seulement, la même règle que la surface --json de la ligne de commande. La capacité v2:projection de la session est le marqueur de conformité qui atteste que le modèle vaut pour tous les types.

Le vocabulaire d’écriture

La même discipline vaut côté écriture, déclarée type par type. Un gestionnaire /set lit les clés qu’il connaît, une par une ; une clé qu’il ne connaît pas n’est comparée à rien, ne déclenche rien, et la réponse disait updated. C’est la première règle prise à l’envers : pas un échec silencieux mais un succès silencieux, qui est pire, parce qu’un échec finit par se voir et qu’un faux vert jamais (timezone contre timeZone en a été un cas). Un type peut désormais déclarer son canon d’écriture au répartiteur : une liste blanche des propriétés qu’il accepte en create comme en update, plus les propriétés qu’il connaît et refuse quand même, chacune avec son motif. Le répartiteur retire alors de create et update tout objet qui nomme une propriété hors du canon, et fusionne le refus dans notCreated / notUpdated : le gestionnaire ne voit jamais l’objet, le patch est donc tout ou rien pour lui, et les autres objets du même appel passent (RFC 8620 §5.3). Deux refus, parce que se tromper de nom et se tromper de chemin ne se corrigent pas de la même façon :
  • un objet de create porte des propriétés ordinaires ; un nom que le type ne définit pas est invalidProperties, qui le nomme ;
  • un objet d’update est un PatchObject dont les clés sont des pointeurs JSON, et un pointeur dont la racine n’est pas une propriété du type viole le « toutes les parties avant la dernière doivent déjà exister » de la RFC : invalidPatch.
Une propriété que le type calcule, ou dont l’écriture passe par un autre geste, dit lequel : Principal.receivesMail est refusé avec le nom de la méthode qui rend une équipe joignable, plutôt que comme « inconnue », ce qui serait exact et inutilisable. Un objet entier relu est un patch valide. La RFC 8620 §5.3 permet à un client de renvoyer un objet qu’il a lu, propriétés posées par le serveur comprises. Le canon aurait refusé toute mise à jour qui nomme une propriété posée par le serveur ; le répartiteur met maintenant ces noms de côté et, après chaque contrôle d’autorité, les relit par le Foo/get du type : quand chacun vaut sa valeur actuelle, le patch est admis sans eux, sinon le refus tient, nommément. Déclarer est délibéré et se fait type par type, jamais en bloc : transformer un argument ignoré en erreur casse un client déployé exactement comme un renommage, et aucun test serveur ne peut le voir, donc chaque type se déclare une fois le balayage de ses émetteurs fait. Ce balayage est désormais achevé : les types de la surface JMAP déclarent leur canon, Email et Thread compris. Email accepte à la création, en plus de ses propriétés nommées, les pseudo-propriétés header:<Nom>[:<forme>] de la RFC 8621 §4.1.3, reconnues par préfixe.
Stabilité du fil : au sein de la ligne v0.30, les formes v2 évoluent de façon additive : nouvelles propriétés optionnelles et nouvelles méthodes, pas de renommage ni de changement de sémantique des champs servis. Les durcissements qui transforment un argument jusque-là ignoré en erreur sont étagés (journalisation d’abord, rejet une fois les clients confirmés propres).