Passer au contenu principal
oximail-core est la crate socle sur laquelle reposent toutes les autres crates de domaine. Elle implémente JMAP Core (RFC 8620) : la ressource de session que le client utilise pour découvrir le serveur, le répartiteur qui route un lot de méthodes, le modèle de capacités qui les autorise, les types de push, les primitives de blob et d’identifiant, le vocabulaire d’erreurs typées, et l’EventBus qui permet aux crates de domaine de communiquer sans s’importer les unes les autres.

Overview

Toute requête JMAP authentifiée passe par cette crate. La couche HTTP, dans oximail-server, analyse la requête, construit un RequestContext, puis le transmet au MethodRegistry défini ici. Le registre résout les références arrière, vérifie les capacités et les droits d’accès, puis dispatche chaque appel vers le handler enregistré pour ce nom de méthode. Les crates de domaine (oximail-mail, oximail-calendar, oximail-contact, et les autres) implémentent le trait JmapMethod et enregistrent leurs handlers. oximail-core ne possède que la mécanique transverse et un petit ensemble de méthodes de niveau core. Elle se situe au bas du chemin de requête. Elle dépend de oximail-store pour les primitives d’identifiant, de blob et d’état, et ne dépend de rien au-dessus d’elle. La crate mail n’importe jamais la crate calendar. Les deux dépendent de oximail-core et communiquent par son EventBus.

