using EmailMicroservice.Models; using EmailMicroservice.Protos; using Grpc.Core; using Microsoft.Extensions.Options; using System.Net.Http.Headers; using System.Text; using System.Xml.Linq; using GrpcEmailAttachment = EmailMicroservice.Protos.EmailAttachment; using GrpcEmailMessage = EmailMicroservice.Protos.EmailMessage; namespace EmailMicroservice.Services { public class EwsMailService : Protos.ExchangeMailService.ExchangeMailServiceBase { private readonly MailSettings _mailSettings; private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; private static readonly XNamespace s = "http://schemas.xmlsoap.org/soap/envelope/"; private static readonly XNamespace t = "http://schemas.microsoft.com/exchange/services/2006/types"; public EwsMailService(IOptions mailSettings, ILogger logger, IHttpClientFactory httpClientFactory) { _mailSettings = mailSettings.Value; _logger = logger; _httpClientFactory = httpClientFactory; } public override async Task SendMail(SendMailRequest request, ServerCallContext context) { // Validar destinatarios if (request.Email == null || !request.Email.To.Any()) { _logger.LogWarning("Se intentó enviar un correo sin destinatarios."); throw new RpcException(new Status(StatusCode.InvalidArgument, "La lista de destinatarios 'to' no puede estar vacía.")); } var client = _httpClientFactory.CreateClient(); client.DefaultRequestHeaders.Authorization = GetBasicAuthHeader(); // 1. Construir XML de Adjuntos (Corrección: Namespace 't' en lugar de 'm') var attachmentsXml = new StringBuilder(); if (request.Email.Attachments.Any()) { // CORRECCIÓN: Usar en lugar de attachmentsXml.Append(""); foreach (var attachment in request.Email.Attachments) { attachmentsXml.Append($@"{attachment.Filename}{Convert.ToBase64String(attachment.Content.ToByteArray())}"); } attachmentsXml.Append(""); } // 2. Construir XML de Destinatarios (Lógica más limpia para evitar etiquetas vacías) var toRecipientsXml = $"{string.Join("", request.Email.To.Select(r => $"{r}"))}"; var ccRecipientsXml = ""; if (request.Email.Cc.Any()) { ccRecipientsXml = $"{string.Join("", request.Email.Cc.Select(r => $"{r}"))}"; } // Detectamos HTML si hay etiquetas de cierre comunes o si empieza con < bool isHtml = request.Email.Body.Trim().StartsWith("<") || request.Email.Body.Contains(" Body -> Attachments -> ToRecipients -> CcRecipients var soapRequest = $@" {request.Email.Subject} {new XCData(request.Email.Body)} {attachmentsXml} {toRecipientsXml} {ccRecipientsXml} "; _logger.LogDebug("Enviando solicitud SOAP a EWS (SendMail): {SoapRequest}", soapRequest); await PostSoapRequestAsync(client, soapRequest); return new SendMailResponse { Success = true, Message = "Correo enviado con éxito vía HttpClient." }; } public override async Task ReadInbox(ReadInboxRequest request, ServerCallContext context) { var client = _httpClientFactory.CreateClient(); client.DefaultRequestHeaders.Authorization = GetBasicAuthHeader(); var response = new ReadInboxResponse(); // PASO 1: Ejecutar FindItem para obtener los IDs y datos básicos de los correos. var findItemSoapRequest = BuildFindItemSoapRequest(request); var findItemResponseString = await PostSoapRequestAsync(client, findItemSoapRequest); var findItemDoc = XDocument.Parse(findItemResponseString); var itemNodes = findItemDoc.Descendants(t + "Message"); var emails = new List(); foreach (var itemNode in itemNodes) { var email = new GrpcEmailMessage { Subject = itemNode.Element(t + "Subject")?.Value ?? "", From = itemNode.Element(t + "From")?.Element(t + "Mailbox")?.Element(t + "EmailAddress")?.Value ?? "", Id = itemNode.Element(t + "ItemId")?.Attribute("Id")?.Value ?? "", Body = "" }; emails.Add(email); } response.TotalCount = itemNodes.Count(); if (emails.Any()) { _logger.LogInformation("Obteniendo cuerpo y detalles para {Count} correos...", emails.Count); // Usamos concurrencia para que sea rápido (varias peticiones a la vez) var detailTasks = emails.Select(async email => { // Llamamos a tu método helper que hace el 'GetItem' SOAP var detailedEmail = await GetItemDetailsAsync(client, email.Id); if (detailedEmail == null) { _logger.LogWarning($"No se pudieron obtener detalles para el item {email.Id}"); return; // Saltamos este ciclo y seguimos con el siguiente correo } email.Body = detailedEmail.Body; email.To.AddRange(detailedEmail.To); email.Cc.AddRange(detailedEmail.Cc); if (request.IncludeAttachments) { email.Attachments.AddRange(detailedEmail.Attachments); } }); await Task.WhenAll(detailTasks); } response.Emails.AddRange(emails); return response; } private async Task GetItemDetailsAsync(HttpClient client, string itemId) { // Construimos el SOAP Request pidiendo explícitamente TO y CC var soapRequest = $@" Default Text "; var responseString = await PostSoapRequestAsync(client, soapRequest); var xdoc = XDocument.Parse(responseString); var messageNode = xdoc.Descendants(t + "Message").FirstOrDefault(); if (messageNode == null) return null; var emailResult = new GrpcEmailMessage { Id = itemId, Subject = messageNode.Element(t + "Subject")?.Value ?? "", Body = messageNode.Element(t + "Body")?.Value ?? "", From = messageNode.Element(t + "From")?.Element(t + "Mailbox")?.Element(t + "EmailAddress")?.Value ?? "" }; // --- LOGICA DE PARSEO PARA TO (PARA) --- var toRecipientsNode = messageNode.Element(t + "ToRecipients"); if (toRecipientsNode != null) { var tos = toRecipientsNode.Descendants(t + "EmailAddress").Select(x => x.Value).Where(x => !string.IsNullOrEmpty(x)); emailResult.To.AddRange(tos); } // --- LOGICA DE PARSEO PARA CC (COPIA) --- var ccRecipientsNode = messageNode.Element(t + "CcRecipients"); if (ccRecipientsNode != null) { var ccs = ccRecipientsNode.Descendants(t + "EmailAddress").Select(x => x.Value).Where(x => !string.IsNullOrEmpty(x)); emailResult.Cc.AddRange(ccs); } // --- LOGICA DE ADJUNTOS --- var attachmentsNode = messageNode.Element(t + "Attachments"); if (attachmentsNode != null) { // Procesamos FileAttachments // DEFINICIÓN CLAVE: "Task" permite retornar null dentro de la lambda var attachmentTasks = attachmentsNode.Elements(t + "FileAttachment") .Select(async Task (fileAttachment) => { var idAttribute = fileAttachment.Element(t + "AttachmentId")?.Attribute("Id"); // Validación segura: si no hay ID, retornamos null if (idAttribute == null || string.IsNullOrEmpty(idAttribute.Value)) return null; string attachmentId = idAttribute.Value; // Descargar contenido string contentBase64 = await GetAttachmentContentAsync(client, attachmentId); return new GrpcEmailAttachment { Filename = fileAttachment.Element(t + "Name")?.Value ?? "adjunto", MimeType = fileAttachment.Element(t + "ContentType")?.Value ?? "application/octet-stream", Content = Google.Protobuf.ByteString.FromBase64(contentBase64) }; }); // Esperamos a que terminen todas las descargas var attachmentsResult = await Task.WhenAll(attachmentTasks); // FILTRADO FINAL: // 1. Where(a => a != null): Quitamos los nulos. // 2. Select(a => a!): El signo '!' le jura al compilador que el valor ya no es nulo. emailResult.Attachments.AddRange(attachmentsResult.Where(a => a != null).Select(a => a!)); } return emailResult; } private async Task GetAttachmentContentAsync(HttpClient client, string attachmentId) { var soapRequest = $@""; var responseString = await PostSoapRequestAsync(client, soapRequest); var xdoc = XDocument.Parse(responseString); return xdoc.Descendants(t + "Content").FirstOrDefault()?.Value ?? ""; } private string BuildFindItemSoapRequest(ReadInboxRequest request) { var filters = new StringBuilder(); var filterList = new List(); if (request.UnreadOnly) { filterList.Add(@""); } if (!string.IsNullOrEmpty(request.FilterSubject)) { filterList.Add($@""); } if (!string.IsNullOrEmpty(request.FilterFrom)) { filterList.Add($@""); } if (filterList.Any()) { filters.Append(""); if (filterList.Count > 1) { filters.Append(""); filters.Append(string.Join("", filterList)); filters.Append(""); } else { filters.Append(filterList.First()); } filters.Append(""); } int maxResults = request.MaxResults > 0 ? request.MaxResults : 10; return $@" IdOnly {filters} "; } private AuthenticationHeaderValue GetBasicAuthHeader() { var emailAddress = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("EMAIL_ADDRESS")) ? _mailSettings.EmailAddress : Environment.GetEnvironmentVariable("EMAIL_ADDRESS"); var pass = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("EMAIL_PASS")) ? _mailSettings.Password : Environment.GetEnvironmentVariable("EMAIL_PASS"); var byteArray = Encoding.ASCII.GetBytes($"{emailAddress}:{pass}"); return new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); } private async Task PostSoapRequestAsync(HttpClient client, string soapRequest) { var content = new StringContent(soapRequest, Encoding.UTF8, "text/xml"); var response = await client.PostAsync(_mailSettings.EwsUrl, content); var responseString = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { _logger.LogError("Error del servidor EWS: {StatusCode}. Respuesta: {ResponseBody}", response.StatusCode, responseString); string detail = TryParseSoapFault(responseString); throw new RpcException(new Status(StatusCode.Internal, $"Error del servidor EWS: {response.StatusCode}. Detalle: {detail}")); } return responseString; } private string TryParseSoapFault(string xml) { try { var xdoc = XDocument.Parse(xml); var faultString = xdoc.Descendants(s + "faultstring").FirstOrDefault(); if (faultString != null) { return faultString.Value; } } catch { } return "No se pudo analizar la respuesta de error."; } } }