|
- using ClosedXML;
- using Google.Protobuf.WellKnownTypes;
- using MailKit;
- using MailKit.Net.Imap;
- using MailKit.Search;
- using MailKit.Security;
- using MimeKit;
- using Org.BouncyCastle.Security;
- using Services.ReActAgent;
- using System.Text.Json;
- using ToolsServices;
-
- namespace Services
- {
- #region Classe static annexe JsonDb
- public static class JsonDb
- {
- static string FullFileName = FichiersInternesService.ListeMailsSend;
-
- public static ThreadStore Load()
- {
- if (!File.Exists(FullFileName)) return new ThreadStore();
- var json = File.ReadAllText(FullFileName);
- return JsonSerializer.Deserialize<ThreadStore>(json) ?? new ThreadStore();
- }
-
- public static bool Save(ThreadItem thread)
- {
- ThreadStore data;
-
- // Charger fichier existant
- if (File.Exists(FullFileName))
- {
- string json = File.ReadAllText(FullFileName);
- data = JsonSerializer.Deserialize<ThreadStore>(json, new JsonSerializerOptions { WriteIndented = true }) ?? new ThreadStore();
- }
- else
- {
- data = new ThreadStore();
- }
-
- // Chercher si le thread existe déjà
- var existing = data.Threads.FirstOrDefault(t => t.Id == thread.Id);
- if (existing != null)
- {
- int index = data.Threads.IndexOf(existing);
- data.Threads[index] = thread; // remplacement
- }
- else
- {
- data.Threads.Add(thread); // ajout
- }
-
- // Sauvegarder
- string updatedJson = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
- File.WriteAllText(FullFileName, updatedJson);
- return true;
- }
-
- public static void Save(ThreadStore store)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(FullFileName)!);
- var json = JsonSerializer.Serialize(store, new JsonSerializerOptions { WriteIndented = true });
- var tmp = FullFileName + ".tmp";
- File.WriteAllText(tmp, json);
- File.Move(tmp, FullFileName, true);
- }
- }
- #endregion
-
- #region Classe static EmailSendService
- public static class EmailSendService
- {
- #region Variables
- private static ReActAgent.ReActAgent _ReActAgent = new();
- private static List<IMailFolder> ListInboxFolders = new List<IMailFolder>();
- private static CompteMailUser ParametreUser = new();
- #endregion
-
- #region Méthodes publiques
- public static int NbRelancesEnAttente()
- {
- var lstReponsesEnAttente = LstRelancesEnAttente();
- return lstReponsesEnAttente.Count;
- }
-
- public static ThreadStore ChargerDonnees()
- {
- var fileMailsSend = FichiersInternesService.ListeMailsSend;
- ThreadStore store = new ThreadStore();
- if (File.Exists(fileMailsSend))
- {
- var json = System.IO.File.ReadAllText(fileMailsSend);
- store = JsonSerializer.Deserialize<ThreadStore>(json) ?? new ThreadStore();
- }
- return store;
- }
-
- public static List<ThreadItem> LstRelancesEnAttente()
- {
- var store = JsonDb.Load();
- var lstReponsesEnAttente = store.Threads
- .Where(t => !t.SubjectClosed) // Sujet non fermé
- .Where(t => t.IsOverdue) // En retard
- .ToList();
- return lstReponsesEnAttente;
- }
-
- private class MessageDossier
- {
- public UniqueId uid { get; set; }
- public MimeMessage mimeMessage { get; set; } =new();
- public IMailFolder? dossier { get; set; }
- public string dossierFullname { get; set; } = "";
- public string direction { get; set; } = "";
- }
-
- public static async Task<(bool, string, int)> RunOnce(CompteMailUser parametreUser)
- {
- var nbMsgSentSaved = 0;
- var nbMsgInboxSaved = 0;
- var nbRelancesCreated = 0;
- var nbConvReOuvertes = 0;
-
- ParametreUser = parametreUser;
- try
- {
- LoggerService.LogInfo("EmailSendService.RunOnce");
- var dateFilter = DateTime.UtcNow.AddDays(-1 * ParametreUser.DelaySentRecup);
-
- var parametresMail = EmailService.LoadParametres();
- if(parametresMail == null)
- {
- return (false, "Paramètres email introuvable",0);
- }
-
- var store = JsonDb.Load();
-
- using var client = new ImapClient();
- await client.ConnectAsync(parametresMail.ServeurImap, parametresMail.ServeurImapPort, SecureSocketOptions.SslOnConnect);
- await client.AuthenticateAsync(parametresMail.UserAdresse, parametresMail.UserMotPasse);
-
- ListInboxFolders.Clear();
- var personal = client.GetFolder(client.PersonalNamespaces[0]);
- await ProcessFolder(personal);
-
- // Récupérer le dossier "Sent"
- var sentFolder = await personal.GetSubfolderAsync("Sent");
-
- // Si ça ne marche pas (par exemple dossier en français "Envoyés") :
- if (sentFolder == null || !sentFolder.Exists)
- {
- var allFolders = await personal.GetSubfoldersAsync(false);
- sentFolder = allFolders.FirstOrDefault(f => f.Name.Equals("Sent", StringComparison.OrdinalIgnoreCase)
- || f.Name.Equals("Envoyés", StringComparison.OrdinalIgnoreCase));
- }
-
- // Si on a bien trouvé un dossier "Sent" ou équivalent
- if (sentFolder == null)
- {
- return (false, "Dossier mails envoyés introuvables",0);
- }
- var sent = sentFolder;
-
-
-
- await sent.OpenAsync(MailKit.FolderAccess.ReadOnly);
- //var uidsSent = await sent.SearchAsync(SearchQuery.All);
-
-
-
-
-
- var uidsSent = await sent.SearchAsync(SearchQuery.DeliveredAfter(dateFilter));
-
- List<MessageDossier> lst = new();
- foreach (var uid in uidsSent)
- {
- var msg = await sent.GetMessageAsync(uid);
- MessageDossier nouveau = new();
- nouveau.uid = uid;
- nouveau.mimeMessage = msg;
- nouveau.dossier = sent;
- nouveau.dossierFullname = sentFolder.FullName;
- nouveau.direction = "out";
- lst.Add(nouveau);
- }
-
- foreach (var oneFolder in ListInboxFolders)
- {
- var inbox = oneFolder!;// client.Inbox;
- await inbox.OpenAsync(MailKit.FolderAccess.ReadWrite);
- //var uidsInbox = await inbox.SearchAsync(SearchQuery.All);
- var uidsInbox = await inbox.SearchAsync(SearchQuery.DeliveredAfter(dateFilter));
- foreach (var uid in uidsInbox)
- {
- var msg = await inbox.GetMessageAsync(uid);
- MessageDossier nouveau = new();
- nouveau.uid = uid;
- nouveau.mimeMessage = msg;
- nouveau.dossier = inbox;
- nouveau.dossierFullname = inbox.FullName;
- nouveau.direction = "in";
-
- lst.Add(nouveau);
- }
- }
- lst = lst.OrderBy(e => e.mimeMessage.Date).ToList();
- foreach (var message in lst)
- {
- var bCreateMsg = await AddMessage(message.uid, store, message.mimeMessage, message.direction,message.dossierFullname, parametreUser.OverdueDaysSent);
- if (bCreateMsg)
- nbMsgSentSaved++;
- JsonDb.Save(store);
- }
-
-
-
- /*
- foreach (var uid in uidsSent)
- {
- var msg = await sent.GetMessageAsync(uid);
-
- var bCreateMsg = await AddMessage(uid, store, msg, "out", sentFolder.FullName, parametreUser.OverdueDaysSent);
- if (bCreateMsg)
- nbMsgSentSaved++;
- JsonDb.Save(store);
- }
-
- // Tous les dossiers Inbox
-
- foreach (var oneFolder in ListInboxFolders)
- {
- var inbox = oneFolder!;// client.Inbox;
- await inbox.OpenAsync(MailKit.FolderAccess.ReadWrite);
- //var uidsInbox = await inbox.SearchAsync(SearchQuery.All);
- var uidsInbox = await inbox.SearchAsync(SearchQuery.DeliveredAfter(dateFilter));
- foreach (var uid in uidsInbox)
- {
- var msg = await inbox.GetMessageAsync(uid);
-
- var bCreateMsg = await AddMessage(uid, store, msg, "in", oneFolder.FullName, parametreUser.OverdueDaysSent);
- if (bCreateMsg)
- nbMsgInboxSaved++;
- JsonDb.Save(store);
- }
- }
- */
-
- await client.DisconnectAsync(true);
-
- // Analyse IA pour déterminer les relances
-
- foreach (var thread in store.Threads)
- {
- // On prend le dernier mail sortant
- var lastOut = thread.Messages
- .FindLast(m => m.Direction == "out");
-
- var theLast = thread.Messages.FindLast(m => m.Direction == "out" || m.Direction == "in");
- thread.DateLastMessage = theLast!.Date;
-
- if (lastOut != null && lastOut.RequiresResponse)
- {
- // On ne régénère pas une relance si on l'a déjà faite pour ce mail
- bool alreadyFollowedUp = (thread.LastFollowUpForMessageId == lastOut.Id);
- if (!alreadyFollowedUp)
- {
- // Dernier mail entrant
- var lastIn = thread.Messages
- .Where(m => m.Direction == "in")
- .OrderBy(m => m.Date.ToUniversalTime())
- .LastOrDefault();
-
- // relance si
- bool needsFollowUp = (
- lastIn == null // pas de réponse
- || (lastIn.Date.ToUniversalTime() < lastOut.Date.ToUniversalTime()) // pas de réponse APRES le dernier message
- || (lastIn.Date.ToUniversalTime() >= lastOut.Date.ToUniversalTime() && !lastIn.CoversAllPoints) // réponse APRES le dernier message MAIS tous les points ne sont pas couverts
- );
-
- if (needsFollowUp)
- {
- //if(thread.FollowUpDraft == null || thread.FollowUpDraft == "")
- thread.FollowUpDraft = await GenerateFollowUp(thread);
- // Même si cloturée, on ré-ouvre la conversation
- if(thread.SubjectClosed == true)
- {
- nbConvReOuvertes++;
- thread.SubjectClosed = false;
- }
-
- thread.LastFollowUpForMessageId = lastOut.Id;
- nbRelancesCreated++;
- }
- }
- }
- JsonDb.Save(store);
- }
-
- JsonDb.Save(store);
- LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages envoyés sauvegardés : {nbMsgSentSaved}");
- LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages reçus sauvegardés : {nbMsgInboxSaved}");
- LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre relances générées : {nbRelancesCreated}");
- LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre coversation réouvertes : {nbConvReOuvertes}");
- LoggerService.LogDebug("EmailSendService.RunOnce : terminé");
- return (true, "", nbConvReOuvertes);
- }
- catch(Exception ex)
- {
- LoggerService.LogError($"EmailSendService.RunOnce : {ex.Message}");
- LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages envoyés sauvegardés : {nbMsgSentSaved}");
- LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages reçus sauvegardés : {nbMsgInboxSaved}");
- LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre relances générées : {nbRelancesCreated}");
- LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre coversation réouvertes : {nbConvReOuvertes}");
-
- return (false, $"EmailSendService.RunOnce : {ex.Message}", nbConvReOuvertes);
- }
- }
-
- public static bool Save(ThreadItem thread)
- {
- LoggerService.LogInfo("EmailSendService.Save");
- return JsonDb.Save(thread);
- }
-
- public async static Task<bool> ArchiverRestaurerItem(ThreadItemMini thread)
- {
- await Task.Delay(1);
- var store = ChargerDonnees();
- var tts = store.Threads;
- ThreadItem? t = tts!.Where(e => e.Id == thread.Id)!.FirstOrDefault();
- if (t == null)
- {
- return false;
- }
- t.SubjectClosed = !t.SubjectClosed;
- Save(t);
- return true;
- }
-
- public async static Task<bool> SaveSendRelanceItem(ThreadItemMini thread)
- {
- await Task.Delay(1);
- var store = ChargerDonnees();
- var tts = store.Threads;
- ThreadItem? t = tts!.Where(e=>e.Id == thread.Id)!.FirstOrDefault();
- if (t == null)
- {
- return false;
- }
- t.FollowUpDraft = thread.FollowUpDraft;
- Save(t);
- return true;
- }
- public async static Task<bool> SaveAsync(ThreadItem thread)
- {
- await Task.Delay(1);
- LoggerService.LogInfo("EmailSendService.Save");
- return JsonDb.Save(thread);
- }
-
- public static void AllConversationsChangeStatus(bool isClosed)
- {
- var store = JsonDb.Load();
- foreach(var conversation in store.Threads)
- {
- conversation.SubjectClosed = isClosed;
- }
- JsonDb.Save(store);
-
- }
- #endregion
-
- #region Méthodes privées
- private static async Task ProcessFolder(IMailFolder folder)
- {
- var excludedFolders = new[] { "Junk", "spam", "Sent", "Trash", "Drafts" };
- if (Array.Exists(excludedFolders, f => string.Equals(f, folder.Name, StringComparison.OrdinalIgnoreCase)))
- return;
-
-
- if (folder.Attributes.HasFlag(FolderAttributes.NoSelect))
- {
- LoggerService.LogDebug($"📂 {folder.FullName} (conteneur uniquement, pas de mails)");
- }
- else
- {
- ListInboxFolders.Add(folder);
- LoggerService.LogDebug($"📂 {folder.FullName}");
- }
-
- // Parcours récursif des sous-dossiers
- foreach (var sub in folder.GetSubfolders())
- {
- await ProcessFolder(sub);
- }
- }
-
- private static async Task<bool> AddMessage(UniqueId uid, ThreadStore store, MimeMessage msg, string direction, string folder, int overdueDays)
- {
- //var folder = (direction == "out" ? "Sent" : "Inbox");
- var subject = msg.Subject ?? "(sans objet)";
- var body = msg.TextBody ?? msg.HtmlBody ?? "";
- var messageId = msg.MessageId ?? Guid.NewGuid().ToString();
- var references = msg.References?.ToArray() ?? Array.Empty<string>();
- var inReplyTo = msg.InReplyTo;
-
- // Trouve un thread existant basé sur Reply-To
- var thread = (ThreadItem?)null;
-
- if (messageId != null)
- {
- // On cherche le message par son ID (pour ne pas créer un doublon)
- thread = store.Threads.Find(t =>
- t.Messages.Exists(m => m.Id == messageId)
- );
- }
- if (thread == null && inReplyTo != null)
- {
- // On cherche si une reponse existe déjà pour mettre ce message dans le même thread
- thread = store.Threads.Find(t =>
- t.Messages.Exists(m => m.Id == inReplyTo)
- );
- }
- if (thread == null)
- {
- thread = store.Threads.Find(t =>
- t.Messages.Exists(m => m.InReplyTo == messageId)
- );
- }
- // Vérifie si déjà importé via UID
- /*
- if (thread != null && thread.Messages.Exists(m => m.Folder == folder && m.Uid == uid))
- return false;
- */
- if (thread != null)
- {
- // Si le message existe déjà dans le thread, il a peut être été déplacé de dossier
- if (thread.Messages.Exists(m => m.Id == msg.MessageId))
- {
- if (thread.Messages.Exists(m => m.Folder == folder && m.Id == msg.MessageId))
- {
- // existe et n'a pas été déplacé : on arrête
- return false;
- }
- else
- {
- // a été déplacé --> mettre à jour le folder et uid
- var msgExist = thread.Messages.Find(m => m.Id == msg.MessageId);
- if (msgExist != null)
- {
- msgExist.Folder = folder;
- //msgExist.Uid = uid;
- return true;
- }
- else
- {
- // cas impossible ou msgExist est null
- return false;
- }
- }
- }
- }
-
- if (thread == null)
- {
- thread = new ThreadItem(overdueDays) { Subject = subject, DateMessage = msg.Date };
- store.Threads.Add(thread);
- }
-
- var analysis = await AnalyzeMessage(body, direction);
-
- thread.Messages.Add(new MailMessageItem
- {
- Id = msg.MessageId!,
- InReplyTo = msg.InReplyTo,
- //UidString = uid.ToString(),
- Folder = folder,
- Subject = subject,
- From = msg.From.Mailboxes.FirstOrDefault()?.Address ?? "",
- To = string.Join("; ", msg.To.Mailboxes.Select(m => m.Address)),
- Direction = direction,
- Date = msg.Date,
- Body = body,
- RequiresResponse = analysis.requiresResponse,
- CoversAllPoints = analysis.coversAllPoints
- });
- return true;
- }
-
- private static async Task<(bool requiresResponse, bool coversAllPoints)> AnalyzeMessage(string body, string direction)
- {
- try
- {
- var prompt = "";
-
- if (direction == "out")
- {
- //prompt = $"Réponds uniquement par oui ou non. Une réponse est-elle nécessaire à ce mail\nDébut du mail:\n{body} \n- fin du mail";
- prompt = PromptService.GetPrompt(PromptService.ePrompt.EmailSendService_AnalyserMail_BesoinReponse, body);
- }
- else
- {
- //prompt = $"Réponds uniquement par oui ou non. Tous les points de ce mail ont-ils été traités\nDébut du mail:\n{body} \n- fin du mail";
- prompt = PromptService.GetPrompt(PromptService.ePrompt.EmailSendService_AnalyserMail_AllPointsChecked, body);
- }
-
- var (reponseOllama, m) = await _ReActAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.AnalyseMails, false, prompt, "Analyse mails sortants");
-
- if(reponseOllama == null || reponseOllama.ToString().Trim().Length ==0)
- {
- return (true, true);
- }
- if (direction == "out")
- {
- var rep = (reponseOllama.ToString().ToLower().Contains("oui"));
- return (rep, false);
- }
- else
- {
- var rep = (reponseOllama.ToString().ToLower().Contains("oui"));
- return (false, rep);
- }
- }
- catch
- {
- return (true, true);
- }
- }
-
- private static async Task<string> GenerateFollowUp(ThreadItem thread)
- {
- var conversation = string.Join("\n\n", thread.Messages.ConvertAll(m => $"[{m.Direction}] {m.Body}"));
- //var prompt = $"Génère un brouillon de mail en langue française de relance poli basé sur cette conversation :\n{conversation}";
- var prompt = PromptService.GetPrompt(PromptService.ePrompt.EmailSendService_AnalyserMail_Relance, thread.Subject, conversation, ParametreUser.UserNomPrenom, ParametreUser.UserRole, ParametreUser.UserEntreprise);
-
- var (reponseOllama, _) = await _ReActAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.AnalyseMails, false, prompt, "Génération mail de relance");
- return reponseOllama;
- }
- #endregion
- }
- #endregion
- }
|