Skip to main content
Au-delà du courrier, OxiMail sert les agendas, les contacts, les tâches, les fichiers et le chat en JMAP, avec des passerelles CalDAV/CardDAV pour les clients classiques (protocoles historiques). Cette page couvre les comportements qu’un exploitant doit comprendre, et les réglages et outils de réparation qui vont avec.

Planification de réunions (iTIP)

Quand des utilisateurs s’invitent, le serveur échange les messages de planification (REQUEST / REPLY / CANCEL) par courrier, selon la RFC 5546. Deux règles empêchent les ratés :
  • Côté organisateur seulement. OxiMail n’envoie de courrier de planification que pour les événements dont l’ORGANIZER se résout vers un compte local (RFC 5546 §3.2 : seul le serveur de l’organisateur envoie les invitations). Un événement arrivé comme invitation entrante ne réémet jamais de REQUEST quand le client du participant le réécrit, et supprimer une copie reçue ne diffuse jamais de CANCEL non autorisé : sur les chemins JMAP et CalDAV, en mode fermé.
  • Émis après le commit. Les messages iTIP ne partent qu’une fois la transaction qui a modifié l’événement réellement validée. Un conflit d’édition concurrente (stateMismatch) ou tout échec de validation n’émet rien : une invitation ne peut plus s’échapper pour un changement annulé, et un REQUEST de mise à jour porte toujours le contenu postérieur au changement, avec la SEQUENCE incrémentée.
Les invitations entrantes (parties text/calendar) sont traitées à l’ingestion, y compris le style de propriétés paramétrées d’Outlook, et mettent à jour l’agenda du destinataire, sous une troisième règle :
  • Appliquées seulement par qui a autorité (RFC 5546 §3.2). Un message classé pourriel ou refusé par DMARC ne bouge rien ; un REQUEST sur un événement connu et un CANCEL ne s’appliquent que depuis l’organisateur que l’événement stocké nomme, un nouveau REQUEST seulement quand son ORGANIZER est l’expéditeur, un REPLY seulement depuis le participant qu’il modifie. Détenir l’UID, que porte chaque .ics transféré et chaque export de flux full, ne permet plus à n’importe qui d’annuler ou de réécrire la réunion d’autrui. Un message refusé reste dans la boîte et le refus est journalisé avec une reason en vocabulaire fermé (junk, dmarc_fail, no_sender, not_the_organizer, organizer_mismatch, no_organizer, not_the_attendee, organizer_replies) ; un domaine externe sans DMARC reste usurpable par un From: exact qui connaît aussi l’UID.
L’événement, sa notification et les entrées de journal de changements s’écrivent dans une seule transaction, si bien qu’un agenda ne change jamais sans que les clients synchronisés en soient avertis.

Libre/occupé et disponibilité

Trois surfaces de lecture répondent à « quand cette personne est-elle libre », toutes à travers une seule autorité d’énumération des occurrences (un événement récurrent se développe partout à l’identique, changement d’heure compris) :
  • Principal/getAvailability (JMAP) compose la disponibilité depuis les agendas du sujet dont includeInAvailability vaut all ou attending (qu’un agenda soit abonné ou non n’y joue aucun rôle) ; un événement privacy: "secret" cache entièrement son temps occupé ; les détails d’un événement ne sont fournis qu’aux appelants qui détiennent un vrai droit de lecture : l’accès au libre/occupé n’implique jamais l’accès aux détails. Un appelant sans droit de libre/occupé sur aucun agenda du sujet reçoit un forbidden explicite, pas une disponibilité vide et trompeuse. Chaque réponse dit si elle est complète (complete) : une ligne sans fin stockée d’avant l’indexation de la fin (oximail calendar backfill-end-dt --apply referme ce cas), ou une série plus longue que le plafond de la portée de lecture, la rend false, et la recherche de créneau (Event/proposeTimes, qui nomme ces personnes dans incompleteFor) comme la page publique de réservation ne la lisent jamais comme « libre » ; la page de réservation ne propose alors aucun créneau (disponibilité).
  • La free-busy-query CalDAV renvoie un VFREEBUSY de périodes occupées fusionnées (le provisoire compte comme occupé ; les événements libres/transparents, à la corbeille ou annulés sont exclus).
  • L’interface de planification d’un client s’appuie sur les mêmes données.

