Skip to main content
OxiMail filtre le courrier côté serveur avec des règles : des objets de filtre typés (une condition, puis des actions) gérés en JMAP. C’est ce qu’édite un écran de réglages du webmail : aucune syntaxe de script, un client peut les lire et les ordonner, et le serveur dit dans sa capability quels champs, opérateurs et actions il exécute. Les règles s’exécutent à la remise finale. Cela inclut le courrier qu’un collègue vous envoie depuis le même serveur : le raccourci de remise locale applique les règles du destinataire et sa réponse d’absence exactement comme pour un message reçu de l’extérieur (voir la couche SMTP et l’ADR-117). Le compte n’a aucune surface Sieve à côté de ses règles : ni méthodes JMAP SieveScript, ni écoute ManageSieve. La capability des règles l’annonce par sieveCoexistence: "none".

Les règles

Une règle associe un arbre de conditions (expéditeur, destinataires, objet, n’importe quel en-tête, taille, pièces jointes, mots-clés, verdict anti-spam…) à des actions (déplacer, supprimer, mots-clés, transférer, modifier un en-tête, notifier, répondre automatiquement). En exploitation, ces propriétés comptent :
  • Un ordre total et déterministe. Les règles s’évaluent dans un ordre défini, sans égalité, donc le même message suit toujours le même chemin. L’ordre fait partie des données, ce n’est pas un hasard du stockage.
  • Une règle désigne un dossier par identifiant ou par rôle. move prend un mailboxId, ou un role parmi inbox, archive, junk et important, résolu au moment où la règle s’exécute : elle survit au renommage du dossier et aux noms traduits. delete range le message dans la corbeille. Une règle ne peut pas désigner un dossier qui n’existe pas : Rule/set la refuse.
  • Appliquer au courrier existant. Rule/apply exécute une règle sur les messages qu’un client désigne par leur identifiant (en général les identifiants rendus par un Email/query précédent), au plus maxApplyEmailsPerCall par appel (1000 par défaut, un réglage de l’instance, voir plus bas). Désigner les messages permet au client de paginer avec Email/query sans jamais sauter un message que la règle vient de sortir d’un dossier. La réponse compte les messages évalués, retenus, modifiés et en échec, et liste modifiedIds ainsi que les identifiants illisibles (notFound). Un message déjà dans l’état que la règle demande (libellé déjà posé, déjà mis de côté) n’est pas modifié et n’est pas compté. Les actions de classement de la règle sont résolues ensemble ; les transferts, notifications et modifications d’en-tête ne s’exécutent jamais sur du courrier déjà remis (ils sont listés dans skippedActions), et dryRun: true évalue et compte sans rien modifier. Un argument inconnu est refusé en invalidArguments plutôt qu’ignoré : sur une méthode qui modifie du courrier en masse, un dryRun ignoré sans le dire n’est pas acceptable. Chaque appel qui n’est pas une simulation écrit une ligne rule/applied dans le journal d’administration du compte, dans la transaction qui enregistre l’exécution sur la règle : la règle et les nombres de messages évalués, retenus, modifiés, en échec et introuvables, jamais un identifiant d’Email ni le nom de la règle. Mille messages mis à la corbeille en un appel laissent une trace.
  • Tester une règle. Rule/test évalue une règle enregistrée, une règle en cours d’édition (validée exactement comme Rule/set le ferait) ou l’ensemble des règles actives dans l’ordre d’évaluation, sur des messages déjà remis, sans exécuter aucune action. Chaque résultat nomme les champs dont la donnée manquait (unavailable), pour qu’un client puisse dire pourquoi une règle n’a pas correspondu. isDuplicate est évalué sur le cache des doublons de la remise, lu et jamais écrit, et un message déjà remis n’est pas son propre doublon : le cache garde, par identifiant, ses huit dernières remises et l’Email que chacune est devenue. L’enveloppe SMTP et le verdict anti-spam sont des données de remise que ce serveur ne garde pas avec le message : dans Rule/test, Rule/apply et la simulation en CLI, ils sont indisponibles (retainsDeliveryData: false).
  • Quand une règle échoue. Une règle échoue quand elle dépasse son temps d’évaluation (un motif pathologique, par exemple), quand elle porte une action que ce serveur n’exécute plus, ou quand un transfert ne peut pas être remis à la file. La règle l’enregistre (lastExecutionFailed, lastErrorReason dans Rule/get), les règles suivantes ne s’exécutent pas, aucune réponse automatique ne part, et le message est remis dans la boîte de réception en plus des dossiers où l’ont rangé les règles précédentes, dont les actions restent acquises. Un filtre ne perd jamais de courrier.
  • Transférer. La copie transférée porte les modifications d’en-tête faites avant elle et un marqueur que seul ce serveur sait signer pour le compte (X-OxiMail-Forwarded-By) : un message que ce compte a déjà transféré ne l’est pas une seconde fois, et un marqueur inventé par un expéditeur est ignoré. La copie part avec un expéditeur d’enveloppe auquel ce serveur reçoit les rebonds (l’adresse SRS, ou le compte lui-même), jamais l’expéditeur d’origine tel quel. Un message classé indésirable n’est jamais transféré. Les règles d’un compte nomment au plus maxForwardDestinations (10) destinations distinctes, et une remise envoie au plus maxEmissionsPerDelivery (5) messages. Les destinations acceptées relèvent de la forwardPolicy de l’exploitant :
    "any" (par défaut) accepte toute adresse, "internal" seulement les domaines dont ce serveur reçoit le courrier, "verified" seulement une adresse dont le titulaire a confirmé (plus bas), "none" aucune nouvelle destination. Rule/set refuse une destination hors politique en forbidden ; un transfert enregistré qu’une politique resserrée n’accepte plus cesse de partir, sans toucher aux règles de personne. Son titulaire en est averti une fois, à ce moment, par une notification push (« Forwarding stopped », qui nomme la règle) : rien dans la boîte ne montre une copie qui ne part plus.
  • Une suppression est enregistrée, ce n’est pas un échec. Un transfert, une notification ou une réponse qu’une garde, la politique ou une borne arrête ne fait rien ; les autres actions de la règle s’exécutent, et lastSuppression dit pourquoi (loop, autoSubmitted, nullSender, unauthenticated, deduplicated, policy, bound, junk, list, notAddressed, rateLimited). Elle revient à null au prochain envoi.
  • Un dossier ou un libellé qui disparaît. Quand un dossier où une règle range est détruit (par JMAP, IMAP ou la CLI), ou le dernier dossier portant un rôle qu’une règle nomme, ou un libellé qu’une règle pose, la règle enregistre mailboxNotFound, roleNotFound ou labelNotFound dans la même transaction, et cette action ne fait plus rien jusqu’à la réécriture des actions de la règle.
  • Notifications. notify envoie un court message mailto: (ses champs to et cc ajoutent des destinataires), avec Auto-Submitted: auto-notified, un jeton du titulaire et un expéditeur d’enveloppe vide. Elle n’est pas envoyée à propos d’un message lui-même automatique ; un rebond, lui, notifie. Les règles notify d’un compte atteignent au plus 30 destinataires par heure (une fenêtre glissante, l’exemple de la RFC 5436 §5), annoncé par maxNotificationsPerHour dans la capability des règles : au-delà, la notification est supprimée avec lastSuppression: "rateLimited" et le message est remis comme d’habitude. Une rafale de courrier correspondant décidait seule de ce que le serveur envoyait à l’adresse de la règle.
  • La télémétrie est un changement. matchCount, lastMatchAt et lastExecutedAt sont écrits par lots (environ une fois par minute), l’issue d’une évaluation tout de suite et seulement quand elle diffère de ce que la règle porte déjà. Chaque écriture fait avancer l’état des règles ; Rule/changes la signale avec updatedProperties, qui liste les propriétés de télémétrie quand rien d’autre n’a changé, pour qu’un client ne relise qu’elles. Un échec enregistré sur une règle reste jusqu’à la réécriture de sa condition ou de ses actions ; la renommer ou la désactiver ne l’efface pas.
