// ============================================================ // SONDE — CONFIGURATION SMTP RÉELLEMENT SERVIE À UN COLLABORATEUR // // Exécute le VRAI service de production (SmtpConfigurationService + repositories // Omika + déchiffreur sMdpStandard) contre une base Omika, et affiche pour chaque // collaborateur demandé : // 1. les lignes physiques lues en base (contexte) ; // 2. la liste des configurations EXPOSÉES AU MOBILE (contrat // GET /api/mails/configurations-smtp : Id, Libelle, Email) ; // 3. la configuration EFFECTIVE utilisée à l'envoi (serveur, port, cryptage, // login, mot de passe résolu, signature, CCI) et sa validité. // // LECTURE SEULE : aucune écriture, aucune connexion SMTP, aucun envoi. // Le mot de passe n'est jamais affiché en clair sans --montrer-mdp. // // USAGE : Omika.SmtpMobileConfigProbe [id...] [--montrer-mdp] // ============================================================ using System.Net; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Omika.Core.Configuration; using Omika.Core.Contracts.Documents; using Omika.Core.Interfaces; using Omika.Core.Security; using Omika.Core.Services; using Omika.Core.Services.Mobile; using Omika.Data.Configuration; using Omika.Data.Connection; using Omika.Data.HfSql.Connection; using Omika.Data.Repositories.Omika; Console.OutputEncoding = Encoding.UTF8; const string CheminConfiguration = @"D:\Avalonia\Omika\config\hfsql-test.local.json"; bool montrerMotDePasse = args.Contains("--montrer-mdp", StringComparer.OrdinalIgnoreCase); bool montrerSignatures = args.Contains("--signatures", StringComparer.OrdinalIgnoreCase); string dossierSignatures = Path.Combine(Directory.GetCurrentDirectory(), ".diag", "signatures"); string[] cibles = args .Where(a => !a.StartsWith("--", StringComparison.Ordinal)) .ToArray(); if (montrerSignatures) { Directory.CreateDirectory(dossierSignatures); } if (cibles.Length == 0) { cibles = ["281474976710679", "281474976710671", "281474976710697"]; } // --- 1. Options HFSQL de la base de test (section « Hfsql ») ----------------- if (!File.Exists(CheminConfiguration)) { Console.Error.WriteLine($"ERREUR : configuration absente ({CheminConfiguration})."); return 2; } HfsqlOptions options; using (JsonDocument document = JsonDocument.Parse(File.ReadAllText(CheminConfiguration))) { JsonElement hfsql = document.RootElement.GetProperty("Hfsql"); JsonElement motsDePasse = hfsql.GetProperty("FilePasswords"); options = new HfsqlOptions { ConnectionName = LireTexte(hfsql, "ConnectionName") ?? "Business", Server = LireTexte(hfsql, "Server"), Port = hfsql.TryGetProperty("Port", out JsonElement port) ? port.GetInt32() : 4900, DatabaseName = LireTexte(hfsql, "DatabaseName"), Login = LireTexte(hfsql, "Login"), Password = LireTexte(hfsql, "Password"), Driver = LireTexte(hfsql, "Driver") ?? HfsqlOptions.DefaultOdbcDriver, FilePasswords = new HfSqlFilePasswordOptions { ProtectionStd = LireTexte(motsDePasse, "ProtectionStd"), ProtectionPlus = LireTexte(motsDePasse, "ProtectionPlus") } }; } Console.WriteLine("=============================================================================="); Console.WriteLine(" CONFIGURATION SMTP SERVIE AU MOBILE — SERVICE DE PRODUCTION, LECTURE SEULE"); Console.WriteLine("=============================================================================="); Console.WriteLine($"Base : {options.DatabaseName} ({options.Server}:{options.Port})"); Console.WriteLine($"Identifiants : {string.Join(", ", cibles)} (lus d'abord comme UT_PKID, puis CO_PKID)"); Console.WriteLine($"Mdp en clair : {(montrerMotDePasse ? "OUI (--montrer-mdp)" : "non (longueur seulement)")}"); Console.WriteLine(); // --- 2. Service de production, câblé sur les vrais repositories Omika -------- await using var connexion = new BusinessOleDbConnection(options, new HfSqlOleDbConnectionFactory()); await connexion.OpenAsync(); var service = new SmtpConfigurationService( new OmikaSmtpConfigurationRepository(connexion), new OmikaCollaborateurRepository(connexion), new MobileEntityIdentityResolver(), new JournalConsole(), Options.Create(new SmtpPasswordOptions()), new SmtpPasswordStandardDecryptor()); foreach (string id in cibles) { Console.WriteLine("=============================================================================="); Console.WriteLine($" IDENTIFIANT FOURNI : {id}"); Console.WriteLine("=============================================================================="); try { (string collaborateurId, string origine) = await ResoudreCollaborateurAsync(connexion, id); Console.WriteLine("-- 0. Résolution de l'identifiant"); Console.WriteLine($" {origine}"); if (collaborateurId.Length == 0) { Console.WriteLine(" → aucun collaborateur résolu : le mobile ne reçoit RIEN."); Console.WriteLine(); continue; } await AfficherContexteAsync(connexion, collaborateurId); IReadOnlyList liste = await service.GetListAsync(collaborateurId); Console.WriteLine("-- CE QUE LE MOBILE REÇOIT (GET /api/mails/configurations-smtp)"); if (liste.Count == 0) { Console.WriteLine(" (aucune configuration : l'écran de sélection sera vide)"); } foreach (SmtpConfigurationDto dto in liste) { string libelle = string.IsNullOrWhiteSpace(dto.Libelle) ? (dto.Email ?? "(sans libellé)") : dto.Libelle; Console.WriteLine($" - Id = {dto.Id}"); Console.WriteLine($" Libelle = {libelle}"); Console.WriteLine($" Email = {dto.Email ?? "(aucun)"}"); } Console.WriteLine(); Console.WriteLine("-- CONFIGURATION EFFECTIVE UTILISÉE À L'ENVOI (résolue côté serveur)"); if (liste.Count == 0) { Console.WriteLine(" (aucune configuration à résoudre)"); } foreach (SmtpConfigurationDto dto in liste) { SmtpConfiguration? configuration = await service.GetAsync(collaborateurId, dto.Id); if (configuration is null) { Console.WriteLine($" [{dto.Id}] NON RÉSOLUE (identifiant absent de la liste chargée)."); continue; } AfficherConfiguration(configuration, montrerMotDePasse, montrerSignatures, dossierSignatures, id); } } catch (Exception ex) { Console.WriteLine($" ERREUR {ex.GetType().Name} : {ex.Message}"); } Console.WriteLine(); } Console.WriteLine("Sonde terminée (aucune modification en base)."); // --- Preuve du cas « configuration de SOCIÉTÉ seule » ----------------------- // Le bug d'origine : un collaborateur en messagerie interne qui n'a AUCUNE ligne // EMAIL_ENVOI personnelle ne recevait rien (ou une configuration fantôme). On // cherche ici de vrais collaborateurs dans cette situation et on affiche ce que // le service leur rend. if (args.Contains("--exemples-societe", StringComparer.OrdinalIgnoreCase)) { Console.WriteLine(); Console.WriteLine("=============================================================================="); Console.WriteLine(" EXEMPLES : messagerie « Back-Office » SANS configuration personnelle"); Console.WriteLine("=============================================================================="); IReadOnlyList> candidats = await connexion.QueryAsync( "SELECT PU_IDUtilisateur FROM ParametreUtilisateur " + "WHERE PU_Code = 'CHOIX_MESSAGERIE' AND PU_Valeur = 'Back-Office'", null); int montres = 0; foreach (IReadOnlyDictionary candidat in candidats) { if (!candidat.TryGetValue("PU_IDUtilisateur", out object? brutId) || brutId is null || !long.TryParse(brutId.ToString(), out long utid)) { continue; } IReadOnlyList> perso = await connexion.QueryAsync( "SELECT PU_Code FROM ParametreUtilisateur WHERE PU_IDUtilisateur = ? AND PU_Code LIKE 'EMAIL_ENVOI%'", [utid]); if (perso.Count > 0) { continue; } IReadOnlyList> users = await connexion.QueryAsync( "SELECT UT_PKID, UT_Login, UT_IDCollaborateur FROM Utilisateur WHERE UT_PKID = ?", [utid]); if (users.Count == 0 || !users[0].TryGetValue("UT_IDCollaborateur", out object? brutColb) || brutColb is null || !long.TryParse(brutColb.ToString(), out long colbId) || colbId <= 0) { continue; } string login = users[0].TryGetValue("UT_Login", out object? brutLogin) ? brutLogin?.ToString() ?? "?" : "?"; Console.WriteLine(); Console.WriteLine($"-- Collaborateur {colbId} (utilisateur {utid}, login {login}) — aucune ligne EMAIL_ENVOI"); IReadOnlyList liste = await service.GetListAsync(colbId.ToString()); foreach (SmtpConfigurationDto dto in liste) { Console.WriteLine($" reçu : Id = {dto.Id} | Libelle = {dto.Libelle} | Email = {dto.Email ?? "(aucun)"}"); SmtpConfiguration? configuration = await service.GetAsync(colbId.ToString(), dto.Id); if (configuration is not null) { AfficherConfiguration( configuration, montrerMotDePasse, montrerSignatures, dossierSignatures, colbId.ToString()); } } if (liste.Count == 0) { Console.WriteLine(" reçu : liste VIDE"); } montres++; if (montres >= 3) { break; } } if (montres == 0) { Console.WriteLine(" (aucun collaborateur dans cette situation dans la base)"); } } // --- Audit : la fusion société + ligne utilisateur perd-elle un champ ? ------ // Un « Configuration SMTP invalide ou incomplète » ne peut venir que d'une // configuration EFFECTIVE sans serveur, port, cryptage, login ou mot de passe. // La fusion (Fusionner) ne reprend de la ligne EMAIL_ENVOI_ que le compte, // la signature et les copies : si la SOCIÉTÉ n'a pas de login ou de mot de passe // alors que la ligne utilisateur les porte, l'envoi échoue. On mesure ce cas. if (args.Contains("--audit-fusion", StringComparer.OrdinalIgnoreCase)) { await AuditerFusionAsync(connexion); } return 0; // ============================================================ // Audit de la fusion société / ligne utilisateur // ============================================================ static async Task AuditerFusionAsync(IHfsqlConnection connexion) { Console.WriteLine(); Console.WriteLine("=============================================================================="); Console.WriteLine(" AUDIT — PRÉSENCE DES CHAMPS SMTP (sociétés vs lignes utilisateur)"); Console.WriteLine("=============================================================================="); Dictionary societes = new(); IReadOnlyList> lignesSociete = await connexion.QueryAsync( "SELECT SO_PKID, SO_Parametres FROM Societe", null); foreach (IReadOnlyDictionary ligne in lignesSociete) { if (!ligne.TryGetValue("SO_PKID", out object? brut) || brut is null || !long.TryParse(brut.ToString(), out long soId) || soId <= 0) { continue; } string? json = ligne.TryGetValue("SO_Parametres", out object? brutJson) ? brutJson?.ToString() : null; PresenceSmtp presence = LirePresence(json); societes[soId] = presence; Console.WriteLine($" Société {soId,-20} {presence.Resume()}"); } Console.WriteLine(); Console.WriteLine("-- C. CAS OÙ LA FUSION ACTUELLE PERD LE LOGIN OU LE MOT DE PASSE"); IReadOnlyList> lignesUtilisateur = await connexion.QueryAsync( "SELECT PU_IDUtilisateur, PU_Code, PU_Valeur FROM ParametreUtilisateur " + "WHERE PU_Code LIKE 'EMAIL_ENVOI\\_%'", null); int cas = 0; foreach (IReadOnlyDictionary ligne in lignesUtilisateur) { string code = ligne.TryGetValue("PU_Code", out object? brutCode) ? brutCode?.ToString() ?? "" : ""; string suffixe = code["EMAIL_ENVOI_".Length..]; if (!long.TryParse(suffixe, out long soId) || !societes.TryGetValue(soId, out PresenceSmtp societe)) { continue; } string? json = ligne.TryGetValue("PU_Valeur", out object? brutValeur) ? brutValeur?.ToString() : null; PresenceSmtp perso = LirePresence(json); bool loginPerdu = !societe.Login && perso.Login; bool mdpPerdu = !societe.MdpStandard && perso.MdpStandard; bool serveurPerdu = (!societe.Serveur || !societe.Port || !societe.Cryptage) && perso.Serveur && perso.Port && perso.Cryptage; if (!loginPerdu && !mdpPerdu && !serveurPerdu) { continue; } cas++; string ut = ligne.TryGetValue("PU_IDUtilisateur", out object? brutUt) ? brutUt?.ToString() ?? "?" : "?"; Console.WriteLine($" UT {ut} / {code}"); Console.WriteLine($" société {soId} : {societe.Resume()}"); Console.WriteLine($" ligne utilisateur : {perso.Resume()}"); Console.WriteLine( $" → perdu par la fusion : " + string.Join(", ", new[] { loginPerdu ? "LOGIN" : null, mdpPerdu ? "MOT DE PASSE" : null, serveurPerdu ? "SERVEUR/PORT/CRYPTAGE" : null }.Where(v => v is not null))); } Console.WriteLine(); Console.WriteLine($" {cas} cas où la fusion jette un champ indispensable alors que la ligne utilisateur le porte."); if (cas == 0) { Console.WriteLine(" (aucun dans cette base : les sociétés portent elles-mêmes login et mot de passe)"); } } static PresenceSmtp LirePresence(string? json) { if (string.IsNullOrWhiteSpace(json)) { return default; } try { using JsonDocument document = JsonDocument.Parse(json); JsonElement racine = document.RootElement; if (racine.ValueKind == JsonValueKind.Array) { if (racine.GetArrayLength() == 0) { return default; } racine = racine[0]; } if (racine.ValueKind != JsonValueKind.Object) { return default; } JsonElement compte = racine.TryGetProperty("Compte", out JsonElement blocCompte) && blocCompte.ValueKind == JsonValueKind.Object ? blocCompte : default; JsonElement smtp = racine.TryGetProperty("SMTP", out JsonElement blocSmtp) && blocSmtp.ValueKind == JsonValueKind.Object ? blocSmtp : default; bool cryptage = smtp.ValueKind == JsonValueKind.Object && smtp.TryGetProperty("nCryptage", out JsonElement n) && n.ValueKind == JsonValueKind.Number && n.GetInt32() is 1 or 2 or 3; return new PresenceSmtp( Present(smtp, "sAdresseServeur"), Present(smtp, "sPortServeur"), cryptage, Present(smtp, "sLogin"), Present(smtp, "sMdpStandard"), Present(smtp, "sMdp"), Present(compte, "sCompte"), Present(compte, "sEmail")); } catch (JsonException) { return default; } } static bool Present(JsonElement element, string nom) => element.ValueKind == JsonValueKind.Object && element.TryGetProperty(nom, out JsonElement valeur) && valeur.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(valeur.GetString()); // ============================================================ // Affichage // ============================================================ /// /// Résout un identifiant fourni vers l'identifiant de COLLABORATEUR attendu par /// le service. Un identifiant utilisateur (UT_PKID) est l'entrée la plus /// fréquente ; un CO_PKID reste accepté (et signalé comme tel). /// static async Task<(string CollaborateurId, string Origine)> ResoudreCollaborateurAsync( IHfsqlConnection connexion, string id) { if (!long.TryParse(id, out long cle) || cle <= 0) { return (string.Empty, "Identifiant non numérique : le mobile n'envoie jamais un tel identifiant."); } IReadOnlyList> utilisateurs = await connexion.QueryAsync( "SELECT UT_PKID, UT_Login, UT_Nom, UT_Prenom, UT_IDCollaborateur FROM Utilisateur WHERE UT_PKID = ?", [cle]); if (utilisateurs.Count > 0) { IReadOnlyDictionary utilisateur = utilisateurs[0]; string login = utilisateur.TryGetValue("UT_Login", out object? brutLogin) ? brutLogin?.ToString() ?? "?" : "?"; string nom = utilisateur.TryGetValue("UT_Nom", out object? brutNom) ? brutNom?.ToString() ?? "" : ""; string prenom = utilisateur.TryGetValue("UT_Prenom", out object? brutPrenom) ? brutPrenom?.ToString() ?? "" : ""; bool lie = utilisateur.TryGetValue("UT_IDCollaborateur", out object? brutColb) && brutColb is not null && long.TryParse(brutColb.ToString(), out long colbId) && colbId > 0; if (!lie) { return (string.Empty, $"UT_PKID {cle} = utilisateur « {login} » ({nom} {prenom}) mais UT_IDCollaborateur est vide : " + "aucun collaborateur, donc aucune configuration."); } long collaborateurId = long.Parse(utilisateur["UT_IDCollaborateur"]!.ToString()!); return (collaborateurId.ToString(), $"UT_PKID {cle} = utilisateur « {login} » ({nom} {prenom}) → collaborateur {collaborateurId}"); } IReadOnlyList> collaborateurs = await connexion.QueryAsync( "SELECT CO_PKID, CO_Nom, CO_Prenom FROM Collaborateur WHERE CO_PKID = ?", [cle]); if (collaborateurs.Count > 0) { IReadOnlyDictionary collaborateur = collaborateurs[0]; string nom = collaborateur.TryGetValue("CO_Nom", out object? brutNom) ? brutNom?.ToString() ?? "" : ""; string prenom = collaborateur.TryGetValue("CO_Prenom", out object? brutPrenom) ? brutPrenom?.ToString() ?? "" : ""; return (id, $"Aucun UT_PKID {cle} : c'est un CO_PKID ({nom} {prenom}), utilisé directement."); } return (string.Empty, $"Identifiant {cle} absent de Utilisateur (UT_PKID) ET de Collaborateur (CO_PKID)."); } static async Task AfficherContexteAsync(IHfsqlConnection connexion, string id) { long pk = long.TryParse(id, out long pkNumerique) ? pkNumerique : 0; Console.WriteLine("-- 1. Collaborateur (CO_PKID)"); IReadOnlyList> collaborateurs = await AfficherLignesAsync( connexion, "SELECT CO_PKID, CO_Nom, CO_Prenom, CO_Email, CO_Actif, GUID FROM Collaborateur WHERE CO_PKID = ?", [pk]); if (collaborateurs.Count == 0) { Console.WriteLine( " ⚠ Aucun Collaborateur pour cet identifiant : le service rend une liste VIDE " + "(il n'existe aucun utilisateur lié)."); } Console.WriteLine("-- 2. Utilisateur lié (UT_IDCollaborateur = CO_PKID)"); IReadOnlyList> utilisateurs = await AfficherLignesAsync( connexion, "SELECT UT_PKID, UT_Login, UT_Nom, UT_Prenom, UT_IDCollaborateur, UT_IDSocietePrincipale " + "FROM Utilisateur WHERE UT_IDCollaborateur = ?", [pk]); if (utilisateurs.Count == 0) { Console.WriteLine( " (aucun Utilisateur pour ce collaborateur : le service ne peut résoudre aucun compte d'envoi)"); } var idsUtilisateur = new List(); foreach (IReadOnlyDictionary ligne in utilisateurs) { if (ligne.TryGetValue("UT_PKID", out object? brutUt) && brutUt is not null && long.TryParse(brutUt.ToString(), out long utid)) { idsUtilisateur.Add(utid); } } Console.WriteLine("-- 3. Sociétés de rattachement (Societe_Collaborateur)"); IReadOnlyList> liaisons = await AfficherLignesAsync( connexion, "SELECT SC_IDCSociete FROM Societe_Collaborateur WHERE SC_IDCollaborateur = ?", [pk]); var idsSociete = new List(); foreach (IReadOnlyDictionary ligne in liaisons) { if (ligne.TryGetValue("SC_IDCSociete", out object? brutSoc) && brutSoc is not null && long.TryParse(brutSoc.ToString(), out long socId) && socId > 0) { idsSociete.Add(socId); } } if (idsSociete.Count > 0) { string placeholders = string.Join(", ", idsSociete.Select(_ => "?")); Console.WriteLine(" Paramètres des sociétés rattachées (Societe.SO_Parametres) :"); IReadOnlyList> societes = await connexion.QueryAsync( $"SELECT SO_PKID, SO_Parametres FROM Societe WHERE SO_PKID IN ({placeholders})", idsSociete.Cast().ToArray()); foreach (IReadOnlyDictionary ligne in societes) { string soId = ligne.TryGetValue("SO_PKID", out object? brutSo) ? brutSo?.ToString() ?? "?" : "?"; string? contenu = ligne.TryGetValue("SO_Parametres", out object? brutParams) ? brutParams?.ToString() : null; Console.WriteLine($" Société {soId}"); ResumeJson($"EMAIL_ENVOI_{soId}", contenu); } } Console.WriteLine("-- 4. ParametreUtilisateur (EMAIL_ENVOI% + CHOIX_MESSAGERIE)"); if (idsUtilisateur.Count == 0) { Console.WriteLine(" (aucun utilisateur : aucune ligne à lire)"); return; } foreach (long utid in idsUtilisateur) { IReadOnlyList> lignes = await connexion.QueryAsync( "SELECT PU_Code, PU_Valeur FROM ParametreUtilisateur WHERE PU_IDUtilisateur = ? " + "AND (PU_Code LIKE 'EMAIL_ENVOI%' OR PU_Code = 'CHOIX_MESSAGERIE')", [utid]); if (lignes.Count == 0) { Console.WriteLine($" (UT_PKID {utid} : aucune ligne EMAIL_ENVOI / CHOIX_MESSAGERIE)"); } foreach (IReadOnlyDictionary ligne in lignes) { string code = ligne.TryGetValue("PU_Code", out object? brutCode) ? brutCode?.ToString() ?? "" : ""; string? contenu = ligne.TryGetValue("PU_Valeur", out object? brutValeur) ? brutValeur?.ToString() : null; Console.WriteLine($" UT_PKID {utid} / {code}"); ResumeJson(code, contenu); } // Inventaire complet : garantit qu'aucune ligne de configuration SMTP // n'échappe au filtre du service (autre code, autre convention). Console.WriteLine($" (toutes les lignes ParametreUtilisateur de UT_PKID {utid})"); IReadOnlyList> toutes = await connexion.QueryAsync( "SELECT PU_Code, PU_Valeur FROM ParametreUtilisateur WHERE PU_IDUtilisateur = ?", [utid]); if (toutes.Count == 0) { Console.WriteLine(" (aucune ligne du tout)"); } foreach (IReadOnlyDictionary ligne in toutes) { string code = ligne.TryGetValue("PU_Code", out object? brutCode) ? brutCode?.ToString() ?? "" : ""; string? contenu = ligne.TryGetValue("PU_Valeur", out object? brutValeur) ? brutValeur?.ToString() : null; string apercu = contenu is null ? "(null)" : contenu.Length <= 45 ? $"« {contenu} » ({contenu.Length} car.)" : $"{contenu.Length} car."; Console.WriteLine($" {code} = {apercu}"); } } } static async Task>> AfficherLignesAsync( IHfsqlConnection connexion, string sql, IReadOnlyList parametres) { IReadOnlyList> lignes = await connexion.QueryAsync(sql, parametres); if (lignes.Count == 0) { Console.WriteLine(" (aucune ligne)"); return lignes; } foreach (IReadOnlyDictionary ligne in lignes) { IEnumerable paires = ligne.Select(kv => $"{kv.Key} = {ResumerValeur(kv.Value)}"); Console.WriteLine(" " + string.Join(" | ", paires)); } return lignes; } /// Résume une valeur JSON de ParametreUtilisateur sans secret. static void ResumeJson(string code, string? valeur) { if (string.IsNullOrWhiteSpace(valeur)) { Console.WriteLine(" (valeur vide)"); return; } string debut = valeur.TrimStart(); if (!debut.StartsWith('{') && !debut.StartsWith('[')) { Console.WriteLine( $" VALEUR NON JSON ({valeur.Length} car.) : ignorée par le service " + "(chiffrement/binaire historique)."); return; } try { using JsonDocument document = JsonDocument.Parse(valeur); JsonElement racine = document.RootElement; if (racine.ValueKind == JsonValueKind.Array) { int index = 0; foreach (JsonElement element in racine.EnumerateArray()) { Console.WriteLine($" [tableau, entrée {index}]"); ResumeConfiguration(element); index++; } } else { ResumeConfiguration(racine); } } catch (JsonException) { Console.WriteLine($" JSON INVALIDE ({valeur.Length} car.) : ignorée par le service."); } } static void ResumeConfiguration(JsonElement element) { string? compte = null; string? email = null; if (element.TryGetProperty("Compte", out JsonElement blocCompte) && blocCompte.ValueKind == JsonValueKind.Object) { compte = LireTexte(blocCompte, "sCompte"); email = LireTexte(blocCompte, "sEmail"); } if (!element.TryGetProperty("SMTP", out JsonElement smtp) || smtp.ValueKind != JsonValueKind.Object) { Console.WriteLine(" (aucun bloc SMTP)"); return; } string? login = LireTexte(smtp, "sLogin"); string? serveur = LireTexte(smtp, "sAdresseServeur"); string? port = LireTexte(smtp, "sPortServeur"); string? signature = LireTexte(smtp, "bufSignature"); string? mdp = LireTexte(smtp, "sMdp"); string? mdpStandard = LireTexte(smtp, "sMdpStandard"); int cryptage = smtp.TryGetProperty("nCryptage", out JsonElement n) && n.ValueKind == JsonValueKind.Number ? n.GetInt32() : 0; bool enCopieCachee = smtp.TryGetProperty("bEnCopieCachee", out JsonElement bcc) && (bcc.ValueKind == JsonValueKind.True || (bcc.ValueKind == JsonValueKind.String && bool.TryParse(bcc.GetString(), out bool drapeau) && drapeau)); int copies = smtp.TryGetProperty("tabCC_CopieEmail", out JsonElement cc) && cc.ValueKind == JsonValueKind.Array ? cc.GetArrayLength() : 0; string listeCopies = "(aucune)"; if (copies > 0) { var adresses = cc.EnumerateArray() .Select(a => a.ValueKind == JsonValueKind.String ? (a.GetString() ?? "").Trim() : a.ToString()) .Select(a => a.Length == 0 ? "«vide»" : a); listeCopies = string.Join(" | ", adresses); } Console.WriteLine($" Compte.sCompte = {ResumerValeur(compte)}"); Console.WriteLine($" Compte.sEmail = {ResumerValeur(email)}"); Console.WriteLine($" SMTP.sAdresseServeur = {ResumerValeur(serveur)}"); Console.WriteLine($" SMTP.sPortServeur = {ResumerValeur(port)}"); Console.WriteLine($" SMTP.nCryptage = {LibelleCryptage(cryptage)}"); Console.WriteLine($" SMTP.sLogin = {ResumerValeur(login)}"); Console.WriteLine($" SMTP.sMdp = {(string.IsNullOrWhiteSpace(mdp) ? "absent" : $"présent ({mdp.Length} car.) — JAMAIS lu par Omika")}"); Console.WriteLine($" SMTP.sMdpStandard = {(string.IsNullOrWhiteSpace(mdpStandard) ? "ABSENT" : $"présent ({mdpStandard.Length} car. base64)")}"); Console.WriteLine($" Signature = {(string.IsNullOrWhiteSpace(signature) ? "aucune" : $"{signature.Length} car. base64")}"); Console.WriteLine($" bEnCopieCachee = {enCopieCachee}"); Console.WriteLine($" tabCC_CopieEmail = {copies} adresse(s) — {listeCopies}"); } static void AfficherConfiguration( SmtpConfiguration configuration, bool montrerMotDePasse, bool montrerSignatures, string dossierSignatures, string etiquetteProprietaire) { Console.WriteLine(); Console.WriteLine($" [{configuration.Id}]"); string? motDePasse = configuration.MotDePasse; string statutMotDePasse = string.IsNullOrWhiteSpace(motDePasse) ? "ABSENT → envoi impossible" : montrerMotDePasse ? $"« {motDePasse} » ({motDePasse.Length} car.)" : $"{motDePasse.Length} caractère(s), résolu et déchiffré"; Console.WriteLine($" Serveur = {Valeur(configuration.Serveur)}"); Console.WriteLine($" Port = {configuration.Port}"); Console.WriteLine($" Cryptage = {LibelleCryptage(configuration.Cryptage)}"); Console.WriteLine($" Login = {Valeur(configuration.Login)}"); Console.WriteLine($" Expéditeur (From) = {Valeur(configuration.Email)}"); Console.WriteLine($" Compte (libellé) = {Valeur(configuration.Compte)}"); Console.WriteLine($" Mot de passe = {statutMotDePasse}"); Console.WriteLine($" Signature = {(string.IsNullOrWhiteSpace(configuration.Signature) ? "aucune" : $"{configuration.Signature.Length} caractère(s) de HTML")}"); Console.WriteLine($" Copie cachée (CCI) = {(configuration.EnCopieCachee ? "oui" : "non")}"); Console.WriteLine($" Copies (CC) = {(configuration.CcCopieEmail.Count == 0 ? "aucune" : string.Join(", ", configuration.CcCopieEmail))}"); try { configuration.Validate(); Console.WriteLine(" Prête à envoyer = OUI (validation complète)"); } catch (EmailEnvoiException ex) { Console.WriteLine($" Prête à envoyer = NON ({ex.Message})"); } if (montrerSignatures) { AfficherSignature(configuration, dossierSignatures, etiquetteProprietaire); } } /// /// Affiche la signature qui sera réellement concaténée au corps du courriel : /// texte visible (balises retirées, images signalées) et fichier HTML complet /// écrit sur disque pour la vérification visuelle. /// static void AfficherSignature(SmtpConfiguration configuration, string dossier, string etiquettes) { Console.WriteLine(" --- SIGNATURE AJOUTÉE AU CORPS ---"); string? html = configuration.Signature; if (string.IsNullOrWhiteSpace(html)) { Console.WriteLine(" (aucune signature : le corps du courriel reste inchangé)"); return; } int images = Regex.Matches(html, "Signature {configuration.Id}" + $"

