> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oximail.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# JMAP Core

> La couche Core de la RFC 8620 telle que OxiMail l'implémente : la ressource session, l'authentification, les requêtes groupées, les références arrière, les capacités et le push.

JMAP (JSON Meta Application Protocol) est l'API que parle OxiMail pour le courrier, les agendas, les contacts, les tâches, les fichiers et le chat. Cette page couvre la couche **Core** de la [RFC 8620](https://www.rfc-editor.org/rfc/rfc8620) : comment un client découvre le serveur, s'authentifie, groupe plusieurs appels de méthode dans une seule requête HTTP, enchaîne ces appels avec des références arrière, déclare les capacités qu'il utilise, et reçoit les notifications push.

Tout ce qui suit décrit ce que OxiMail expose réellement en v0.30.0. Les jeux de méthodes par domaine s'appuient sur cette couche : voir [JMAP Mail](./jmap-mail), [Agendas](./jmap-calendar), [Contacts](./jmap-contacts), [Tâches](./jmap-tasks), [Fichiers](./jmap-files), [Partage](./jmap-sharing) et [Chat](./jmap-chat).

## La ressource session

Un client JMAP commence par récupérer la **ressource session**. C'est le document de découverte unique qui indique au client tout ce dont il a besoin : les capacités prises en charge par le serveur, les comptes accessibles à l'utilisateur, et les URL de toutes les autres opérations.

```http theme={null}
GET /.well-known/jmap
Authorization: Bearer <token>
```

Le point d'accès de la session est **authentifié** : il faut présenter un token valide pour le récupérer. La réponse est un objet JSON (RFC 8620 §2) qui contient les champs suivants.

| Champ             | Contenu                                                                                                                                                                       |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `capabilities`    | La table des capacités au niveau serveur : chaque URN de capacité associée à son objet de configuration (limites, options).                                                   |
| `accounts`        | Chaque compte accessible à cet utilisateur, indexé par identifiant de compte. Chaque entrée contient `name`, `isPersonal`, `isReadOnly` et ses propres `accountCapabilities`. |
| `primaryAccounts` | Pour chaque URN de capacité, l'identifiant du compte considéré comme principal.                                                                                               |
| `username`        | Le nom de l'utilisateur authentifié (son adresse e-mail).                                                                                                                     |
| `apiUrl`          | Le point d'accès où envoyer les appels de méthode : `/jmap`.                                                                                                                  |
| `uploadUrl`       | Le modèle d'URL pour l'envoi de blobs : `/jmap/upload/{accountId}`.                                                                                                           |
| `downloadUrl`     | Le modèle d'URL pour le téléchargement de blobs : `/jmap/download/{accountId}/{blobId}/{name}?type={type}`.                                                                   |
| `eventSourceUrl`  | Le modèle d'URL pour le push (EventSource) : `/jmap/eventsource/?types={types}&closeafter={closeafter}&ping={ping}`.                                                          |
| `state`           | Une chaîne opaque qui change dès que l'objet session lui-même change (par exemple lorsqu'un compte est ajouté).                                                               |

### Ne jamais coder les chemins en dur

Les trois champs `*Url` sont des **modèles d'URL** ([RFC 6570](https://www.rfc-editor.org/rfc/rfc6570)) : un client, en particulier une intégration sans état comme un worker serverless ou un script cron, doit les lire depuis la session et substituer les variables (`{accountId}`, `{blobId}`, ...), jamais reconstruire les chemins de mémoire d'après la disposition d'un autre serveur.

Ce n'est pas théorique. Le point d'envoi d'OxiMail est `/jmap/upload/{accountId}` **sans barre oblique finale** (routage strict) ; d'autres serveurs JMAP utilisent `/jmap/upload/{accountId}/`. Un client qui code la forme avec barre oblique reçoit un `404` à chaque envoi, et le bug ressemble à une panne serveur. Lire `uploadUrl` depuis la session rend le même code client compatible avec n'importe quel serveur RFC 8620.

Un client sans état n'a pas besoin de récupérer la session à chaque invocation :