Ce que CalDAV montre d’un agenda partagé

Un agenda qu’un collègue vous partage se lit aussi depuis un client CalDAV, en lecture seule : le propriétaire garde exactement son comportement, un autre compte du même serveur obtient la lecture, agenda par agenda, selon la même règle de droits que le reste du produit (une seule règle, donc les deux surfaces ne peuvent pas diverger). La découverte ne liste que ce qui est partagé, un agenda non partagé reste absent plutôt que révélé par une erreur, et une écriture répond par un refus qui dit où écrire (« par le workspace, ou en tant que propriétaire de l’agenda »). La privacy propre à l’événement est honorée sur cette face par la même fonction qu’en JMAP (confidentialité, ADR-152) : private est réduit à son heure, le titre remplacé par une étiquette dans la langue du lecteur ; secret n’est pas servi du tout, créneau compris. Un filtre text-match s’applique au texte réduit, si bien que des requêtes oui/non successives ne peuvent pas deviner un titre caché ; l’ETag est calculé sur ce qui est servi, donc deux lecteurs d’une même adresse ne partagent jamais un validateur et les modifications du propriétaire sur une description retenue ne font bouger rien que le lecteur ait le droit de connaître ; un événement qui devient secret atteint sync-collection par la même pierre tombale qu’un événement supprimé, si bien que la copie déjà détenue est retirée. CLASS: sur le fil est une métadonnée, jamais le contrôle. La surface DAV annonce les classes de conformité 1, 3 avec sa classe d’accès (calendar-access, addressbook), jamais la classe 2 : aucun LOCK/UNLOCK n’existe, et un audit exige que toute classe annoncée soit tenue. Un REPORT que la collection n’implémente pas est refusé 403 avec <DAV:supported-report>, un corps vide ou malformé 400, jamais la collection entière ; un segment d’URL qui n’est pas un compte répond 404, pas un principal ; et les plafonds d’ADR-072 s’appliquent aux portes depuis un seul registre (maxIcalBodySizeBytes et maxVcardBodySizeBytes, 1 Mio, répondent 413 sur les PUT CalDAV et CardDAV) plutôt qu’en littéraux dans les routes (protocoles historiques). Un PUT que le magasin refuse répond 403 avec la précondition de la RFC et la raison dans DAV:responsedescription, jamais le 500 que tout échec du magasin produisait : CALDAV:valid-calendar-data pour un événement ou une tâche à deux RRULE, une borne de récurrence hors du profil, un DTEND antérieur à son DTSTART, CARDDAV:valid-address-data pour une fiche dont une adresse, un nom ou un numéro de téléphone porte un caractère de contrôle. Les règles sont celles de JMAP, tenues là où les lignes s’écrivent, si bien qu’aucune porte n’est une entrée de côté (récurrence). oximail doctor compte les événements et tâches stockés au-delà de la borne d’une règle (recurrence_rules) et les règles hors du profil fermé, encore stockées telles que reçues (recurrence_profile). Une CLASS autre que PUBLIC et PRIVATE (CONFIDENTIAL, un x-name) s’importe en secret, un privacy hors de public, private, secret est refusé à chaque porte, et les lignes qui portaient déjà un autre niveau ont été remises sur secret par une migration de schéma : un niveau illisible échoue fermé.

Fuseaux horaires

Les événements flottants (sans fuseau explicite) se résolvent par une chaîne de repli : fuseau de l’événement → fuseau de l’utilisateur → défaut de l’organisation → UTC. L’assistant capture le défaut de l’organisation à l’installation (premier démarrage) ; modifiez-le ensuite avec :
Les noms de fuseaux du registre Windows (venant d’Outlook, Exchange, eM Client) sont normalisés vers IANA sur chaque chemin d’ingestion, et les VEVENT exportés embarquent le VTIMEZONE correspondant pour que les clients stricts calculent des heures locales correctes. Un nom Windows couvre plusieurs zones IANA : il se résout vers la zone que le CLDR donne pour le pays d’établissement déclaré de l’instance (W. Europe Standard Time vaut Europe/Zurich sur une box suisse, Europe/Berlin sans pays déclaré, le défaut mondial). Le pays est lu au démarrage puis toutes les dix minutes : un changement ne demande pas de redémarrage. Une entrée de [legacy.windows_tz_overrides] l’emporte toujours. Sur un hôte dont le fuseau est UTC ou inconnu, l’assistant propose le fuseau du pays d’établissement quand ce pays n’a qu’un seul décalage.