La borne de Rule/apply est la taille d’une requête : chaque message qu’elle désigne est lu, déchiffré et évalué dans cet unique appel HTTP, sur une machine que toutes les organisations de la boîte partagent. C’est donc un réglage de l’instance, annoncé par maxApplyEmailsPerCall, de 1 à 10 000 (1000 par défaut) ; toute autre valeur fait refuser la configuration au lieu de revenir à la valeur par défaut sans le dire :

Transférer seulement vers une adresse confirmée

Avec forward_policy = "verified", une règle ne transfère que vers une adresse dont le titulaire a confirmé, pour ce compte, qu’il accepte les copies (ADR-162). Ce réglage protège le destinataire, et la réputation d’envoi de la boîte : personne ne reçoit le courrier d’un autre parce que quelqu’un a saisi son adresse. Il ne protège pas d’un compte volé, dont le voleur confirme une adresse qu’il détient aussi facilement que le titulaire ; c’est le rôle de la garde décrite plus bas.
Ne passez une boîte en "verified" qu’une fois que le client qu’elle sert propose les écrans de confirmation. Le réglage s’applique tout de suite aux transferts enregistrés : chaque destination pas encore confirmée cesse de recevoir les copies, et sa règle enregistre lastSuppression: "policy".
Comment une destination est confirmée :
  1. Le compte la demande : ForwardConfirmation/set avec create: {address}. Le serveur envoie à l’adresse un code de huit caractères affiché sous la forme XXXX-XXXX, valable sept jours, et enregistre une confirmation pending (id, address, state, createdAt, codeExpiresAt, confirmedAt, expiresAt).
  2. Le titulaire de l’adresse lit le message et, s’il accepte, rend le code au compte. Répondre au message suffit : son Reply-To nomme le compte demandeur, et la partie HTML porte un bouton « Envoyer le code à … », un mailto: vers la même adresse avec le code en corps, seul lien du message. Le serveur ne lit aucune réponse.
  3. Le compte saisit le code : ForwardConfirmation/confirm {id, code} répond confirmed, ou reason (wrongCode avec attemptsLeft, attemptsExhausted, expired, notFound, malformed). Le code se lit à la manière de Crockford : la casse, les tirets et les espaces ne comptent pas, O vaut 0, I et L valent 1, et une saisie qui ne peut pas être un code ne consomme aucun essai.