Features

  • Ressource de session (RFC 8620 §2) : le document de découverte unique servi sur /.well-known/jmap, qui liste les capacités, les comptes, les comptes primaires, et les URL d’API, d’upload, de download et d’EventSource.
  • Dispatch des méthodes (RFC 8620 §3.5) : un registre de handlers JmapMethod indexés par nom de méthode, qui exécute dans l’ordre chaque appel d’une requête groupée.
  • Résolution des références arrière (RFC 8620 §3.7) : références de création (#clientId) et références de résultat ({ "resultOf", "name", "path" }, y compris les clés préfixées par # et le pointeur JSON avec le caractère générique *).
  • Modèle de capacités (RFC 8620 §2, ADR-028) : capacités au niveau du serveur, capacités dynamiques par compte, et les surfaces d’URN v1 et v2 annoncées côte à côte.
  • Vocabulaire d’erreurs typées (RFC 8620 §3.6) : des énumérations d’erreurs au niveau requête, méthode et set, qui se sérialisent vers le type JMAP exact.
  • Machinerie de filtre générique : un combinateur récursif AND / OR / NOT partagé par toutes les méthodes Foo/query.
  • Types de livraison push (RFC 8620 §7) : abonnements push, la charge utile de notification StateChange, et les primitives Web Push (RFC 8291) et VAPID derrière la livraison EventSource et WebSocket.
  • EventBus (ADR-011) : un canal typé de publication et d’abonnement pour les événements inter-domaines.
  • Méthodes de niveau core : Core/echo, PushSubscription/*, ainsi que les types de messages sauvegardés (signets), de liens message-tâche, et les surfaces de streaming v2.

How it works

La ressource de session

Le client commence par récupérer la ressource de session. Session::build assemble la map capabilities du serveur, la map accounts (le compte personnel de l’utilisateur plus les comptes partagés), la map primaryAccounts, les gabarits d’URL d’API, d’upload, de download et d’EventSource, et une chaîne state opaque. Les limites annoncées dans la capacité Core (maxSizeUpload, maxCallsInRequest, maxObjectsInGet, et les autres) sont définies comme des constantes dans cette crate. Les valeurs annoncées dans la session et les valeurs appliquées au dispatch proviennent donc d’une source unique. Session::build_for_account ajoute par-dessus le pipeline ADR-028. Il calcule les capacités par compte à partir du plan de l’organisation et du rôle du compte, puis réduit primaryAccounts aux seules capacités que le compte possède réellement. Les comptes partagés sont ajoutés à part par add_shared_accounts, chacun indexé shared:{ownerId} et ne portant que les capacités des types d’objets qui ont été partagés.

Dispatch et références arrière

MethodRegistry::process parcourt dans l’ordre les appels de méthode d’une requête. Pour chaque appel, il effectue les étapes suivantes :
  1. Résolution des références arrière dans les arguments. Une chaîne "#name" est remplacée par l’identifiant serveur créé plus tôt dans le lot. Un objet avec resultOf / name / path (ou une clé préfixée par #) est remplacé par la valeur située à ce pointeur JSON dans le résultat d’un appel précédent. Une référence vers un identifiant d’appel inconnu, ou un chemin qui ne résout pas, renvoie invalidResultReference au lieu d’un null silencieux.
  2. Validation de accountId : le compte demandé doit correspondre au compte authentifié, à une forme préfixée par shared: de ce compte, ou au compte d’authentification. Une non-correspondance renvoie accountNotFound.
  3. Vérification de la capacité : la capacité requise par le handler (ou l’une de ses capacités additionnelles) doit être présente dans le tableau using de la requête, sinon unknownCapability.
  4. Application des contrôles d’accès ADR-028 : la capacité doit faire partie des capacités du compte (accountNotSupportedByMethod sinon), un compte en lecture seule ne peut pas appeler une méthode /set (forbidden), et la portée d’un mot de passe d’application peut restreindre davantage l’accès (forbidden).
  5. Exécution du handler, dont le résultat est stocké pour que les appels suivants du même lot puissent y faire référence.
Les identifiants créés sont accumulés sur tout le lot et renvoyés dans le createdIds de la réponse. Un échec sur un appel produit une réponse d’erreur pour cet appel et le traitement continue avec le suivant. Le lot n’est pas interrompu.

Capacités : v1 et v2 coexistent

OxiMail annonce deux générations d’URN de capacité dans la même session : l’ensemble standard urn:ietf:params:jmap:* (v1, RFC 8620 / RFC 8621 et apparentées) et l’ensemble OxiMail urn:oximail:params:jmap:v2:*. Un handler v2 ne remplace pas son équivalent v1. Il enregistre son URN v2 comme capacité additionnelle sur le même handler.
Les capacités v1 et v2 ne sont pas mutuellement exclusives. Le répartiteur tient une liste de paires v1 / v2 mutuellement exclusives (V1_V2_MUTUAL_EXCLUSION_PAIRS), mais sur la ligne actuelle cette liste est vide : chaque domaine qui avait une paire (mail, contacts, calendriers) l’a retirée. Une requête qui liste à la fois l’URN v1 et l’URN v2 d’un domaine dans using est acceptée, et la forme du message sur le réseau est l’union des propriétés v1 et v2. Le contrôle d’exclusion s’exécute quand même. Une future surface v3 pourrait donc réintroduire une paire sans réimplémenter le chemin de rejet, mais aujourd’hui il parcourt une liste vide.

EventBus : du cross-domaine sans couplage

L’EventBus est un canal de diffusion typé. Une crate émet un DomainEvent (par exemple EmailIngested, CalendarEventCreated, StateChanged) et un nombre quelconque d’abonnés dans d’autres crates y réagissent. Chaque variante d’événement transporte toutes les données dont un abonné a besoin. La crate abonnée n’a donc jamais à importer la crate émettrice. C’est ce qui empêche la crate mail et la crate calendar de dépendre l’une de l’autre : un courriel iTIP entrant devient un événement ITipReceived que l’abonné calendrier transforme en événement de calendrier. StateChanged est le pont vers le push. Après qu’un handler /set a écrit ses changements, il émet StateChanged avec le nouvel état, et le worker push le transforme en notification pour les clients abonnés.

Livraison push

Les abonnements push (RFC 8620 §7.2) permettent à un client d’enregistrer une URL appelée en POST lorsqu’un état change. La charge utile StateChange (RFC 8620 §7.1) transporte une map de l’identifiant de compte vers le type modifié et le nouvel état. Elle peut être marquée d’un vecteur de causalité distribuée, pour que les clients multi-appareils appliquent les patchs dans un ordre déterministe. Le chiffrement Web Push (RFC 8291) et les primitives de signature VAPID se trouvent dans cette crate. La boucle de livraison elle-même s’exécute dans le worker push.

Invariants

  • Ne jamais échouer en silence. Les identifiants invalides, les capacités inconnues, les non-correspondances de compte et les mauvaises références de résultat renvoient tous une erreur typée. Rien n’est ignoré.
  • Une source unique pour les limites. Les limites annoncées dans la session et les limites appliquées au dispatch sont les mêmes constantes.
  • Aucune dépendance vers le haut. oximail-core ne dépend que de oximail-store. La communication inter-domaines passe par l’EventBus, jamais par un import direct.

Configuration

oximail-core ne lit directement aucun fichier de configuration. Elle consomme des valeurs résolues par le binaire (URL de base, ensemble de capacités, plan de l’organisation, rôle du compte). Les limites de niveau serveur qu’elle annonce sont des constantes de compilation. Les réglages d’administration qui influencent la surface construite par cette crate (l’URL de base publique, le TLS, les plans et les rôles d’organisation) sont documentés dans la référence de configuration.

Protocol / JMAP surface

L’essentiel de la surface JMAP est implémenté dans les crates de domaine. oximail-core possède la mécanique de dispatch et ces méthodes de niveau core :
MéthodeRôle
Core/echoRFC 8620 §4.1, renvoie ses arguments inchangés. Le test de connectivité canonique.
PushSubscription/get, PushSubscription/setRFC 8620 §7.2, gestion des abonnements push d’un client.
SavedMessage/get, /set, /query, /queryChanges, /changesMessages mis en signet (capacité savedmessages).
MessageTaskLink/get, /set, /query, /queryChanges, /changesAssociations entre messages et tâches (capacité tasklinks).
Core/queryStreamStreaming JMAP v2, encapsule un Foo/query pour diffuser les résultats par blocs.
Push/catchupStreaming JMAP v2, rejoue les changements d’état qu’un client en reconnexion a manqués.
Elle fournit aussi QueryChangesStub, un handler générique qui renvoie cannotCalculateChanges (RFC 8620 §5.6) pour les surfaces Foo/queryChanges qui retombent sur une re-requête complète. Les contrats réutilisables de filtre et de dispatch qu’elle exporte sont le trait JmapMethod, le MethodRegistry, et les types génériques Filter / FilterOperator. Voir JMAP Core pour la présentation du protocole côté client.

Design decisions

  • ADR-011 (EventBus inter-domaines) : la crate mail n’importe jamais la crate calendar. Elles communiquent par le canal de diffusion DomainEvent défini ici.
  • ADR-028 (Capacités de compte dynamiques) : les capacités par compte sont l’intersection des capacités du serveur, du plan de l’organisation et du rôle du compte, avec des dérogations explicites. Les contrôles d’accès au moment du dispatch les appliquent.
  • ADR-008 (Identifiants UUIDv7) : la primitive Id (réexportée depuis oximail-store). Un identifiant mal formé n’est jamais ignoré en silence.
  • ADR-014 (Déduplication des blobs par hash de contenu) et ADR-009 (stockage brut plus structuré) : la primitive BlobId réexportée par cette crate est le SHA-256 du contenu.
  • ADR-054 / ADR-056 / ADR-066 (architecture JMAP v2, négociation de capacités, évolution de schéma) : le modèle de double capacité v1 / v2 et la liste d’exclusion mutuelle (aujourd’hui vide).
  • ADR-072 (Plafonds de volume par collection) : les variantes d’erreur tooManyEntries et les limites de ressources annoncées.
Les ADR vivent dans le dépôt du serveur OxiMail. Voir l’index des ADR.

Standards

  • RFC 8620, JMAP Core (session, dispatch, références arrière, capacités, push, erreurs). La référence principale pour cette crate.
  • RFC 6750, authentification par jeton Bearer (le jeton EventSource en paramètre d’URL, §2.3).
  • RFC 6901, pointeur JSON, utilisé pour résoudre les chemins des références de résultat.
  • RFC 8291, chiffrement des messages pour Web Push.
  • RFC 8292, VAPID, pour l’authentification du push.
  • RFC 8887, JMAP sur WebSocket (capacité websocket).

Source map & tests

Fichier (dans la crate)Contenu
src/session.rsLa ressource Session, les limites du serveur en constantes, l’assemblage des comptes partagés. Protégé par test_session_full_checklist.
src/dispatch.rsMethodRegistry, le trait JmapMethod, la résolution des références arrière, la liste d’exclusion v1 / v2, Core/echo, QueryChangesStub. Protégé par test_dispatch_full_checklist.
src/capabilities.rsTenantPlan, AccountRole et build_account_capabilities (l’intersection ADR-028).
src/event_bus.rsDomainEvent, EventBus, EventEmitter.
src/jmap_filter.rsLe combinateur générique Filter / FilterOperator et la validation structurelle.
src/push.rs, src/push_store.rs, src/push_worker.rs, src/web_push.rs, src/vapid.rsAbonnements push, charge utile StateChange, Web Push et VAPID.
src/error.rsRequestError, MethodError, SetError, le vocabulaire d’erreurs JMAP typées.
src/types.rsAccount, plus les réexports Id, BlobId, State depuis oximail-store.
src/methods/Les handlers de niveau core (SavedMessage/*, MessageTaskLink/*, Core/queryStream, Push/catchup, PushSubscription/*).
tests/concurrency.rsCouverture d’intégration du dispatch concurrent.
Les modules de dispatch et de session portent chacun un test unique de type liste de contrôle, qui exerce toutes les responsabilités du module en un seul flux. Modifier le comportement du dispatch ou de la session implique de mettre à jour ce test.

Status

Implémenté et en production en v0.30.0 : la ressource de session, toute la mécanique de dispatch et de références arrière, le modèle de capacités ADR-028, le vocabulaire d’erreurs typées, le combinateur de filtre générique, l’EventBus, les abonnements push et la charge utile StateChange, et toutes les méthodes de niveau core listées ci-dessus. Les méthodes de streaming v2 (Core/queryStream, Push/catchup) et les URN de capacité v2 sont annoncées et câblées sur la ligne actuelle. L’infrastructure d’exclusion mutuelle v1 / v2 existe mais reste inerte : la liste de paires est vide, donc aucune requête n’est rejetée pour avoir associé des capacités v1 et v2.