> ## 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.

# oximail-mail

> Le crate de domaine JMAP Mail : Mailbox, Email, Thread, EmailSubmission, Identity et VacationResponse, le chemin d'ingestion et le chemin de lecture par projection de colonnes.

`oximail-mail` est le crate de domaine JMAP Mail. Il implémente **JMAP Mail** ([RFC 8621](https://www.rfc-editor.org/rfc/rfc8621)) au-dessus de la machinerie de dispatch, de capacités et de stockage fournie par [oximail-core](./oximail-core) et [oximail-store](./oximail-store). Il possède le modèle de données mail (`Mailbox`, `Email`, `Thread`, `EmailSubmission`, `Identity`, `VacationResponse`), les handlers de méthode JMAP pour chacun, l'unique chemin d'ingestion vers lequel tout message entrant ou créé localement converge, et le chemin de lecture par projection de colonnes qui ne matérialise que les colonnes lues par un consommateur donné.

## Vue d'ensemble

Un client JMAP gère sa boîte mail via les méthodes de ce crate : il liste les dossiers avec `Mailbox/get`, récupère les messages avec `Email/get`, recherche avec `Email/query`, regroupe les conversations avec `Thread/get`, et envoie avec `Email/send` ou `EmailSubmission/set`. Chaque handler implémente le trait `JmapMethod` de `oximail-core` et est enregistré par le binaire. Le dispatcher route une requête en lot vers lui après avoir résolu les back-references, validé le compte et vérifié les capacités.

Le crate se situe au-dessus de `oximail-core` et `oximail-store`, et ne s'adresse jamais directement à un autre crate de domaine. Quand un message entrant contient une invitation calendrier, `oximail-mail` n'appelle pas le crate calendrier : il émet un `DomainEvent` sur l'`EventBus`, et l'abonné calendrier réagit. Cela maintient les crates mail et calendrier découplés (ADR-011).

Tout le stockage passe par les traits `Store`, `BlobStore` et `SearchStore`. Chaque méthode porte donc le `tenant_id`, et un handler ne peut physiquement pas lire le courrier d'une autre organisation (ADR-015). Les octets RFC 5322 bruts vivent dans le blob store adressé par contenu. Les métadonnées analysées vivent dans le store structuré. Le message est analysé une seule fois, à l'ingestion, et jamais ré-analysé à la lecture (ADR-009).

## Fonctions

* **Mailbox** (RFC 8621 §2) : dossiers avec rôles (`inbox`, `sent`, `drafts`, `trash`, `junk`, `archive`), hiérarchie, compteurs par dossier, état d'abonnement et droits de partage RFC 9670.
* **Email** (RFC 8621 §4) : l'objet message, dont les métadonnées (`from`, `to`, `subject`, `preview`, `hasAttachment`, `threadId`, mots-clés) sont précalculées à l'ingestion, le corps étant extrait à la demande depuis le blob brut.
* **Thread** (RFC 8621 §3) : l'agrégat de conversation, une liste ordonnée d'identifiants d'emails regroupés par recherche de chaîne Message-Id (ADR-019).
* **EmailSubmission** (RFC 8621 §7) : la demande d'envoi, avec envoi programmé et l'effet de bord `onSuccessUpdateEmail` appliqué *après* la livraison.
* **Identity** (RFC 8621 §6) : les adresses d'expéditeur qu'un compte peut utiliser, avec signatures et champs de profil étendus.
* **VacationResponse** (RFC 8621 §8) : le singleton de réponse automatique par compte.
* **Le chemin d'ingestion** : un unique point de convergence `ingest_email` pour tout message entrant ou créé localement (SMTP entrant, soumission SMTP, `Email/import`, `Email/set create` et migration).
* **Chemin de lecture par projection de colonnes** (ADR-062) : un unique registre déclaratif de colonnes pilote à la fois quelles colonnes SQL une lecture doit faire avec `SELECT` et à quels groupes de propriétés la demande d'un client se résout, de sorte que chaque lecture ne matérialise que ce dont elle a besoin.
* **Le pont v1 vers v2** : `Email/get` et `Email/set` acceptent la forme v1 RFC 8621, la forme v2 modernisée, ou les deux à la fois.

## Comment ça marche

### Le modèle de données

Chaque type est une structure serde indexée par un `Id` UUIDv7. `Mailbox` porte son rôle, son parent, ses compteurs et ses droits de partage. `Email` porte les métadonnées précalculées ; le corps (`textBody`, `htmlBody`, `bodyValues`, `bodyStructure`) n'est pas stocké en colonnes mais extrait à la demande depuis le blob brut quand un client le réclame. `Thread` est l'agrégat léger, juste une liste ordonnée `email_ids`, car le travail de regroupement a lieu à l'ingestion. `VacationResponse` est un singleton : son identifiant vaut toujours la chaîne `"singleton"`, puisqu'il n'y en a qu'un par compte.

### Ingestion : un seul chemin, entièrement vérifié

`ingest_email` est l'unique point de convergence pour toutes les façons qu'un message a d'entrer dans le système. Il analyse le MIME une seule fois (avec limites de profondeur et de nombre de parties : un message trop complexe est rejeté, pas tronqué en silence), stocke les octets RFC 5322 bruts comme blob adressé par contenu ainsi que chaque partie MIME comme son propre blob, résout le thread par Message-Id, In-Reply-To et References, écrit les métadonnées structurées, indexe le texte recherchable dans Tantivy, recalcule les compteurs de threads du dossier, enregistre les changements JMAP pour `Email`, `Mailbox` et `Thread`, et émet les événements `StateChanged` et `EmailIngested` correspondants. Une partie `text/calendar` émet en plus `ITipReceived` pour l'abonné calendrier.

L'ingestion entière (la ligne du message, son appartenance au dossier **et** les entrées du journal de changements qui la rendent visible à la synchronisation incrémentale `Email/changes`) est validée dans une seule transaction unifiée. Soit le message atterrit avec sa visibilité de synchronisation, soit toute l'ingestion est annulée vers un `4xx` que l'expéditeur retente. Une écriture du journal de changements ne peut plus être perdue sur un busy\_timeout après la validation de la ligne du message, ce qui laissait un message durable invisible pour toujours à la synchronisation incrémentale (corrigé avec la migration v193, qui répare aussi l'arriéré sur disque).

Le chemin est protégé par un test de checklist explicite : chaque étape de `ingest_email` est vérifiée par `test_ingest_full_checklist`, donc une étape ajoutée sans assertion correspondante fait échouer la suite. C'est la discipline derrière l'ADR-017 : tout message accepté par le serveur doit finir stocké quelque part.

### Le chemin de lecture par projection de colonnes

Une lecture naïve charge chaque colonne de la ligne `emails` même quand l'appelant n'a besoin que des drapeaux. Le chemin de projection (ADR-062) évite cela. Un unique registre déclaratif, `EMAIL_COLUMNS`, est la source de vérité : chaque entrée associe une colonne SQL à son type d'accesseur (obligatoire ou optionnel, gestion du NULL), à son groupe de stockage et à son sélecteur de propriété de transport.

Deux consommateurs lisent ce registre. `email_select_columns` construit la liste `SELECT` pour une projection donnée, en n'utilisant que des noms de colonnes statiques, de sorte qu'aucune chaîne client n'est jamais concaténée dans une requête. `EmailProjection::from_jmap_properties` résout l'ensemble de `properties` demandé par un client vers l'ensemble minimal de groupes qui les portent. `MailStore::get_emails_projected` réunit les deux : il construit le `SELECT` étroit et analyse le résultat par nom de colonne, en tolérant tout sous-ensemble ou tout ordre de colonnes.

Un ensemble obligatoire de 16 colonnes (celles qui alimentent le parser, la logique de sécurité et de dérivation, le descripteur de chiffrement et l'état de cycle de vie) est toujours matérialisé, quelle que soit la projection. Tout le reste (le corps inline, le JSON d'en-têtes, les listes d'adresses d'enveloppe, les références, l'aperçu, les jetons de recherche, les résultats d'authentification) n'est chargé que quand un groupe le sélectionne. La sortie de transport est identique octet pour octet à une lecture complète ; la projection ne fait que réduire les colonnes récupérées.

<Note>
  Le chemin de projection est en **lecture seule**. Son gain est la réduction du chargement de colonnes : une requête qui ne lit que les drapeaux cesse de charger le JSON d'en-têtes, le corps inline, l'enveloppe, les références, l'aperçu, les jetons de recherche et les résultats d'authentification. Chaque valeur émise est identique à une matérialisation complète. L'état de suivi des changements par message (modseq) n'est délibérément **pas** sur la surface de projection : il est lu par une requête latérale dédiée, de sorte que la projection ne peut jamais affecter la sortie de suivi des changements.
</Note>

### Le pont v1 vers v2

OxiMail annonce deux générations de capacité mail côte à côte : la capacité standard `urn:ietf:params:jmap:mail` (v1, RFC 8621) et l'extension mail modernisée d'OxiMail, `urn:oximail:params:jmap:v2:mail` (l'extension JMAP mail modernisée d'OxiMail, un Internet-Draft en cours). La surface v2 ne remplace pas la v1 : le même handler enregistre l'URN v2 comme capacité *additionnelle*. `Email/get` et `Email/set` acceptent donc la forme v1 RFC 8621, la forme v2 modernisée, ou les deux à la fois.

<Note>
  Les capacités mail v1 et v2 ne sont **pas** mutuellement exclusives. Le dispatcher du core tient une liste de paires v1 et v2 mutuellement exclusives, mais cette liste est **vide** sur la ligne actuelle : mail a abandonné sa paire. Une requête qui liste à la fois `urn:ietf:params:jmap:mail` et `urn:oximail:params:jmap:v2:mail` dans `using` est acceptée, et la réponse est l'union des propriétés v1 et v2. Un client v2 qui lit les propriétés modernisées `userFlags`, `folder` ou `state` n'a pas à annoncer la capacité v1.
</Note>

Le modèle v2 ajoute une disposition catégorisée des propriétés, une machine à états de cycle de vie (`draft` vers `sending` vers `sent`, avec `scheduled`, `failed`, `cancelled`), des drapeaux utilisateur natifs, une famille d'audit et un vecteur de causalité. Les mots-clés v1 (`$seen`, `$flagged`, `$answered`) et les `userFlags` v2 convergent vers une seule source de vérité stockée : un client v1 qui lit `keywords` et un client v2 qui lit `userFlags` voient les mêmes faits, dérivés de la même colonne.

### Envoi : la soumission après la livraison

`Email/send` et `EmailSubmission/set` créent la demande d'envoi. Le crate définit le trait `SubmissionSender` (implémenté par la file de livraison du crate SMTP) et le trait `RecipientExpander` (implémenté par l'extension de groupes de distribution), tous deux déclarés ici pour éviter une dépendance circulaire : le crate mail possède le contrat, les autres crates l'implémentent.

