921 lines
36 KiB
C#
921 lines
36 KiB
C#
// ============================================================
|
|
// 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<SmtpConfigurationDto> 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<IReadOnlyDictionary<string, object?>> 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<string, object?> candidat in candidats)
|
|
{
|
|
if (!candidat.TryGetValue("PU_IDUtilisateur", out object? brutId) || brutId is null
|
|
|| !long.TryParse(brutId.ToString(), out long utid))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
IReadOnlyList<IReadOnlyDictionary<string, object?>> 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<IReadOnlyDictionary<string, object?>> 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<SmtpConfigurationDto> 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_<soc> 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<long, PresenceSmtp> societes = new();
|
|
IReadOnlyList<IReadOnlyDictionary<string, object?>> lignesSociete = await connexion.QueryAsync(
|
|
"SELECT SO_PKID, SO_Parametres FROM Societe",
|
|
null);
|
|
|
|
foreach (IReadOnlyDictionary<string, object?> 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<IReadOnlyDictionary<string, object?>> 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<string, object?> 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
|
|
// ============================================================
|
|
|
|
/// <summary>
|
|
/// Résout un identifiant fourni vers l'identifiant de COLLABORATEUR attendu par
|
|
/// le service. Un identifiant utilisateur (<c>UT_PKID</c>) est l'entrée la plus
|
|
/// fréquente ; un <c>CO_PKID</c> reste accepté (et signalé comme tel).
|
|
/// </summary>
|
|
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<IReadOnlyDictionary<string, object?>> 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<string, object?> 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<IReadOnlyDictionary<string, object?>> collaborateurs = await connexion.QueryAsync(
|
|
"SELECT CO_PKID, CO_Nom, CO_Prenom FROM Collaborateur WHERE CO_PKID = ?",
|
|
[cle]);
|
|
|
|
if (collaborateurs.Count > 0)
|
|
{
|
|
IReadOnlyDictionary<string, object?> 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<IReadOnlyDictionary<string, object?>> 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<IReadOnlyDictionary<string, object?>> 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<long>();
|
|
foreach (IReadOnlyDictionary<string, object?> 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<IReadOnlyDictionary<string, object?>> liaisons = await AfficherLignesAsync(
|
|
connexion,
|
|
"SELECT SC_IDCSociete FROM Societe_Collaborateur WHERE SC_IDCollaborateur = ?",
|
|
[pk]);
|
|
|
|
var idsSociete = new List<long>();
|
|
foreach (IReadOnlyDictionary<string, object?> 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<IReadOnlyDictionary<string, object?>> societes = await connexion.QueryAsync(
|
|
$"SELECT SO_PKID, SO_Parametres FROM Societe WHERE SO_PKID IN ({placeholders})",
|
|
idsSociete.Cast<object?>().ToArray());
|
|
|
|
foreach (IReadOnlyDictionary<string, object?> 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<IReadOnlyDictionary<string, object?>> 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<string, object?> 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<IReadOnlyDictionary<string, object?>> 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<string, object?> 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<IReadOnlyList<IReadOnlyDictionary<string, object?>>> AfficherLignesAsync(
|
|
IHfsqlConnection connexion,
|
|
string sql,
|
|
IReadOnlyList<object?> parametres)
|
|
{
|
|
IReadOnlyList<IReadOnlyDictionary<string, object?>> lignes =
|
|
await connexion.QueryAsync(sql, parametres);
|
|
|
|
if (lignes.Count == 0)
|
|
{
|
|
Console.WriteLine(" (aucune ligne)");
|
|
return lignes;
|
|
}
|
|
|
|
foreach (IReadOnlyDictionary<string, object?> ligne in lignes)
|
|
{
|
|
IEnumerable<string> paires = ligne.Select(kv => $"{kv.Key} = {ResumerValeur(kv.Value)}");
|
|
Console.WriteLine(" " + string.Join(" | ", paires));
|
|
}
|
|
|
|
return lignes;
|
|
}
|
|
|
|
/// <summary>Résume une valeur JSON de <c>ParametreUtilisateur</c> sans secret.</summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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, "<img\\b", RegexOptions.IgnoreCase).Count;
|
|
int imagesIntegrees = Regex.Matches(html, "src\\s*=\\s*[\"']data:", RegexOptions.IgnoreCase).Count;
|
|
int liens = Regex.Matches(html, "<a\\b", RegexOptions.IgnoreCase).Count;
|
|
int tableaux = Regex.Matches(html, "<table\\b", RegexOptions.IgnoreCase).Count;
|
|
|
|
Console.WriteLine($" HTML = {html.Length} caractère(s), {images} image(s) " +
|
|
$"({imagesIntegrees} intégrée(s) en base64), {liens} lien(s), {tableaux} tableau(x)");
|
|
|
|
string texte = SignatureVersTexte(html);
|
|
Console.WriteLine(" Texte visible :");
|
|
foreach (string ligne in texte.Split('\n'))
|
|
{
|
|
Console.WriteLine($" | {ligne}");
|
|
}
|
|
|
|
string nomFichier = Regex.Replace($"{etiquettes}_{configuration.Id}", "[^A-Za-z0-9_.-]", "_");
|
|
string chemin = Path.Combine(dossier, nomFichier + ".html");
|
|
File.WriteAllText(
|
|
chemin,
|
|
$"<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Signature {configuration.Id}</title></head>" +
|
|
$"<body><p>--- corps du courriel ---</p>{html}</body></html>",
|
|
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
|
Console.WriteLine($" Aperçu HTML complet : {chemin}");
|
|
}
|
|
|
|
/// <summary>Rend lisible un fragment HTML de signature : texte, images signalées.</summary>
|
|
static string SignatureVersTexte(string html)
|
|
{
|
|
string travail = html;
|
|
|
|
travail = Regex.Replace(travail, "<!--.*?-->", " ", RegexOptions.Singleline);
|
|
travail = Regex.Replace(travail, "<(script|style|head)\\b[^>]*>.*?</\\1\\s*>", " ", RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
|
|
|
// Images : on signale leur présence sans déverser le base64 dans la console.
|
|
travail = Regex.Replace(
|
|
travail,
|
|
"<img\\b[^>]*?alt\\s*=\\s*[\"']([^\"']*)[\"'][^>]*>",
|
|
" [image: $1] ",
|
|
RegexOptions.IgnoreCase);
|
|
travail = Regex.Replace(travail, "<img\\b[^>]*>", " [image] ", RegexOptions.IgnoreCase);
|
|
|
|
travail = Regex.Replace(travail, "<\\s*br\\s*/?>", "\n", RegexOptions.IgnoreCase);
|
|
travail = Regex.Replace(travail, "</\\s*(p|div|tr|h[1-6]|li|table|blockquote)\\s*>", "\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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Journal minimal : les messages du service (invariants de diagnostic) sont
|
|
/// affichés tels quels, sans exposer de secret.
|
|
/// </summary>
|
|
internal sealed class JournalConsole : ILogger<SmtpConfigurationService>
|
|
{
|
|
public IDisposable? BeginScope<TState>(TState state)
|
|
where TState : notnull
|
|
=> null;
|
|
|
|
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Information;
|
|
|
|
public void Log<TState>(
|
|
LogLevel logLevel,
|
|
EventId eventId,
|
|
TState state,
|
|
Exception? exception,
|
|
Func<TState, Exception?, string> formatter)
|
|
{
|
|
if (!IsEnabled(logLevel))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.WriteLine($" [journal {logLevel}] {formatter(state, exception)}");
|
|
}
|
|
}
|