Notifications

Les changements d’agenda produisent des objets CalendarEventNotification pour les personnes concernées : l’écriture d’un bénéficiaire sur un agenda partagé notifie le propriétaire, et un message de planification entrant notifie l’utilisateur concerné (REQUEST/CANCEL vers le destinataire, REPLY vers l’organisateur ; les changements de son propre fait sont tus). Les notifications sont inscrites au journal de changements et poussées : les clients les voient sans interroger. Les alertes (rappels) acceptent des déclencheurs relatifs et absolus, et sonnent une fois par occurrence grâce à une table de déduplication. « Plus tard » sur une alerte qui a sonné est un verbe du serveur, Event/snoozeAlert ou Task/snoozeAlert : l’alerte sonne à nouveau une fois à l’heure choisie, au plus 7 jours plus tard (alertes). Un texte push est rédigé dans la langue du lecteur, avec le format de date du pays de l’organisation (alertes).

Tâches

Les listes de tâches suivent le modèle des agendas : méthodes JMAP, passerelle CalDAV VTODO, listes partagées. Chaque compte a exactement une liste de tâches par défaut, garantie par un invariant en base : un Task/set create sans taskListIds y atterrit. Les alertes de tâches acceptent un déclencheur absolu sans exiger d’échéance. Une tâche porte son timeZone (RFC 8984) : un DUE:…Z écrit par CalDAV s’ancre en UTC au lieu de flotter, un TZID est normalisé en IANA, et le planificateur d’alertes honore l’ancre de la tâche avant la chaîne principal → organisation, si bien qu’un rappel sonne à l’instant de la tâche et non à l’horloge murale du principal ; une ligne d’avant la colonne reste flottante, son sens historique. Le partage d’une liste passe par le même calcul de droits que les boîtes et les agendas (TaskList/get filtre ce qu’un lecteur d’un autre compte peut lire et calcule son myRights, Task/get|query sont bornés aux listes lisibles, Task/set consulte les droits par liste, les partages et les retraits notifient le bénéficiaire) ; une seconde copie de la règle est un défaut, donc il n’y en a pas (tâches).

Contacts

Les contacts sont des ContactCards JMAP (RFC 9610) avec une passerelle CardDAV. Le serveur possède l’uid (un uid fourni par le client à la création est rejeté), et un PUT CardDAV qui créerait un doublon inter-sources d’un UID existant est refusé plutôt que dupliqué en silence. Les composantes de nom et d’adresse font l’aller-retour complet entre vCard et la forme JMAP. Une propriété que le serveur ne connaît pas est refusée nommément à la création comme à la modification, jamais gardée en silence : une fiche créée avec un prénom et un nom séparés, ce qu’un logiciel venu d’ailleurs envoie naturellement, répondait succès et existait sans nom. Une addressbook-query ne se compte plus elle-même dans ses résultats, si bien qu’une recherche vide a l’air vide. Un caractère de contrôle dans une adresse, un nom ou un numéro de téléphone est refusé à chaque porte, nommé par son chemin, là où le retour chariot égaré d’un script d’alimentation était stocké puis retrouvé par aucune comparaison d’adresse. Une fusion de doublons envoie les fiches absorbées à la corbeille avec l’identifiant de leur cible (mergedInto) au lieu de les détruire, si bien qu’une fusion erronée se défait en restaurant la fiche, dans l’horizon de la corbeille des contacts (contacts). L’autocomplétion d’adresse sur une fiche contact passe par le serveur (GET /geocode, une route authentifiée) : le navigateur n’appelle jamais lui-même le service de géocodage, et l’instance lui parle d’une seule voix, avec son propre User-Agent et, par défaut, une requête par seconde. La question va d’abord au pays de l’organisation (son pays d’établissement déclaré, sinon celui de l’instance ; aucun pays déclaré, c’est le monde entier), dans la langue du navigateur (la première étiquette d’Accept-Language, transmise seulement si elle a la forme d’une étiquette de langue). Une recherche qui ne trouve rien dans ce pays est reposée au monde entier : un contact à l’étranger reste trouvable. Un client peut nommer lui-même le pays avec country (un code ISO 3166-1 alpha-2 attribué, ou any ; toute autre valeur donne 400). Le texte saisi atteint toujours le service de géocodage, mais il n’est plus rattaché à une personne. [geocode] enabled = false retire la route (404), et le client garde la saisie libre.