L'effet de bord `onSuccessUpdateEmail`, le patch qui déplace un brouillon envoyé vers le dossier Sent et bascule ses mots-clés, est appliqué **après** la fin de la livraison, pas avant. C'est le correctif du bug send-to-self de Stalwart : le dedup tient compte du contexte de la boîte, et la mise à jour post-livraison ne laisse jamais un message vers soi-même se faire avaler par une correspondance Message-Id prématurée.

### Garanties de correction

Voici des garanties que le crate respecte, chacune liée à un échec passé précis qu'il refuse de répéter :

* **Le message vers soi-même n'est jamais perdu en silence.** Le dedup tient compte du contexte de la boîte et `onSuccessUpdateEmail` s'exécute après la livraison, donc un message adressé à son propre compte est livré et stocké au lieu d'être dédupliqué.
* **Les identifiants invalides vont dans `notFound`.** Tout identifiant d'`Email/get` qui ne s'analyse pas ou n'existe pas est signalé dans le tableau `notFound`, jamais filtré en silence.
* **`hasAttachment` lit le drapeau stocké.** Le filtre `hasAttachment` d'`Email/query` et la propriété d'`Email/get` utilisent le drapeau de pièce jointe calculé à partir des vraies parties MIME à l'ingestion, pas un balayage du texte du corps extrait.
* **Un échec de déchiffrement remonte, il ne se cache pas.** Si un corps ne peut pas être déchiffré à la lecture, `Email/get` renvoie un `serverFail`, pas une réponse réduite aux métadonnées qui ressemblerait à un message légitimement vide.