1. `GET /.well-known/jmap` une fois, mettre en cache l'objet de session avec sa chaîne `state`.
2. À chaque exécution, utiliser directement les modèles en cache.
3. Chaque réponse de méthode JMAP porte un `sessionState` : quand il ne correspond plus au `state` en cache (ou qu'une URL substituée se met à répondre `404`), récupérer la session à nouveau et remplacer le cache.

L'objet de la capacité `Core` annonce aussi les limites strictes du serveur, qu'un client doit respecter avant d'envoyer une requête.

| Limite                  | Valeur en v0.30.0 |
| ----------------------- | ----------------- |
| `maxSizeUpload`         | 50 Mo             |
| `maxConcurrentUpload`   | 4                 |
| `maxSizeRequest`        | 10 Mo             |
| `maxConcurrentRequests` | 8                 |
| `maxCallsInRequest`     | 64                |
| `maxObjectsInGet`       | 500               |
| `maxObjectsInSet`       | 500               |

<Note>
  Un utilisateur peut voir plus d'un compte dans la session. Outre son compte personnel, tout dossier, agenda ou carnet d'adresses partagé avec lui via le [partage JMAP](./jmap-sharing) apparaît comme une entrée de compte supplémentaire (indexée `shared:{ownerId}`), qui ne porte que les capacités partagées et parfois `isReadOnly: true`.
</Note>

## Authentification

OxiMail utilise des tokens **Bearer** ([RFC 6750](https://www.rfc-editor.org/rfc/rfc6750)). On obtient un token en envoyant les identifiants au point d'accès de connexion.

```http theme={null}
POST /auth/login
Content-Type: application/json

{ "email": "alice@example.com", "password": "..." }
```

La réponse renvoie le token et l'identifiant du compte.

```json theme={null}
{ "accessToken": "...", "accountId": "..." }
```

Envoyez ce token sur chaque requête suivante.

```http theme={null}
Authorization: Bearer <token>
```

### Durée de vie et renouvellement du token

Les tokens expirent **24 heures** après leur émission. Pour rester connecté sans ressaisir de mot de passe, renouvelez le token.

```http theme={null}
POST /auth/refresh
Authorization: Bearer <token-actuel>
```

Si le token est encore valide, ou expiré depuis **moins de 7 jours**, le serveur supprime l'ancien token et en renvoie un nouveau avec une nouvelle expiration à 24 heures. Le format de la réponse est le même que pour la connexion : `{ "accessToken": "...", "accountId": "..." }`. Passé le délai de grâce de 7 jours, le renouvellement échoue avec un `401` et l'utilisateur doit se reconnecter.

### Tokens liés à l'appareil DPoP (RFC 9449)

Un client peut activer la **preuve de possession** à la connexion en envoyant une preuve DPoP (un JWT signé par une clé ES256 éphémère) avec ses identifiants. La session est alors liée à cette clé : `POST /auth/refresh` exige une preuve fraîche signée par la même clé. Un bearer token volé seul ne peut donc plus être renouvelé.

Deux couches de durcissement côté serveur prolongent ce mécanisme. Les deux sont configurables et désactivées par défaut :

* **Nonce serveur (§8).** Chaque réponse d'authentification porte un en-tête `DPoP-Nonce`. Une preuve qui contient une revendication `nonce` est toujours vérifiée contre lui. Un nonce périmé répond `401` avec l'erreur `use_dpop_nonce` et un nonce frais, et le client réessaie (la séquence standard de la RFC 9449). Avec `[auth] dpop_nonce_required = true`, le serveur rejette en plus les preuves sans nonce : la fraîcheur contrôlée par le serveur remplace la fenêtre d'horloge.
* **Preuve de possession par requête (§7).** Avec `[auth] dpop_resource = "require"`, une session liée par DPoP doit envoyer une preuve sur **chaque** requête qui présente son token dans l'en-tête `Authorization`, liée à ce token par la revendication `ath`. Un bearer volé ne peut alors plus appeler JMAP du tout, et pas seulement plus se renouveler. `"monitor"` journalise ce que `require` rejetterait, pour un déploiement sans risque. Les sessions qui n'ont jamais lié de clé et les mots de passe d'application ne sont pas concernés. La comparaison `htu` normalise les ports par défaut selon la RFC 3986 §6.2 (un `:443` explicite d'un côté ou de l'autre correspond quand même).

Comme les éléments médias (`<video>`, `<audio>`) ne peuvent pas porter d'en-tête `DPoP`, une session liée sous `require` frappe à la place un jeton délégué de courte durée : `POST /jmap/media-token` (lui-même protégé par DPoP) renvoie un token de 5 minutes limité à exactement un blob, passé en `?media_token=` dans le `src` de l'élément. Il est refusé sur toute autre route, pour tout autre blob, et après expiration.

### Le ticket éphémère de streaming

L'API `EventSource` du navigateur ne peut pas définir d'en-têtes de requête personnalisés, donc elle ne peut pas envoyer `Authorization: Bearer`. La RFC 6750 §2.3 autorise à la place le token dans la chaîne de requête, et OxiMail accepte toujours `?access_token=` sur ses deux routes de streaming. Mais un token dans une URL atterrit dans les journaux d'accès des reverse proxies et dans tout ce qui enregistre des URL. La façon prise en charge d'ouvrir un flux est donc un **ticket éphémère** dédié, frappé par un appel authentifié normalement, par en-tête :

```http theme={null}
POST /jmap/stream-ticket
Authorization: Bearer <token>
```

Sans corps. La réponse n'est jamais mise en cache (`Cache-Control: no-store`) :

```json theme={null}
{ "streamTicket": "...", "expiresAt": 1767225600 }
```

Passez-le en `?stream_ticket=` sur l'URL du flux, à la place du token de session. Ce qu'est ce ticket :

* **De courte durée : 60 secondes.** Il autorise l'*ouverture* d'un flux, pas la durée de vie du flux : un flux déjà connecté n'est pas fermé quand son ticket expire. La fenêtre n'a qu'à couvrir la frappe puis la connexion, plus la reconnexion automatique du navigateur à l'intérieur de ce laps de temps. Une coupure plus tardive demande une nouvelle frappe au niveau applicatif, que le client doit implémenter de toute façon.
* **Limité à `(organisation, compte, session d'appareil)`,** et accepté sur `/jmap/eventsource` et `/jmap/push-patches` uniquement : présenté ailleurs, il est refusé. Transporter la session d'appareil n'est pas un détail : `/jmap/push-patches` enregistre sa connexion sous cet identifiant de session, et c'est ce lien qui fait que les bases de comparaison du flux sont celles-là même que les réponses `/get` de la session ont semées. Un flux qui l'aurait perdu fonctionnerait, en envoyant silencieusement des valeurs complètes pour toujours au lieu de patchs.
* **Possession déléguée, donc il ne porte aucune preuve propre.** Il a été frappé par un appel passé dans l'extracteur normal, token dans un en-tête, et sous `dpop_resource = "require"` avec une preuve de ressource : c'est pourquoi le ticket lui-même est exempté de cette porte. Il ne porte pas non plus d'identité au-delà de son périmètre : les deux gestionnaires de flux lisent l'organisation, le compte et la session, rien d'autre.

Un ticket qui fuite dans un journal est inerte dans la minute ; le token de session de 24 heures n'entre jamais dans une URL. Le jeton `?media_token=` limité à un blob, décrit plus haut, est un token différent et volontairement plus étroit : un blob, cinq minutes, `/jmap/download` seulement.

<Note>
  `?access_token=` reste accepté sur les deux routes de streaming pour compatibilité, y compris pour les sessions liées par DPoP sous `dpop_resource = "require"` (l'API du navigateur ne peut pas envoyer d'en-têtes : c'est le seul résidu documenté de la porte de preuve par ressource). Les nouveaux clients doivent frapper un ticket.
</Note>

## Émettre des requêtes

Tous les appels de méthode vont vers un seul point d'accès, dans une unique requête POST groupée. Le corps de la requête (RFC 8620 §3.3) comporte trois parties.

```http theme={null}
POST /jmap
Authorization: Bearer <token>
Content-Type: application/json
```

```json theme={null}
{
  "using": [
    "urn:ietf:params:jmap:core",
    "urn:ietf:params:jmap:mail"
  ],
  "methodCalls": [
    ["Mailbox/get", { "accountId": "a1", "ids": null }, "c0"],
    ["Email/get", { "accountId": "a1", "ids": ["e1", "e2"] }, "c1"]
  ]
}
```

* **`using`** déclare les capacités sur lesquelles la requête s'appuie. Une méthode dont la capacité n'est pas listée dans `using` est rejetée avec `unknownCapability`. La méthode n'est jamais exécutée malgré tout.
* **`methodCalls`** est un tableau ordonné. Chaque appel est un tableau de trois éléments : `[nomMéthode, arguments, callId]`. Le `callId` est votre propre étiquette, renvoyée telle quelle pour que vous puissiez associer chaque réponse à son appel.

La réponse reprend cette structure avec un tableau `methodResponses`, dans le même ordre, chaque élément étant étiqueté avec le `callId` correspondant.

```json theme={null}
{
  "methodResponses": [
    ["Mailbox/get", { "accountId": "a1", "state": "...", "list": [ ... ] }, "c0"],
    ["Email/get", { "accountId": "a1", "state": "...", "list": [ ... ], "notFound": [] }, "c1"]
  ]
}
```

<Note>
  Un identifiant invalide ne disparaît jamais en silence. Tout ce qu'un appel `/get` ne trouve pas revient dans `notFound`. OxiMail n'abandonne jamais discrètement un identifiant illisible ou inconnu.
</Note>

## Références arrière (result references)

L'intérêt du groupage, c'est qu'un appel peut alimenter le suivant **dans la même requête**, ce qui évite un aller-retour. C'est une **référence arrière**, aussi appelée result reference (RFC 8620 §3.7). À la place d'une valeur d'argument littérale, vous passez un objet avec un préfixe `#` sur le nom de l'argument.

```json theme={null}
{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
  "methodCalls": [
    ["Email/query", { "accountId": "a1", "filter": { "inMailbox": "inbox" } }, "c0"],
    ["Email/get", {
      "accountId": "a1",
      "#ids": {
        "resultOf": "c0",
        "name": "Email/query",
        "path": "/ids"
      }
    }, "c1"]
  ]
}
```

Le second appel signifie : « pour mon argument `ids`, prends le résultat de l'appel `c0` (qui doit être un `Email/query`) et récupère la valeur au pointeur JSON `/ids` ». Le serveur résout la référence à partir du résultat du premier appel avant d'exécuter le second.

Une référence arrière doit indiquer le bon `callId` précédent, le bon `name` de méthode et un `path` valide. Si l'un de ces éléments est faux, OxiMail renvoie `invalidResultReference` au lieu de substituer `null` ou une liste vide.

## Capacités

Le serveur annonce ce qu'il sait faire via des URN de capacité dans la table `capabilities` de la session. Le client déclare ensuite celles qu'il compte utiliser en les listant dans le tableau `using` de la requête. OxiMail annonce deux familles.

### JMAP standard (v1)

Les capacités normalisées par l'IETF, dans l'espace de noms `urn:ietf:params:jmap:*` : Core, Mail ([RFC 8621](https://www.rfc-editor.org/rfc/rfc8621)), Submission, réponse d'absence, Sieve ([RFC 9661](https://www.rfc-editor.org/rfc/rfc9661)), Quota ([RFC 9425](https://www.rfc-editor.org/rfc/rfc9425)), Principals et partage ([RFC 9670](https://www.rfc-editor.org/rfc/rfc9670)), Contacts ([RFC 9610](https://www.rfc-editor.org/rfc/rfc9610)), Agendas, Fichiers et WebSocket ([RFC 8887](https://www.rfc-editor.org/rfc/rfc8887)).

### JMAP modernisé OxiMail (v2)

OxiMail expose aussi un ensemble de capacités modernisées dans l'espace de noms `urn:oximail:params:jmap:v2:*`. Ce sont les extensions JMAP modernisées d'OxiMail (Internet-Drafts en cours), qui couvrent notamment :

| URN de capacité                              | Ce qu'elle ajoute                                                                                              |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `urn:oximail:params:jmap:v2:mail`            | Une surface mail modernisée (import, envoi, annulation, réindexation) posée sur les méthodes Email classiques. |
| `urn:oximail:params:jmap:v2:threads`         | Le type `Thread` de première classe, avec fusion et convergence.                                               |
| `urn:oximail:params:jmap:v2:labels`          | Des labels plats et partageables, au lieu de dossiers imbriqués.                                               |
| `urn:oximail:params:jmap:v2:rules`           | Des règles de filtrage typées, avec une garantie de temps d'exécution par règle.                               |
| `urn:oximail:params:jmap:v2:calendar`        | Des règles de récurrence et des participants de première classe.                                               |
| `urn:oximail:params:jmap:v2:contacts`        | Un modèle de contacts modernisé, avec une table de mutabilité documentée.                                      |
| `urn:oximail:params:jmap:v2:streaming`       | La reprise du push et la prise en charge des requêtes en flux.                                                 |
| `urn:oximail:params:jmap:v2:resource-limits` | Des limites de conformité annoncées, qu'un client peut vérifier avant d'envoyer.                               |

Voir [JMAP v2 modernisé](./jmap-v2) pour la surface complète.

### Mélanger v1 et v2 dans une même requête

En v0.30.0, les surfaces mail, contacts et agenda v1 et v2 sont servies par les **mêmes gestionnaires**, grâce à un mécanisme de double capacité : un gestionnaire qui annonce une capacité v1 comme capacité principale accepte aussi la capacité v2 correspondante comme capacité additionnelle. Une requête peut donc lister **à la fois** une capacité v1 et son équivalent v2 dans `using` en même temps. La structure renvoyée sur le réseau est l'union des propriétés v1 et v2, et le client lit celle qu'il veut via l'argument `properties` de `/get`.

<Note>
  Le protocole conserve une vérification d'exclusion mutuelle pour un usage futur, mais en v0.30.0 l'ensemble des paires v1/v2 mutuellement exclusives est vide. Rien n'est rejeté lorsqu'on associe une capacité v1 à son équivalent v2. Si une future surface v3 réintroduisait une séparation stricte, une requête qui associerait les deux URN exclus serait rejetée avec `unknownCapability`.
</Note>

## Le téléchargement de blob, et ce que signifie un échec

Les blobs sont récupérés depuis le modèle `downloadUrl`, soit `/jmap/download/{accountId}/{blobId}/{name}?type={type}`, avec l'en-tête `Authorization`, ou avec un `?media_token=` pour un élément média (voir [plus haut](#tokens-liés-à-lappareil-dpop-rfc-9449)).

Une propriété du modèle de stockage est visible ici. Pour un message, le **blob brut RFC 5322 est la source unique de vérité** ; les blobs par partie qu'un client télécharge en sont un *cache dérivé*. Les parties qui sont des pièces jointes sont matérialisées à l'ingestion ; les parties de corps ne le sont pas du tout. Un téléchargement portant sur une partie absente du disque est donc normal, et le serveur y répond en re-dérivant : il localise le message propriétaire, charge et déchiffre le brut côté serveur, le re-analyse, extrait la partie correspondante, **vérifie que l'empreinte en clair de la partie égale le `blobId` demandé**, puis sert les octets avec un `200`. Des octets non vérifiés ne sont jamais servis.

Quand cela ne peut pas aboutir, la réponse dit de quel cas il s'agit, au lieu d'un `404` nu :

| Réponse | `type`                   | Ce que cela signifie                                                                                                                                                                                      |
| ------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `404`   | `blobNotFound`           | Aucun message propriétaire n'est connu pour cette empreinte. Il n'y a rien à partir de quoi dériver, et rien n'a été perdu non plus.                                                                      |
| `500`   | `blobMissing`            | Un message propriétaire est connu, mais son blob brut est absent ou indéchiffrable. La source de vérité elle-même a disparu : c'est un signal de perte de données, et le serveur le journalise comme tel. |
| `500`   | `partDerivationMismatch` | Les messages propriétaires se sont chargés et analysés, mais aucun ne dérive de partie portant l'empreinte demandée : les lignes de localisation sont périmées ou corrompues.                             |

La distinction est tout l'enjeu : un client, ou un opérateur qui lit les journaux, peut désormais différencier « ce blob n'a jamais existé ici » de « ce blob devrait exister et le message derrière lui a disparu », ce qu'un `404` unique confondait.

<Note>
  La re-dérivation s'applique aux requêtes portant sur les blobs de votre **propre** compte. Les portes de téléchargement des comptes partagés et des avatars de pairs conservent volontairement une sémantique de `404` indiscernable, pour ne pas devenir un oracle d'existence, et ne re-dérivent jamais.
</Note>

**Un blob téléversé est à l'abri avant que vous le référenciez.** Entre votre téléversement et le `/set` qui le nomme, un blob est non référencé par protocole (RFC 8620 §6.1). Le ramasse-miettes de blobs ne récupère jamais un blob non référencé plus jeune qu'une **fenêtre de grâce de 24 heures**, lue sur l'horodatage du fichier lui-même ; un horodatage illisible ou situé dans le futur conserve le blob au lieu de le collecter. Téléversez, prenez le temps qu'il vous faut, référencez ensuite.

## Push : savoir quand l'état change

Chaque type JMAP suit une **chaîne d'état** opaque. Quand vous appelez `Foo/get`, la réponse inclut l'état courant (`state`). Plus tard, vous pouvez appeler `Foo/changes` avec le dernier état que vous avez vu, et le serveur vous indique exactement quels objets ont été créés, mis à jour ou détruits depuis. Vous ne sondez jamais des listes complètes : vous synchronisez des deltas.

Pour savoir *quand* appeler `Foo/changes`, OxiMail envoie une petite notification `StateChange` dès que l'état d'un type avance. Il existe deux transports, tous deux annoncés dans la session.

### EventSource (Server-Sent Events)

Un flux SSE standard à l'adresse `eventSourceUrl`.

```
GET /jmap/eventsource/?types=*&stream_ticket=<ticket>
```

Le serveur diffuse des événements `StateChange`. Chaque événement nomme le compte et la collection dont l'état a bougé, ainsi que la nouvelle chaîne d'état. Utilisez `types=Email,Mailbox` pour filtrer sur certaines collections, `ping=` pour l'intervalle de keep-alive, et `stream_ticket=` pour l'authentification dans le navigateur ([voir plus haut](#le-ticket-éphémère-de-streaming)). Le flux est limité au compte et à l'organisation authentifiés : vous ne recevez que vos propres changements.

### WebSocket

OxiMail parle aussi JMAP par-dessus WebSocket (RFC 8887). La session annonce une URL `wss://.../jmap/ws` avec `supportsPush: true`. Un client qui détient déjà une connexion WebSocket peut donc à la fois envoyer des appels de méthode et recevoir le push sur le même canal.

Dans les deux cas, le principe est le même : une notification push est un indice qu'une collection a changé d'état. Le client enchaîne avec `Foo/changes` pour récupérer le delta réel.

### La fenêtre de rejeu et les deltas refusés

Le journal des changements est conservé **90 jours**, et ce chiffre est publié, pas interne : la capacité `urn:oximail:params:jmap:v2:streaming` l'annonce en `replayWindowSeconds`, dérivé de la constante même par laquelle le worker de rétention purge. L'annonce ne peut donc pas dériver du mécanisme.

Un client dont le `sinceState` pointe dans l'intervalle purgé est **refusé**, avec l'erreur de la RFC 8620 §5.2 :

```json theme={null}
["error", { "type": "cannotCalculateChanges" }, "c0"]
```

Ce refus signifie : *le delta n'est pas calculable, faites une resynchronisation complète*. Ce n'est pas une panne passagère et réessayer n'y changera rien. Les objets créés puis détruits à l'intérieur de l'intervalle purgé sont irrécupérables depuis le journal, et un objet détruit là resterait sinon indéfiniment dans le cache du client, avec un `Foo/get` qui répond `notFound`. Traitez `cannotCalculateChanges` en jetant l'état mis en cache pour cette collection et en la re-interrogeant intégralement.

Le plancher qui produit ce refus est enregistré par la purge elle-même, dans la même transaction que la suppression, par (organisation, compte, collection). C'est donc une propriété du stockage, et tous les consommateurs en héritent : toutes les méthodes `Foo/changes`, `Push/catchup`, le rejeu des push-patches, et le `sync-collection` de CalDAV et CardDAV, qui répond au même cas en exigeant une synchronisation complète. Un curseur *au niveau* du plancher reste servable : seul un curseur en dessous est refusé. Un compte qui n'a jamais subi de purge répond à n'importe quel curseur.

<Note>
  Être hors fenêtre est un cas attendu : un téléphone réinstallé, un portable de retour après une longue absence, un client DAV en pause. C'était être hors fenêtre *en silence* qui constituait le défaut : avant l'existence de ce plancher, un tel client recevait un delta partiel accompagné d'un `newState` frais, et en concluait qu'il était parfaitement synchronisé.
</Note>

### Un identifiant, une issue

Dans une même réponse `Foo/changes`, un identifiant apparaît dans **au plus un** des ensembles `created`, `updated`, `destroyed`, et une seule fois (RFC 8620 §5.2). Le stockage réduit les lignes brutes du journal à l'issue terminale sur la fenêtre demandée : créé puis mis à jour donne `created` ; créé puis détruit est omis entièrement, puisque le client n'a jamais vu l'objet ; mis à jour puis détruit donne `destroyed` ; détruit puis recréé donne `updated`. Un client n'a donc jamais à réconcilier un identifiant qui se prétend à la fois mis à jour et détruit, ni besoin de l'heuristique « `updated` intersecté avec `notFound` » pour retirer un objet.

## Pour aller plus loin

* [JMAP Mail](./jmap-mail) : les méthodes de boîtes aux lettres et de messages.
* [JMAP v2 modernisé](./jmap-v2) : la surface modernisée en détail.
* [Concepts](../concepts) : le modèle de données et le vocabulaire derrière ces méthodes.
* [Architecture en un coup d'œil](../architecture) : la place de la couche JMAP dans le binaire unique.