ForwardConfirmation/get, /set, /changes et /confirm relèvent de la capability des règles. Elles ne répondent que si la politique du serveur est "verified" (sinon forbidden, qui nomme la politique), et seulement au compte lui-même, jamais en accès partagé ni à un invité de réunion : le code part au nom du compte. Il n’y a pas de mise à jour : une confirmation est demandée, confirmée ou retirée (destroy), et la retirer arrête les règles qui nomment l’adresse. Les bornes qui rendent un code court sûr :
  • Le code n’existe en clair que dans le message. Le serveur garde un HMAC lié à la demande et à l’adresse ; il ne rend jamais le code au compte et ne l’écrit jamais dans un journal. Un code qui ne peut pas être mis en file annule la demande.
  • Cinq essais par compte et par adresse sur sept jours, consommés dans la transaction de la comparaison, et comptés dans une trace qui survit à un retrait et à une nouvelle demande.
  • Un code par compte et par adresse tous les sept jours, dix codes par compte et par jour, trois codes par adresse et par jour sur toute l’instance. La trace par adresse ne garde qu’un HMAC de l’adresse.
  • Une demande restée sans réponse est effacée. Toutes les dix minutes, un traitement supprime les demandes en attente dont le code a expiré, inscrit la destruction dans les /changes du compte et la pousse : l’adresse du tiers ne reste pas en clair dans un compte qui l’a oubliée. Une nouvelle demande remplace une demande expirée au lieu d’être refusée comme doublon.
Le message vient de postmaster@ du domaine du compte, avec un expéditeur d’enveloppe vide, marqué Auto-Submitted. Son texte est fixe : seule l’adresse du compte demandeur varie, jamais un nom affiché ni un texte écrit par l’utilisateur. Il dit ce que fait le code, que personne ne le demandera pour se connecter, d’où vient l’adresse et ce qu’elle devient, que les journaux du serveur en gardent une trace, ce que le destinataire peut demander, l’échéance dans le fuseau de l’organisation, et à qui écrire. Il nomme le responsable que l’organisation a déclaré, les pays où l’adresse est traitée et, quand quelqu’un d’autre exploite le serveur, cet exploitant ; sans rien de déclaré, « l’exploitant du domaine » et postmaster@ du domaine du compte. Il est rédigé dans la langue de l’organisation, puis en anglais (en français puis en anglais si aucune langue n’est réglée), et l’exploitant peut y ajouter une variante nationale (textes envoyés à des tiers). Le titulaire d’une adresse n’a pas de compte ici : l’exploitant lui répond depuis la console.
list trouve chaque confirmation de l’adresse dans toutes les organisations et affiche l’empreinte que portent les lignes d’audit ; revoke les supprime (les règles qui nomment l’adresse cessent de transférer) et bloque l’adresse pour de bon : aucun code ne lui sera plus jamais envoyé. Les deux lisent la base directement, serveur démarré ou non ; revoke est une simulation sans --apply.

Un transfert est gardé comme un identifiant

Une règle de transfert survit à un mot de passe volé : qui la pose continue de recevoir le courrier après le changement du mot de passe. La poser est donc traité comme un changement d’identifiant (ADR-163) :
  • Une preuve récente par passkey. Un Rule/set après lequel une règle active transfère vers une nouvelle adresse (une création, des actions remplacées, une règle réactivée) répond stepUpRequired quand le compte a une passkey que cette session n’a pas prouvée dans les dix dernières minutes. Un compte sans passkey passe. Supprimer, désactiver ou renommer une règle ne demande rien.
  • Jamais depuis un mot de passe d’application. La même écriture est refusée en forbidden pour cette règle quand la requête passe par un mot de passe d’application.
  • Un mot de passe réinitialisé par un tiers suspend les transferts. Un mot de passe posé par un administrateur (Principal/setPasswordAdmin, oximail account set-password, la route REST d’administration) désactive, dans sa transaction, chaque règle active qui transfère et marque en échec chaque copie transférée encore en file ; la réponse, la sortie de la CLI et la ligne d’audit account.password_set donnent les deux nombres (forwardsSuspended, forwardCopiesCancelled). Une personne qui change son propre mot de passe ne suspend rien.
  • Une destination retirée emporte ses copies en file. Quand plus aucune règle active ne transfère vers une adresse (une règle détruite, désactivée ou réécrite), ou que sa confirmation est retirée ou révoquée, les copies encore en attente pour elle sont marquées en échec, la raison inscrite sur l’entrée de la file.
  • Chaque transfert posé ou retiré est une ligne d’audit (rule/forward-added, rule/forward-removed) dans la chaîne du compte, avec une empreinte HMAC de l’adresse, jamais l’adresse.