### Invariants

* **Ne jamais échouer en silence.** Les identifiants non analysables ou inconnus atterrissent dans `notFound` ; un message accepté est toujours stocké (ADR-017) ; un échec de déchiffrement renvoie une erreur, jamais un corps vide.
* **Analyser une seule fois.** Le message est analysé à l'ingestion et stocké à la fois en octets bruts et en métadonnées structurées ; les lectures ne ré-analysent jamais (ADR-009).
* **Un seul mécanisme de lecture.** `Email/get` et les chemins de lecture IMAP passent tous par le même constructeur de projection, donc la liste de colonnes et le parser de ligne ne dérivent jamais vers deux implémentations.
* **Aucun import inter-domaines.** L'intégration calendrier passe par l'`EventBus`, jamais par un import direct.

## Configuration

`oximail-mail` ne lit aucun fichier de configuration directement ; il consomme les stores et les limites que le binaire résout. Les réglages côté opérateur qui influencent la surface mail (la taille maximale de message, les plafonds de volume par collection, les chemins de base des blobs et de la recherche, et la clé de chiffrement au repos) sont documentés dans la [référence de configuration](../operator/configuration). Le modèle de chiffrement au repos derrière les blobs de message (le schéma façon LUKS, pas du bout en bout) est traité dans [chiffrement au repos](../operator/encryption-at-rest).

