oximail-smtp est la couche SMTP. Il possède le protocole de transport des deux côtés de la frontière du courrier : recevoir le courrier du monde extérieur sur le port 25, accepter le courrier authentifié du client d’un utilisateur sur le port 587, remettre le courrier en file vers les serveurs distants, générer des avis de non-remise quand la remise échoue, et fonctionner comme MX de secours pour un serveur principal indisponible. C’est le seul crate qui parle le SMTP brut. Tout ce qu’il accepte converge vers l’unique chemin d’ingestion de oximail-mail, et tout ce qu’il envoie sort de la file de remise persistante.
Vue d’ensemble
Un message atteint ce crate de deux manières, et ces deux manières sont des chemins de code volontairement distincts. Un serveur externe qui se connecte au port 25 pour remettre du courrier à un destinataire local exécute process_inbound_data. Le client d’un utilisateur authentifié qui se connecte au port 587 pour envoyer du courrier exécute process_submission_data. Ce ne sont pas deux branches d’une même fonction avec un drapeau : ce sont deux méthodes distinctes avec des règles distinctes, parce que le port 25 (entrant) et le port 587 (soumission) sont des protocoles fondamentalement différents. Le courrier entrant n’est pas de confiance et doit être authentifié, scoré et scellé avant la remise. Le courrier de soumission est déjà authentifié et doit au contraire être validé contre les identités de l’utilisateur expéditeur avant d’être mis en file vers l’extérieur.
Le crate se situe au-dessus de oximail-store et oximail-mail, et dépend du crate maison oximail-auth pour chaque primitive d’authentification du courrier (DKIM, SPF, DMARC, ARC, MTA-STS, DANE, SRS). Le volet du spam lié à l’analyse de contenu est orchestré ici, mais vit dans le crate oximail-spam : ce crate décide vers où le verdict envoie le message, pas comment le message est scoré.
Chaque message accepté respecte l’ADR-017 : un courrier qui a reçu un 250 OK du SMTP est toujours remis quelque part (la boîte de réception du destinataire, le dossier Indésirables ou la quarantaine) et jamais abandonné en silence.
Fonctionnalités
- Deux chemins de code séparés :
process_inbound_data (port 25) et process_submission_data (port 587), routés par process_data selon que la session est une soumission authentifiée. L’entrant vérifie et score ; la soumission valide l’identité de l’expéditeur et met en file.
- Pipeline d’authentification et anti-spam entrant : vérification DKIM / SPF / DMARC / ARC (via
oximail-auth), scellement ARC de la chaîne, un en-tête Authentication-Results (RFC 8601), puis le pipeline anti-spam orchestré depuis le scoreur oximail-spam, puis la remise par le chemin d’ingestion.
- Validation de la soumission : le
MAIL FROM d’enveloppe de l’utilisateur authentifié et l’en-tête From: du corps du message sont tous deux vérifiés contre les identités du compte (son adresse propre plus les alias JMAP). Toute divergence est refusée. Le serveur ne relaie pas un expéditeur falsifié, même pour un utilisateur authentifié.
- Réponses de rejet avec une URL d’aide postmaster : chaque rejet synchrone et chaque avis de non-remise asynchrone porte une URL stable
https://oximail.ch/postmaster?p=<Reason> pour que le postmaster expéditeur puisse diagnostiquer lui-même.
- File de remise persistante : une file sortante adossée à SQLite, avec réessais à backoff exponentiel et une fenêtre d’abandon RFC 5321, interrogée par un worker en arrière-plan qui résout le MX, signe en DKIM au moment de la remise et applique la politique de transport (MTA-STS / DANE).
- Génération de DSN (RFC 3464) : un avis de non-remise structuré vers l’expéditeur d’origine en cas d’échec permanent, d’échec de remise partielle ou d’expiration en file.
- Chemin MX de secours (ADR-052) : mode file (stocker-et-relayer, par défaut) ou mode proxy synchrone.
- SMTP AUTH : SASL
PLAIN et LOGIN sur le port de soumission, avec suivi par session des tentatives en force brute.
- Extensions de transport : découpage
BDAT (RFC 3030), REQUIRETLS (RFC 8689) et réécriture d’enveloppe SRS lors du transfert.
Deux chemins, un point de routage
Chaque connexion TCP reçoit un SmtpSession qui suit la machine à états du protocole (Connected puis Greeted puis MailFrom puis RcptTo puis Data / Bdat) et indique si la connexion est arrivée sur un port de soumission. Quand DATA se termine, process_data prend l’unique décision qui sépare les deux mondes :
si session.is_submission && session.authenticated.is_some()
→ process_submission_data (port 587)
sinon
→ process_inbound_data (port 25)
Les deux handlers ne partagent rien d’autre que les octets bruts. C’est une règle de conception ferme, pas un détail d’implémentation : une soumission authentifiée ne doit jamais tomber par accident dans le pipeline anti-spam entrant, et le courrier entrant non fiable ne doit jamais atteindre la file sortante.
Le chemin entrant
process_inbound_data exécute, dans l’ordre :
- Contrôle de confiance (ADR-031). La boucle locale est toujours de confiance ;
[security] trusted_ips étend l’ensemble. Un pair de confiance contourne SPF / DNSBL / greylist / rejet de politique DMARC, mais l’analyse de contenu reste active, car une application locale compromise peut quand même spammer.
- Authentification.
oximail-auth vérifie SPF, DKIM, DMARC et la chaîne ARC. Le crate scelle ensuite la chaîne ARC (RFC 8617) et ajoute en tête un en-tête Authentication-Results (RFC 8601) pour que les consommateurs en aval voient le verdict sans relancer la vérification.
- Pipeline anti-spam. Le pipeline scoré (orchestré ici, scoré dans
oximail-spam) renvoie une décision. Reject produit une réponse de rejet 550 ; Defer un 451 ; les verdicts remis (Junk / suspect / en masse / propre) routent vers le placement de dossier et un tag X-Spam-Status via une unique fonction de disposition (ADR-080). Seul Junk est classé dans le dossier Indésirables ; les messages suspects et en masse sont remis dans la boîte de réception avec un tag et ne sont jamais marqués indésirables.
- Remise. Chaque destinataire est remis par le chemin d’ingestion. Si l’ingestion échoue après l’acceptation du message, l’ADR-017 prend le relais : le message est tout de même remis (avec le mot-clé
$junk) plutôt qu’abandonné. Si tous les destinataires échouent, la réponse est un 451 pour que l’expéditeur réessaie ; un échec partiel génère un avis de non-remise pour les destinataires en échec.
Le scoring de contenu du spam n’est pas implémenté dans ce crate. oximail-smtp construit le FilterContext, appelle le scoreur et agit sur la SpamDecision renvoyée : dossier, tag, rejet ou report. Le modèle de scoring lui-même (heuristiques, réputation, bayésien, analyse de contenu) vit dans oximail-spam. Cette page documente l’orchestration et les conséquences sur la remise, pas le scoring.
Le chemin de soumission
process_submission_data exige une session authentifiée et ne fait aucune vérification d’authentification, aucun scoring anti-spam, aucun ARC et aucun Authentication-Results. Ce travail concerne le courrier entrant, pas le courrier que l’utilisateur envoie. Il impose à la place la légitimité de l’expéditeur deux fois :
- Le
MAIL FROM d’enveloppe doit correspondre à l’adresse du compte authentifié ou à l’une de ses identités JMAP (alias, sous-adresses). En cas d’erreur de recherche, le contrôle échoue en mode fermé (ADR-027) : un incident transitoire de base de données rejette plutôt que de relayer.
- L’en-tête
From: du corps du message est vérifié contre le même ensemble d’identités. Un utilisateur authentifié qui passe le contrôle d’enveloppe peut encore falsifier un From: de corps ; ce second contrôle le refuse.
Une soumission qui passe est stockée sous forme de blob chiffré par tenant (ADR-020 : si la clé du tenant est indisponible, la soumission est rejetée avec un 451, jamais stockée en clair) puis mise en file pour la remise sortante. La signature DKIM intervient plus tard, dans le worker sortant, au moment de la remise.
Réponses de rejet et convention postmaster
Chaque rejet et chaque avis de non-remise porte une URL d’aide stable, https://oximail.ch/postmaster?p=<Reason>, sur le modèle du standard de fait du secteur (le support.google.com/mail/?p=<Reason> de Google). La valeur ?p= est un slug par motif, pas une référence par message. L’URL est donc stable et cachable, et correspond à une ancre nommée sur la page publique /postmaster. postmaster.rs est l’unique source de vérité pour les slugs ; la couche SMTP et la page d’aide la partagent.
Le code de statut étendu est choisi selon la raison du refus, pas par commodité :
| Motif | Réponse | Statut étendu | Standard |
|---|
DMARC p=reject (alignement SPF + DKIM en échec) | 550 | 5.7.26 | RFC 7372 §3.3 |
| Refusé par le pipeline de contenu anti-spam | 550 | 5.7.1 | RFC 5321 |
| La boîte du destinataire n’existe pas | 550 | 5.1.1 | RFC 5321 |
| Le MX de secours ne relaie pas pour ce domaine | 550 | 5.1.1 | RFC 5321 |
| Échec de remise permanent (retourné à l’expéditeur) | DSN | | RFC 3464 |
Le rejet DMARC est honnête sur ce qu’il est : 5.7.26 (« plusieurs contrôles d’authentification ont échoué », RFC 7372) est une décision d’authentification, pas un score de contenu. C’est le code que Gmail et Microsoft renvoient pour un rejet DMARC. Le code précédent citait un motif de contenu arbitraire sous un 5.7.1, produisant un avis contradictoire. Les trois sites de rejet DMARC (pipeline scoré, MX de secours, repli sans pipeline) passent désormais par une unique fonction partagée smtp_dmarc_reject_reply pour rester synchronisés.
Remise sortante
Un message soumis ou relayé atterrit dans la file de remise SQLite. Un worker en arrière-plan interroge les entrées arrivées à échéance et remet chacune : il résout le MX de destination, applique la politique de transport (MTA-STS, RFC 8461 ; DANE, RFC 7672), signe le message en DKIM au moment de la remise et l’envoie sur une connexion TLS opportuniste ou imposée.
Un échec temporaire planifie un réessai à backoff exponentiel (1 min puis 5 min puis 30 min puis 2 h puis 8 h puis 24 h). Un message qui ne peut pas être remis avant l’expiration de la fenêtre d’abandon (5 jours par défaut, selon la recommandation de la RFC 5321 §4.5.4.1, le même défaut que Postfix) génère un avis de non-remise RFC 3464 vers l’expéditeur d’origine et quitte la file.
Le chemin MX de secours
Quand le serveur tourne avec [mode] role = "backup", le handler entrant branche au RCPT TO et au DATA vers l’un des deux modes (ADR-052) :
- Mode file (par défaut) : stocker-et-relayer. Le secours accepte le courrier avec un
250 OK, puis un worker de réessai le relaie vers le principal en arrière-plan. Si le principal refuse avec un 5xx, un avis de non-remise part vers l’expéditeur d’origine. Conforme aux RFC, mais cela ouvre une fenêtre entre l’acceptation par l’expéditeur et l’avis, pendant laquelle un expéditeur falsifié peut considérer son spam comme remis.
- Mode proxy (opt-in, ADR-052) : le secours relaie la session SMTP vers le principal en temps réel. MAIL FROM, RCPT TO, DATA, corps et réponses transitent tous de manière synchrone, si bien qu’un
5xx du principal est propagé à l’expéditeur comme un 5xx, sans backscatter ni avis différé. Si le principal est injoignable au début de la session, le secours bascule de façon transparente vers l’acceptation en file pour ce courrier.
Invariants
- Ne jamais échouer en silence. Un message accepté (
250 OK) est toujours remis quelque part (ADR-017) ; un échec de remise permanent produit toujours un avis de non-remise vers l’expéditeur ; une erreur de recherche d’identité à la soumission échoue en mode fermé, pas ouvert.
- Entrant et soumission sont des chemins séparés. La soumission valide le
MAIL FROM et le From: de corps contre les identités authentifiées ; l’entrant ne valide pas l’identité de l’expéditeur mais authentifie, score et scelle. Aucun chemin ne peut tomber dans l’autre.
- Aucun blob en clair. Une soumission dont la clé de chiffrement du tenant est indisponible est rejetée avec un
451, jamais stockée en clair (ADR-020).
- Une réponse par motif. Chaque motif de rejet correspond à un code de statut étendu et à un slug d’aide postmaster, définis une seule fois dans
postmaster.rs.
Configuration
oximail-smtp ne lit aucun fichier de configuration directement ; le binaire résout les valeurs et construit le serveur. Les réglages côté opérateur qui façonnent la surface SMTP (les adresses et ports d’écoute entrant et soumission, le nom d’hôte utilisé dans Received: et SRS, les limites de débit, les ensembles [security] trusted_ips et [server] trusted_proxies, le bloc [mode] du MX de secours, et la surcharge de durée de vie maximale sortante) sont documentés dans la référence de configuration. Le modèle de chiffrement au repos derrière les blobs en file (le schéma de type LUKS, pas le bout en bout) est traité dans chiffrement au repos.
Écouteurs SMTP
oximail-smtp n’est pas un crate JMAP : il ne sert aucune méthode JMAP ni aucune route HTTP. Sa surface est le protocole SMTP sur ces écouteurs :
| Écouteur | Port | Chemin | Rôle |
|---|
| MX entrant | 25 | process_inbound_data | Recevoir le courrier des serveurs externes pour les destinataires locaux. Vérifie SPF / DKIM / DMARC / ARC, exécute le pipeline anti-spam, remet par l’ingestion. Aucune validation d’identité d’expéditeur. |
| Soumission | 587 | process_submission_data | Accepter le courrier authentifié du client d’un utilisateur. Exige SMTP AUTH ; valide le MAIL FROM et le From: de corps contre les identités du compte ; met en file pour la remise sortante. |
| MX de secours | 25 | branche secours (ADR-052) | Quand role = "backup" : stocker-et-relayer (file) ou proxy synchrone vers le principal. |
Le port de soumission gère SASL PLAIN et LOGIN, STARTTLS, le découpage BDAT (RFC 3030) et REQUIRETLS (RFC 8689). La surface d’envoi côté JMAP (Email/send, EmailSubmission/set) vit dans oximail-mail ; ce crate implémente le contrat SubmissionSender que la file de remise remplit pour lui.
Décisions de conception
- ADR-017 (le courrier accepté doit être remis) : tout message ayant reçu un
250 OK finit dans la boîte de réception, les Indésirables ou la quarantaine ; un échec d’ingestion remet avec $junk plutôt que d’abandonner. Pour l’entrant, l’ADR-017 l’emporte sur le chiffrement (remise avec $junk) ; pour la soumission sortante, le chiffrement l’emporte (rejet 451 avant tout 250 OK).
- ADR-052 (mode proxy du MX de secours) : le MX de secours tourne en mode file (stocker-et-relayer, relais en arrière-plan, avis différé sur
5xx du principal) ou en mode proxy (proxy de session synchrone qui propage la réponse du principal sans backscatter).
- ADR-049 (extension de la couverture MTA-STS) : la politique de sécurité du transport (MTA-STS, DANE) est honorée sur le chemin de relais du MX de secours, pas seulement sur le sortant direct.
- ADR-031 (politique de réseau de confiance) : la boucle locale et
[security] trusted_ips contournent SPF / DNSBL / greylist / rejet DMARC, mais jamais le pipeline de contenu.
- ADR-080 (colonne vertébrale de disposition du spam) : une unique fonction de disposition mappe chaque verdict de spam remis vers le placement de dossier plus le tag
X-Spam-Status, si bien que les messages suspects et en masse sont remis dans la boîte de réception avec un tag et que seul Junk est classé Indésirables.
- ADR-077 (modèle unifié de classification du spam) : un message vers un destinataire spamtrap est capturé comme corpus de spam de référence (remise forcée en
$junk) plutôt que rejeté ou reporté, pour que le piège collecte exactement le spam de masse qu’il existe pour étudier.
- ADR-027 (audit de l’échec silencieux) : les contrôles d’identité de la soumission échouent en mode fermé en cas d’erreur de recherche ; un incident transitoire de base de données rejette l’envoi plutôt que de le relayer.
- ADR-076 (parser et auth restent AGPL, pas de crates.io) : les primitives d’authentification du courrier proviennent du crate maison AGPL
oximail-auth, une dépendance par chemin de l’espace de travail.
Les ADR vivent dans le dépôt du serveur OxiMail ; voir l’index des ADR.
Standards
- RFC 5321 : Simple Mail Transfer Protocol. Le protocole de transport des chemins entrant et soumission, l’en-tête de trace
Received: (§4.4) et la recommandation sur la fenêtre d’abandon (§4.5.4.1).
- RFC 7372 : codes de statut d’authentification du courrier. Le statut étendu
5.7.26 utilisé pour un rejet DMARC p=reject (§3.3).
- RFC 3464 : avis d’état de remise (DSN). L’avis structuré généré en cas d’échec de remise permanent ou partiel et d’expiration en file.
- RFC 8601 : en-tête
Authentication-Results, ajouté en tête du courrier entrant avec le verdict.
- RFC 3030 :
BDAT / CHUNKING pour la phase de données entrante.
- RFC 8689 :
REQUIRETLS, propagé sur les enveloppes d’avis de non-remise et de sortie générée par Sieve.
Les RFC d’authentification du courrier elles-mêmes (RFC 6376 DKIM, RFC 7208 SPF, RFC 7489 DMARC, RFC 8617 ARC, RFC 8461 MTA-STS, RFC 7672 DANE) sont implémentées dans le crate maison oximail-auth (qui aura sa propre page de référence) et consommées par oximail-smtp.
Carte des sources et tests
| Fichier (dans le crate) | Contenu |
|---|
src/inbound.rs | Le SmtpServer, la boucle de commandes, process_data (la séparation des chemins), process_inbound_data, process_submission_data, deliver_to_recipient, et les fonctions de réponse de rejet (smtp_dmarc_reject_reply, smtp_spam_reject_reply). |
src/session.rs | SmtpSession et la machine à états SessionState. |
src/auth.rs | L’enveloppe de vérification oximail-auth (AuthResolver, verify_message, should_reject, should_flag_junk). |
src/outbound.rs | Le worker sortant : résolution MX, politique de transport, signature DKIM à la remise, TLS. |
src/queue.rs | DeliveryQueue : la file sortante persistante, le backoff de réessai et la fenêtre d’abandon. |
src/dsn.rs | La génération d’avis de non-remise RFC 3464. |
src/postmaster.rs | PostmasterReason : l’unique source de vérité pour les slugs de rejet ?p= et les URL d’aide. |
src/backup.rs, src/backup_proxy.rs | Le mode file et le mode proxy synchrone du MX de secours (ADR-052). |
src/srs.rs | La réécriture d’enveloppe SRS lors du transfert. |
src/mta_sts.rs, src/dane.rs | Les contrôles de politique de transport sortante (consomment les primitives oximail-auth). |
src/greylist.rs, src/bayes.rs, src/spam_store.rs | Le filtre greylist et le stockage local du corpus de spam que le scoreur lit. |
src/header_mutate.rs, src/sanitize.rs, src/transport.rs, src/rate_limit.rs, src/trusted.rs | Mutation d’en-têtes, assainissement du contenu, helpers de transport, limitation de débit et ensemble des pairs de confiance. |
postmaster.rs porte un test qui vérifie que l’URL d’aide de chaque motif est bien formée et stable. Les fonctions de réponse de rejet portent des tests qui fixent le rejet DMARC à 5.7.26 et le rejet de contenu à 5.7.1, tous deux avec l’URL postmaster. La machine à états de session a son propre test de transitions.
Statut
Implémenté et en production en v0.30.0 : les deux chemins de code séparés entrant et soumission, le pipeline entrant complet d’authentification, scellement ARC, orchestration anti-spam et remise, la validation d’identité d’enveloppe et d’en-tête de la soumission, la file de remise persistante avec backoff et la fenêtre d’abandon RFC 5321, la génération de DSN RFC 3464, la convention de rejet avec URL d’aide postmaster, et les deux modes du MX de secours (file et proxy, ADR-052).
Le scoring de contenu du spam que le chemin entrant orchestre est implémenté dans le crate oximail-spam ; sur la build communautaire (AGPL), le scoreur exécute les signaux heuristiques et d’authentification, et la capture de corpus spamtrap (ADR-077) est active en production. Les primitives d’authentification du courrier sont fournies par le crate maison oximail-auth, qui aura sa propre page de référence.