Les scripts Sieve déjà présents

Une boîte migrée depuis un autre serveur, ou un compte OxiMail antérieur au passage aux seules règles, peut contenir un script Sieve. À chaque démarrage, le serveur importe en règles chaque script actif qu’il n’a pas encore vu :
  • chaque branche du script devient une règle portant le nom du script, dans l’ordre du script, avec stopAfter: true ;
  • une construction sans équivalent en règle est laissée de côté, et l’import est journalisé comme partiel. discard, par exemple, devient delete, qui range le message dans la corbeille au lieu de le détruire ;
  • un script qui ne fait rien (celui que les anciennes versions posaient sur chaque compte) ne donne aucune règle ;
  • les règles importées apparaissent dans Rule/changes, donc un client connecté les voit.
Le texte du script reste en base. L’import a lieu une seule fois par script.

Le script global de l’exploitant

[sieve] global_script désigne un script Sieve facultatif, commun au serveur, qui s’exécute avant les règles du compte. S’il se termine par une action finale (redirect, discard, reject avec stop), les règles ne s’exécutent pas. C’est la place d’une politique de l’organisation que les utilisateurs ne doivent pas pouvoir contourner.
L’interpréteur prend en charge exactement ces extensions :
Un require de toute autre extension fait échouer la compilation en le disant, et nomme ce qui est disponible (RFC 5228 §3.2 : un script qui exige une extension absente ne doit pas s’exécuter). Un script peut aussi exiger un comparateur : comparator-i;ascii-casemap, comparator-i;ascii-numeric et comparator-i;octet sont les trois que l’analyseur accepte et que l’exécution implémente. Un test header à plusieurs noms d’en-tête (["From", "Sender"]) correspond à n’importe lequel ; la liste n’est jamais réduite à son premier élément. L’analyseur est soumis au fuzzing : un suffixe de taille qui déborde (size :over 17179869184G) est refusé en number too large au lieu de devenir en silence :over 0 et de correspondre à tous les messages.

Réponses automatiques

Un message reçoit au plus une réponse automatique : celle d’une règle replyWith qui correspond d’abord, sinon la VacationResponse JMAP du compte, dans ses dates. Toute réponse automatique suit la RFC 3834 :
  • elle part vers l’expéditeur d’enveloppe (le Return-Path), avec Auto-Submitted: auto-replied et un expéditeur d’enveloppe vide ;
  • elle n’est pas envoyée à un expéditeur vide, à un message lui-même automatique, à une liste ou un envoi en nombre, ni à un message classé indésirable ;
  • elle ne répond qu’à un expéditeur dont ce serveur a authentifié le domaine (un SPF valide sur ce domaine, ou une signature DKIM de ce domaine ou de son domaine organisationnel), et seulement à un message adressé à l’une des adresses du compte. Le domaine organisationnel est celui que DMARC trouve par sa descente DNS (RFC 9989), et non plus par la liste des suffixes publics embarquée : un suffixe qui publie psd=y ne permet plus à l’un de ses clients d’en authentifier un autre, et un sous-domaine sans enregistrement DMARC n’a pas de domaine organisationnel au-dessus de lui. La descente n’a lieu que si SPF n’a pas réussi et que la signature porte sur un autre nom ; un échec DNS n’envoie rien ;
  • chaque réponse (chaque règle replyWith, et l’absence) part au plus une fois par expéditeur tous les 7 jours ; l’enregistrement est écrit avant l’envoi, et une réponse qui ne peut pas être enregistrée ne part pas.
Les réponses automatiques sont supprimées pour un compte désactivé (le courrier est toujours remis), ne partent pas après l’échec d’une règle, et répondent aussi aux expéditeurs locaux.

Où regarder quand un filtre se comporte mal

  • Les verbes CLI oximail rule … listent, affichent, écrivent, suppriment et simulent les règles d’un compte ; voir la référence CLI.
  • Les décisions de remise sont journalisées avec le compte et la règle en cause ; voir Exploitation pour la marche à suivre avec journald.