## Surface protocolaire / JMAP

`oximail-mail` implémente les méthodes JMAP Mail. Elles sont annoncées sous `urn:ietf:params:jmap:mail` (v1) et, le cas échéant, `urn:oximail:params:jmap:v2:mail` (v2).

| Méthode                                                      | Rôle                                                                                                                           |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `Mailbox/get`, `/set`, `/query`, `/queryChanges`, `/changes` | RFC 8621 §2 : dossiers, rôles, hiérarchie, compteurs, partage.                                                                 |
| `Email/get`, `/set`, `/query`, `/queryChanges`, `/changes`   | RFC 8621 §4 : récupérer, muter, rechercher, paginer et comparer les messages.                                                  |
| `Email/import`                                               | RFC 8621 §4.8 : importer un message RFC 5322 brut dans une boîte.                                                              |
| `Email/parse`                                                | RFC 8621 §4.9 : analyser un blob en structure Email sans le stocker.                                                           |
| `Email/send`                                                 | Envoi de confort (créer le message et le soumettre en un seul appel).                                                          |
| `Email/cancel`, `Email/wake`, `Email/reindex`                | Cycle de vie v2 : annuler un envoi programmé, réveiller un message en sommeil, reconstruire l'index de recherche d'un message. |
| `EmailSubmission/get`, `/set`, `/query`, `/changes`          | RFC 8621 §7 : demandes d'envoi, envoi programmé, `onSuccessUpdateEmail`.                                                       |
| `Thread/get`, `/changes`, `/queryChanges`                    | RFC 8621 §3 : l'agrégat de conversation.                                                                                       |
| `Identity/get`, `/set`, `/changes`                           | RFC 8621 §6 : identités d'expéditeur et signatures.                                                                            |
| `VacationResponse/get`, `/set`                               | RFC 8621 §8 : le singleton de réponse automatique par compte.                                                                  |
| `SearchSnippet/get`                                          | RFC 8621 §5 : extraits de correspondance surlignés pour une requête.                                                           |
| `Quota/get`                                                  | Quota de stockage par compte.                                                                                                  |
| `Preferences/get`, `/set`                                    | Singleton de préférences utilisateur.                                                                                          |

Voir [JMAP Mail](../developer/jmap-mail) pour la visite guidée côté client, quand elle sera disponible.

## Décisions de conception

