devis PostgreSQL et courriel : la serie des devis de service est complete
PostgreSQL — rend visibles deux defauts deja vecus ici : un reseau ECRIT dans
pg_hba au lieu d'etre derive, et une ligne host en clair la ou il faut hostssl
(verrou qui saute sans bruit, les clients verify-full continuant de marcher).
Interroge pg_settings, jamais le fichier : un grep de postgresql.conf annonce
ssl_cert_file=snakeoil alors que le serveur sert le certificat de l'AC — la
valeur vient d'un conf.d que le grep ne voyait pas. L'instrument etait
incomplet, pas la configuration.
Courriel — chaque maillon interroge la ou il dit la verite : postmap -q pour
la resolution LDAP de Postfix, doveadm user pour celle de Dovecot (le maillon
exact ou la livraison avait bloque), vraies conversations SMTP/IMAP. Verifie
aussi qu'une adresse INEXISTANTE ne resout pas — sinon la boite est un
fourre-tout et le devis ne mesure plus rien.
Trouve immediatement une divergence reelle : Dovecot connait la boite de
sysadmin@chezlepro.internal, Postfix ne sait pas y router (query_filter
(mail=%s), et l'attribut mail porte sysadmin@chezlepro.ca). Collision de
roles : une identite ne porte qu'une adresse mail et on lui en demande deux —
notification joignable hors du systeme, et cle de routage local. Arbitrage a
rendre avant correction.
Tests negatifs : PostgreSQL 3 ecarts nommes, code 1. Une variable morte
trouvee dans une branche que le cas nominal n'emprunte jamais.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 08:53:49 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Devis du courriel : la chaîne Postfix → LDAP → LMTP → Dovecot → IMAP tient-elle ?
|
|
|
|
|
|
|
|
|
|
Lit les relevés de `playbooks/maintenance/devis-courriel.yml`. Sort en code 1 s'il y a un
|
|
|
|
|
écart.
|
|
|
|
|
|
|
|
|
|
La chaîne est longue et chaque maillon peut casser sans que le suivant s'en aperçoive. On
|
|
|
|
|
interroge donc chaque maillon là où il dit la vérité : `postmap -q` pour la résolution LDAP
|
|
|
|
|
de Postfix, `doveadm user` pour celle de Dovecot — c'est exactement là que la livraison
|
|
|
|
|
avait bloqué — et une vraie conversation SMTP/IMAP pour ce que les serveurs annoncent.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import glob
|
|
|
|
|
import ipaddress
|
|
|
|
|
import json
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
RACINE = Path(__file__).resolve().parent.parent
|
|
|
|
|
MOTIF = str(RACINE / "instance" / "devis-courriel.json.*")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _valeur(brut: str, cle: str) -> str:
|
|
|
|
|
m = re.search(rf"^{cle}=(.*)$", brut, re.M)
|
|
|
|
|
return m.group(1).strip() if m else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _dialecte(brut: str, port: str) -> str:
|
|
|
|
|
"""Ce que le serveur annonce sur un port donné."""
|
|
|
|
|
m = re.search(rf"^PORT={port}$(.*?)(?=^PORT=|\Z)", brut, re.M | re.S)
|
|
|
|
|
return m.group(1) if m else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def analyser(releves: list[dict]) -> list[str]:
|
|
|
|
|
ecarts = []
|
|
|
|
|
postfix = next((r for r in releves if r["role"] == "postfix"), None)
|
|
|
|
|
dovecot = next((r for r in releves if r["role"] == "dovecot"), None)
|
|
|
|
|
|
|
|
|
|
if postfix:
|
|
|
|
|
h, brut = postfix["hote"], postfix["brut"]
|
|
|
|
|
sonde = _valeur(brut, "SONDE")
|
|
|
|
|
|
|
|
|
|
if not _valeur(brut, "TROUVE"):
|
|
|
|
|
ecarts.append(
|
|
|
|
|
f"{h} : Postfix ne resout PAS « {sonde} » — rien ne peut lui etre livre "
|
2026-08-08 09:42:45 -04:00
|
|
|
f"localement. Le routage local se fait par IDENTIFIANT : verifier qu'un "
|
|
|
|
|
f"compte porte bien cet `uid` dans l'annuaire, et que la carte est "
|
|
|
|
|
f"limitee au domaine local (`domain =`)")
|
devis PostgreSQL et courriel : la serie des devis de service est complete
PostgreSQL — rend visibles deux defauts deja vecus ici : un reseau ECRIT dans
pg_hba au lieu d'etre derive, et une ligne host en clair la ou il faut hostssl
(verrou qui saute sans bruit, les clients verify-full continuant de marcher).
Interroge pg_settings, jamais le fichier : un grep de postgresql.conf annonce
ssl_cert_file=snakeoil alors que le serveur sert le certificat de l'AC — la
valeur vient d'un conf.d que le grep ne voyait pas. L'instrument etait
incomplet, pas la configuration.
Courriel — chaque maillon interroge la ou il dit la verite : postmap -q pour
la resolution LDAP de Postfix, doveadm user pour celle de Dovecot (le maillon
exact ou la livraison avait bloque), vraies conversations SMTP/IMAP. Verifie
aussi qu'une adresse INEXISTANTE ne resout pas — sinon la boite est un
fourre-tout et le devis ne mesure plus rien.
Trouve immediatement une divergence reelle : Dovecot connait la boite de
sysadmin@chezlepro.internal, Postfix ne sait pas y router (query_filter
(mail=%s), et l'attribut mail porte sysadmin@chezlepro.ca). Collision de
roles : une identite ne porte qu'une adresse mail et on lui en demande deux —
notification joignable hors du systeme, et cle de routage local. Arbitrage a
rendre avant correction.
Tests negatifs : PostgreSQL 3 ecarts nommes, code 1. Une variable morte
trouvee dans une branche que le cas nominal n'emprunte jamais.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 08:53:49 -04:00
|
|
|
if _valeur(brut, "FANTOME"):
|
|
|
|
|
ecarts.append(f"{h} : une adresse inexistante resout quand meme — la boite est "
|
|
|
|
|
f"un fourre-tout, n'importe quel nom serait accepte")
|
|
|
|
|
|
2026-08-08 09:42:45 -04:00
|
|
|
# Un courrier qui ne bouge pas est le seul symptome que ni la resolution ni
|
|
|
|
|
# les dialectes ne revelent.
|
|
|
|
|
differes = _valeur(brut, "DIFFERES")
|
|
|
|
|
if differes.isdigit() and int(differes) > 0:
|
|
|
|
|
raisons = re.search(r"RAISONS<<(.*?)>>", brut, re.S)
|
|
|
|
|
detail = " ".join((raisons.group(1) if raisons else "").split())[:140]
|
|
|
|
|
ecarts.append(f"{h} : {differes} message(s) en attente dans la file — "
|
|
|
|
|
f"le courrier ne part pas. {detail}")
|
|
|
|
|
|
devis PostgreSQL et courriel : la serie des devis de service est complete
PostgreSQL — rend visibles deux defauts deja vecus ici : un reseau ECRIT dans
pg_hba au lieu d'etre derive, et une ligne host en clair la ou il faut hostssl
(verrou qui saute sans bruit, les clients verify-full continuant de marcher).
Interroge pg_settings, jamais le fichier : un grep de postgresql.conf annonce
ssl_cert_file=snakeoil alors que le serveur sert le certificat de l'AC — la
valeur vient d'un conf.d que le grep ne voyait pas. L'instrument etait
incomplet, pas la configuration.
Courriel — chaque maillon interroge la ou il dit la verite : postmap -q pour
la resolution LDAP de Postfix, doveadm user pour celle de Dovecot (le maillon
exact ou la livraison avait bloque), vraies conversations SMTP/IMAP. Verifie
aussi qu'une adresse INEXISTANTE ne resout pas — sinon la boite est un
fourre-tout et le devis ne mesure plus rien.
Trouve immediatement une divergence reelle : Dovecot connait la boite de
sysadmin@chezlepro.internal, Postfix ne sait pas y router (query_filter
(mail=%s), et l'attribut mail porte sysadmin@chezlepro.ca). Collision de
roles : une identite ne porte qu'une adresse mail et on lui en demande deux —
notification joignable hors du systeme, et cle de routage local. Arbitrage a
rendre avant correction.
Tests negatifs : PostgreSQL 3 ecarts nommes, code 1. Une variable morte
trouvee dans une branche que le cas nominal n'emprunte jamais.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 08:53:49 -04:00
|
|
|
for port in ("25", "587"):
|
|
|
|
|
d = _dialecte(brut, port)
|
|
|
|
|
if not d.strip():
|
|
|
|
|
ecarts.append(f"{h}:{port} : aucune reponse SMTP")
|
|
|
|
|
elif "STARTTLS" not in d:
|
|
|
|
|
ecarts.append(f"{h}:{port} : STARTTLS n'est pas annonce — le courriel "
|
|
|
|
|
f"sortirait en clair")
|
|
|
|
|
|
|
|
|
|
supernet = postfix.get("supernet")
|
|
|
|
|
if supernet:
|
|
|
|
|
reseau_attendu = ipaddress.ip_network(supernet)
|
|
|
|
|
for morceau in _valeur(brut, "MYNETWORKS").split():
|
|
|
|
|
morceau = morceau.strip("[]")
|
|
|
|
|
try:
|
|
|
|
|
res = ipaddress.ip_network(morceau, strict=False)
|
|
|
|
|
except ValueError:
|
|
|
|
|
continue
|
|
|
|
|
if res.is_loopback:
|
|
|
|
|
continue
|
|
|
|
|
if res.version != reseau_attendu.version or not res.subnet_of(reseau_attendu):
|
|
|
|
|
ecarts.append(f"{h} : `mynetworks` autorise {res}, hors du supernet "
|
|
|
|
|
f"derive {reseau_attendu} — relais plus large que le tenant")
|
|
|
|
|
|
|
|
|
|
if dovecot:
|
|
|
|
|
h, brut = dovecot["hote"], dovecot["brut"]
|
|
|
|
|
sonde = _valeur(brut, "SONDE")
|
|
|
|
|
userdb = re.search(r"USERDB<<(.*?)>>", brut, re.S)
|
|
|
|
|
corps = userdb.group(1) if userdb else ""
|
|
|
|
|
if "mail_path" not in corps and "home" not in corps:
|
|
|
|
|
ecarts.append(f"{h} : Dovecot ne sait pas ou ecrire pour « {sonde} » — "
|
|
|
|
|
f"la resolution userdb echoue, la livraison LMTP aussi")
|
|
|
|
|
d = _dialecte(brut, "143")
|
|
|
|
|
if not d.strip():
|
|
|
|
|
ecarts.append(f"{h}:143 : aucune reponse IMAP")
|
|
|
|
|
elif "STARTTLS" not in d:
|
|
|
|
|
ecarts.append(f"{h}:143 : STARTTLS n'est pas annonce")
|
|
|
|
|
|
|
|
|
|
if postfix and dovecot:
|
|
|
|
|
if not _valeur(postfix["brut"], "TROUVE") and "mail_path" in dovecot["brut"]:
|
|
|
|
|
ecarts.append(
|
2026-08-08 09:42:45 -04:00
|
|
|
"les deux moities divergent : Dovecot accepte la boite, Postfix ne sait "
|
|
|
|
|
"pas y router. Dovecot a `allow_all_users = yes` et accepte donc "
|
|
|
|
|
"N'IMPORTE QUELLE partie locale — le garde-fou est entierement chez "
|
|
|
|
|
"Postfix, et il vient de sauter.")
|
devis PostgreSQL et courriel : la serie des devis de service est complete
PostgreSQL — rend visibles deux defauts deja vecus ici : un reseau ECRIT dans
pg_hba au lieu d'etre derive, et une ligne host en clair la ou il faut hostssl
(verrou qui saute sans bruit, les clients verify-full continuant de marcher).
Interroge pg_settings, jamais le fichier : un grep de postgresql.conf annonce
ssl_cert_file=snakeoil alors que le serveur sert le certificat de l'AC — la
valeur vient d'un conf.d que le grep ne voyait pas. L'instrument etait
incomplet, pas la configuration.
Courriel — chaque maillon interroge la ou il dit la verite : postmap -q pour
la resolution LDAP de Postfix, doveadm user pour celle de Dovecot (le maillon
exact ou la livraison avait bloque), vraies conversations SMTP/IMAP. Verifie
aussi qu'une adresse INEXISTANTE ne resout pas — sinon la boite est un
fourre-tout et le devis ne mesure plus rien.
Trouve immediatement une divergence reelle : Dovecot connait la boite de
sysadmin@chezlepro.internal, Postfix ne sait pas y router (query_filter
(mail=%s), et l'attribut mail porte sysadmin@chezlepro.ca). Collision de
roles : une identite ne porte qu'une adresse mail et on lui en demande deux —
notification joignable hors du systeme, et cle de routage local. Arbitrage a
rendre avant correction.
Tests negatifs : PostgreSQL 3 ecarts nommes, code 1. Une variable morte
trouvee dans une branche que le cas nominal n'emprunte jamais.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 08:53:49 -04:00
|
|
|
return ecarts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
|
|
|
fichiers = sorted(glob.glob(argv[0] if argv else MOTIF))
|
|
|
|
|
if not fichiers:
|
|
|
|
|
print("Relevés absents. Lancer d'abord : make courriel-plan", file=sys.stderr)
|
|
|
|
|
return 2
|
|
|
|
|
releves = [json.loads(Path(f).read_text(encoding="utf-8")) for f in fichiers]
|
|
|
|
|
ecarts = analyser(releves)
|
|
|
|
|
print(f"Devis du courriel — {' + '.join(r['hote'] for r in releves)}\n")
|
|
|
|
|
for e in ecarts:
|
|
|
|
|
print(f" [écart] {e}")
|
|
|
|
|
if not ecarts:
|
|
|
|
|
print("CONFORME : la chaîne tient, de la résolution LDAP à la boîte.")
|
|
|
|
|
return 0
|
|
|
|
|
print(f"\nÉCART : {len(ecarts)} divergence(s).")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main(sys.argv[1:]))
|