Les corbeilles

Les événements, les tâches et les contacts ont chacun une corbeille en deux temps (ADR-035), et depuis #1471 les trois passent par la sortie du Drive au lieu de 30 jours en dur détruits en silence : un réglage par domaine, trash_retention_days dans [calendar], [contacts] et [tasks] (30 par défaut, plancher 1 ; la même clé existe dans [drive] et [notes]), un compte dont un message est sous séquestre garde ses objets, chaque destruction est une seule transaction en cascade et s’annonce à /changes comme un Destroyed sur la collection du domaine, si bien que la corbeille d’un client synchronisé ne garde jamais ce que le serveur a retiré (stockage). Les salons de chat ont leur propre corbeille, avec la même clé dans [chat] (salons de chat).

Horizons par locataire

Les six valeurs du fichier valent pour toute l’instance. Chaque locataire peut surcharger chacune d’elles par une surcharge d’exécution dont la clé est le chemin TOML : drive.trash_retention_days, calendar.trash_retention_days, contacts.trash_retention_days, tasks.trash_retention_days, notes.trash_retention_days, chat.trash_retention_days. Une valeur que le serveur ne peut pas utiliser garde les données : une destruction ne se rattrape pas, un séjour plus long si. La surcharge s’écrit par l’API d’administration, PUT /admin/v1/tenants/{id}/config (ou PUT /admin/v1/config pour le locataire par défaut), qui refuse tout ce qui n’est pas un entier de 0 à 36500. Ou par la CLI, simulation d’abord, puis --apply :
La CLI montre le type de la valeur dans la simulation et applique les règles de l’API d’administration avant la simulation comme avant l’écriture : une valeur que l’API refuse (hors plage, négative, non entière) est refusée avec les mêmes détails, code de sortie 2, rien d’écrit. oximail config clear-override retire la surcharge, et le locataire revient à la valeur de l’instance. Le balayage des corbeilles parcourt les locataires qui ont des comptes et applique à chacun ses six horizons. Il tourne au démarrage puis une fois par heure, et relit les surcharges à chaque passage : une surcharge ne demande pas de redémarrage et prend effet dans l’heure, une modification du fichier demande un redémarrage. oximail drive purge-trash et oximail chat purge-trash appliquent la même politique quand --days n’est pas donné, locataire par locataire. Avec --days, ce seul horizon vaut pour toute la portée, y compris dans un locataire dont la surcharge vaut 0. Dans la sortie --json, horizonSource dit lequel s’applique (flag ou policy), et sous une politique tenantHorizons donne l’horizon de chaque locataire en jours, null voulant dire pas de purge. horizonDays reste la valeur de --days, ou la valeur de l’instance quand la politique s’est appliquée. Le courrier fonctionne à l’inverse. Sa politique de corbeille, retention.trash_days, ne purge rien tant qu’elle est absente : un locataire doit la poser (stockage). Les six corbeilles ci-dessus purgent à la valeur de l’instance tant que leur surcharge est absente. Pour les sept, 0 veut dire pas de purge.

Partage

Agendas, listes de tâches, carnets d’adresses et dossiers du drive partagent un seul modèle de droits (cartes shareWith indexées par principal, formes RFC 9670). Deux garanties utiles à l’exploitant :
  • L’état par lecteur reste par lecteur. Un bénéficiaire qui masque un agenda partagé (isVisible) ou met en favori un fichier partagé n’affecte que sa propre vue, jamais l’objet du propriétaire.
  • Les bénéficiaires doivent être adossés à un humain, ou être un groupe. Un droit accordé à une identité technique non connectable est rejeté à l’écriture sur tous les domaines (un groupe est l’exception : il résout vers ses membres) : un droit qui ne pourrait jamais s’exercer est un mensonge dans l’ACL. La garde du drive des comptes partagés a sa propre surface : oximail account custodian (voir multi-tenant).

Réparations en un coup

Les artefacts d’époque migration ont des commandes de réparation dédiées, en simulation par défaut : Les deux enregistrent des changements JMAP pour que les clients se resynchronisent proprement. Voir la référence CLI.