* **ADR-009 (stocker brut plus structuré)** : le message est analysé une fois à l'ingestion ; les octets RFC 5322 bruts vont au blob store et les métadonnées analysées au store structuré, donc `hasAttachment`, `preview` et `threadId` sont calculés une fois, jamais à la lecture.
* **ADR-014 (dedup de blob par hash de contenu)** : le message brut et chaque partie MIME sont adressés par `SHA-256` du contenu, donc des pièces jointes identiques partagent un seul blob ; deux messages avec le même Message-Id dans des boîtes différentes restent deux objets Email distincts.
* **ADR-019 (thread par chaîne Message-Id)** : les threads sont résolus par recherche exacte de la chaîne Message-Id stockée en texte clair, pas un hash, donc le risque de collision est nul.
* **ADR-017 (un email accepté doit être livré)** : tout message accepté par le serveur finit stocké ; le test de checklist de `ingest_email` vérifie chaque étape du chemin.
* **ADR-062 (cohabitation stockage RFC 5322 brut plus structuré, brouillon/projection)** : le chemin de lecture par projection de colonnes et la cohabitation du corps de brouillon v2 ; un brouillon v2 garde son corps dans des colonnes natives jusqu'à ce qu'`Email/send` sérialise un blob à la frontière SMTP.
* **ADR-011 (bus d'événements inter-domaines)** : l'intégration calendrier est un événement `ITipReceived`, pas un appel direct vers le crate calendrier.

Les ADR vivent dans le dépôt serveur d'OxiMail ; voir l'[index des ADR](../reference/adr-index).

## Standards

* [RFC 8621](https://www.rfc-editor.org/rfc/rfc8621) : JMAP for Mail. La référence principale de ce crate : Mailbox (§2), Thread (§3), Email (§4), recherche et extraits (§5), Identity (§6), EmailSubmission (§7), VacationResponse (§8).
* [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322) : Internet Message Format. Le format de transport des octets bruts stockés dans le blob store et analysés à l'ingestion.
* [RFC 9670](https://www.rfc-editor.org/rfc/rfc9670) : JMAP Sharing. Les champs `shareWith` et `myRights` de la boîte.

L'extension mail v2 modernisée (la disposition catégorisée des propriétés, la machine à états de cycle de vie et les drapeaux utilisateur natifs) est l'extension JMAP mail modernisée propre à OxiMail, un Internet-Draft en cours. Elle est annoncée sous `urn:oximail:params:jmap:v2:mail`.

## Carte des sources et tests

| Fichier (dans le crate)   | Contenu                                                                                                                                                                   |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/email.rs`            | La structure de métadonnées `Email`, `EmailFilter`, `EmailSort`.                                                                                                          |
| `src/email_v2.rs`         | Les extensions Email v2 : la machine à états de cycle de vie `EmailState`, les groupes de propriétés catégorisés, les drapeaux utilisateur, l'audit, la causalité.        |
| `src/email_projection.rs` | Le registre `EMAIL_COLUMNS`, `email_select_columns` et `EmailProjection::from_jmap_properties` : la fondation de la projection de colonnes.                               |
| `src/mailbox.rs`          | La structure `Mailbox` et l'énumération `MailboxRole`.                                                                                                                    |
| `src/thread.rs`           | L'agrégat `Thread` (ADR-019).                                                                                                                                             |
| `src/submission.rs`       | `EmailSubmission`, `Envelope` et les traits `SubmissionSender` / `RecipientExpander`.                                                                                     |
| `src/identity.rs`         | La structure `Identity`.                                                                                                                                                  |
| `src/vacation.rs`         | Le singleton `VacationResponse`.                                                                                                                                          |
| `src/ingest.rs`           | `ingest_email` : l'unique point de convergence d'ingestion, protégé par `test_ingest_full_checklist`.                                                                     |
| `src/mail_store.rs`       | `MailStore` : les opérations de store structuré, dont `get_emails_projected`.                                                                                             |
| `src/methods/`            | Les handlers de méthode JMAP (`email_get`, `email_set`, `email_query`, `mailbox_*`, `thread_*`, `identity_*`, `email_submission_*`, `vacation_response_*` et les autres). |
| `src/subscribers/`        | Les abonnés aux événements iTIP (envoyeurs de reply / request / counter).                                                                                                 |

Le chemin d'ingestion porte un unique test de checklist qui vérifie chaque étape de `ingest_email` ; modifier le chemin impose de mettre à jour ce test. La forme de transport v2 est figée par un test de format de transport inter-couches.

## État

Implémenté et en production en **v0.30.0** : le modèle complet Mailbox / Email / Thread / EmailSubmission / Identity / VacationResponse, tous les handlers de méthode JMAP listés ci-dessus, l'unique chemin `ingest_email` avec sa protection par checklist, et le pont de double capacité v1 / v2 sur `Email/get` et `Email/set`.

Le **chemin de lecture par projection de colonnes est câblé de bout en bout sur les chemins de lecture** (réel, testé) : la primitive de store (`get_emails_projected` / `get_email_projected`), `Email/get` (qui résout une demande de `properties` vers son ensemble minimal de groupes) et les chemins de lecture IMAP pilotent tous leur projection à partir des besoins réels des consommateurs. Le delta net de comportement de transport est nul : la projection ne fait que réduire les colonnes chargées.

<Note>
  Quelques éléments adjacents à la projection sont **encore en spécification ou différés** et ne sont pas câblés sur la ligne actuelle : le rejet strict d'une propriété `Email/get` inconnue, l'élagage de sur-émission pour `headerJson`, le court-circuit propriété-de-corps-sans-`bodyValues`, et le delta inline d'`Email/changes` (en attente de la décision produit sur les threads / conversations). Le refactor du *handler* v2 (le chemin de mise à jour d'`Email/set` qui applique la machine à états sur les types v2) est planifié comme suite ; les *types* v2 et la forme de transport sont implémentés aujourd'hui, et les handlers v1 existants continuent d'opérer sur les métadonnées v1.
</Note>