--- corps du courriel ---

{html}", new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); Console.WriteLine($" Aperçu HTML complet : {chemin}"); } /// Rend lisible un fragment HTML de signature : texte, images signalées. static string SignatureVersTexte(string html) { string travail = html; travail = Regex.Replace(travail, "", " ", RegexOptions.Singleline); travail = Regex.Replace(travail, "<(script|style|head)\\b[^>]*>.*?", " ", RegexOptions.IgnoreCase | RegexOptions.Singleline); // Images : on signale leur présence sans déverser le base64 dans la console. travail = Regex.Replace( travail, "]*?alt\\s*=\\s*[\"']([^\"']*)[\"'][^>]*>", " [image: $1] ", RegexOptions.IgnoreCase); travail = Regex.Replace(travail, "]*>", " [image] ", RegexOptions.IgnoreCase); travail = Regex.Replace(travail, "<\\s*br\\s*/?>", "\n", RegexOptions.IgnoreCase); travail = Regex.Replace(travail, "", "\n", RegexOptions.IgnoreCase); travail = Regex.Replace(travail, "<\\s*(p|div|tr|h[1-6]|li|table|blockquote)\\b[^>]*>", "\n", RegexOptions.IgnoreCase); travail = Regex.Replace(travail, "<[^>]+>", " "); travail = WebUtility.HtmlDecode(travail); travail = travail.Replace('\u00a0', ' '); // Jetons base64 résiduels (attributs data:/cid: collés, styles volumineux). travail = Regex.Replace(travail, "[A-Za-z0-9+/]{120,}={0,2}", " "); travail = Regex.Replace(travail, "[ \\t\\r]+", " "); travail = Regex.Replace(travail, " *\n *", "\n"); travail = Regex.Replace(travail, "\n{2,}", "\n"); return travail.Trim(); } static string LibelleCryptage(int cryptage) => cryptage switch { 1 => "1 = aucun", 2 => "2 = TLS / STARTTLS", 3 => "3 = SSL", _ => $"{cryptage} = NON RENSEIGNÉ" }; static string Valeur(string? texte) => string.IsNullOrWhiteSpace(texte) ? "(absent)" : texte; static string ResumerValeur(object? valeur) { if (valeur is null or DBNull) { return "(null)"; } string texte = valeur.ToString() ?? ""; if (texte.Length <= 90) { return string.IsNullOrWhiteSpace(texte) ? "(vide)" : texte; } return $"{texte[..90]}… ({texte.Length} car.)"; } static string? LireTexte(JsonElement element, string nom) => element.TryGetProperty(nom, out JsonElement valeur) && valeur.ValueKind == JsonValueKind.String ? valeur.GetString() : null; /// /// Présence (et non valeur) des champs qui décident de la validité d'une /// configuration SMTP, telle qu'elle est réellement stockée en base. /// internal readonly record struct PresenceSmtp( bool Serveur, bool Port, bool Cryptage, bool Login, bool MdpStandard, bool MdpLegacy, bool Compte, bool Email) { public string Resume() => $"serveur={Drapeau(Serveur)} port={Drapeau(Port)} cryptage={Drapeau(Cryptage)} " + $"login={Drapeau(Login)} sMdpStandard={Drapeau(MdpStandard)} sMdpLegacy={Drapeau(MdpLegacy)} " + $"compte={Drapeau(Compte)} email={Drapeau(Email)}"; private static string Drapeau(bool present) => present ? "oui" : "NON"; } /// /// Journal minimal : les messages du service (invariants de diagnostic) sont /// affichés tels quels, sans exposer de secret. /// internal sealed class JournalConsole : ILogger { public IDisposable? BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Information; public void Log( LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (!IsEnabled(logLevel)) { return; } Console.WriteLine($" [journal {logLevel}] {formatter(state, exception)}"); } }