Feat: Implementa auditoría de envíos de email y mejora la UX
Se introduce un sistema completo de logging para todas las comunicaciones por correo electrónico y se realizan mejoras significativas en la experiencia del usuario, tanto en la retroalimentación del sistema como en la estética de los emails enviados al cliente. ### ✨ Nuevas Características - **Auditoría y Log de Envíos de Email:** - Se ha creado una nueva tabla `com_EmailLogs` en la base de datos para registrar cada intento de envío de correo. - El `EmailService` ahora centraliza toda la lógica de logging, registrando automáticamente la fecha, destinatario, asunto, estado (`Enviado` o `Fallido`), y mensajes de error detallados. - Se implementó un nuevo `EmailLogService` y `EmailLogRepository` para gestionar estos registros. - **Historial de Envíos en la Interfaz de Usuario:** - Se añade un nuevo ícono de "Historial" (<span style="color: #607d8b;">📧</span>) junto a cada factura en la página de "Consulta de Facturas". - Al hacer clic, se abre un modal que muestra una tabla detallada con todos los intentos de envío para esa factura, incluyendo el estado y el motivo del error (si lo hubo). - Esto proporciona una trazabilidad completa y una herramienta de diagnóstico para el usuario final. ### 🔄 Refactorización y Mejoras - **Mensajes de Éxito Dinámicos:** - Se ha mejorado la retroalimentación al enviar una factura por PDF. El sistema ahora muestra un mensaje de éxito específico, como "El email... se ha enviado correctamente a suscriptor@email.com", en lugar de un mensaje técnico genérico. - Se ajustó la cadena de llamadas (`Controller` -> `Service`) para que el email del destinatario esté disponible para la respuesta de la API. - **Diseño Unificado de Emails:** - Se ha rediseñado el template HTML para el "Aviso de Cuenta Mensual" para que coincida con la estética del email de "Envío de Factura PDF". - Ambos correos ahora presentan un diseño profesional y consistente, con cabecera, logo y pie de página, reforzando la imagen de marca. - **Manejo de Errores de Email Mejorado:** - El `EmailService` ahora captura excepciones específicas de la librería `MailKit` (ej. `SmtpCommandException`). - Esto permite registrar en el log errores mucho más precisos y útiles, como rechazos de destinatarios por parte del servidor (`User unknown`), fallos de autenticación, etc., que ahora son visibles en el `Tooltip` del historial.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using GestionIntegral.Api.Data.Repositories.Comunicaciones;
|
||||
using GestionIntegral.Api.Models.Comunicaciones;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
@@ -10,14 +11,22 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
{
|
||||
private readonly MailSettings _mailSettings;
|
||||
private readonly ILogger<EmailService> _logger;
|
||||
private readonly IEmailLogRepository _emailLogRepository;
|
||||
|
||||
public EmailService(IOptions<MailSettings> mailSettings, ILogger<EmailService> logger)
|
||||
public EmailService(
|
||||
IOptions<MailSettings> mailSettings,
|
||||
ILogger<EmailService> logger,
|
||||
IEmailLogRepository emailLogRepository) // Inyectar el nuevo repositorio
|
||||
{
|
||||
_mailSettings = mailSettings.Value;
|
||||
_logger = logger;
|
||||
_emailLogRepository = emailLogRepository;
|
||||
}
|
||||
|
||||
public async Task EnviarEmailAsync(string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml, byte[]? attachment = null, string? attachmentName = null)
|
||||
public async Task EnviarEmailAsync(
|
||||
string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml,
|
||||
byte[]? attachment = null, string? attachmentName = null,
|
||||
string? origen = null, string? referenciaId = null, int? idUsuarioDisparo = null)
|
||||
{
|
||||
var email = new MimeMessage();
|
||||
email.Sender = new MailboxAddress(_mailSettings.SenderName, _mailSettings.SenderEmail);
|
||||
@@ -32,26 +41,14 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
}
|
||||
email.Body = builder.ToMessageBody();
|
||||
|
||||
using var smtp = new SmtpClient();
|
||||
try
|
||||
{
|
||||
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
|
||||
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
|
||||
await smtp.SendAsync(email);
|
||||
_logger.LogInformation("Email enviado exitosamente a {Destinatario}", destinatarioEmail);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al enviar email a {Destinatario}", destinatarioEmail);
|
||||
throw; // Relanzar para que el servicio que lo llamó sepa que falló
|
||||
}
|
||||
finally
|
||||
{
|
||||
await smtp.DisconnectAsync(true);
|
||||
}
|
||||
// Llamar al método centralizado de envío y logging
|
||||
await SendAndLogEmailAsync(email, origen, referenciaId, idUsuarioDisparo);
|
||||
}
|
||||
|
||||
public async Task EnviarEmailConsolidadoAsync(string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml, List<(byte[] content, string name)> adjuntos)
|
||||
public async Task EnviarEmailConsolidadoAsync(
|
||||
string destinatarioEmail, string destinatarioNombre, string asunto, string cuerpoHtml,
|
||||
List<(byte[] content, string name)> adjuntos,
|
||||
string? origen = null, string? referenciaId = null, int? idUsuarioDisparo = null)
|
||||
{
|
||||
var email = new MimeMessage();
|
||||
email.Sender = new MailboxAddress(_mailSettings.SenderName, _mailSettings.SenderEmail);
|
||||
@@ -60,7 +57,6 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
email.Subject = asunto;
|
||||
|
||||
var builder = new BodyBuilder { HtmlBody = cuerpoHtml };
|
||||
|
||||
if (adjuntos != null)
|
||||
{
|
||||
foreach (var adjunto in adjuntos)
|
||||
@@ -68,25 +64,80 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
builder.Attachments.Add(adjunto.name, adjunto.content, ContentType.Parse("application/pdf"));
|
||||
}
|
||||
}
|
||||
|
||||
email.Body = builder.ToMessageBody();
|
||||
|
||||
// Llamar al método centralizado de envío y logging
|
||||
await SendAndLogEmailAsync(email, origen, referenciaId, idUsuarioDisparo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Método privado que centraliza el envío de correo y el registro de logs.
|
||||
/// </summary>
|
||||
private async Task SendAndLogEmailAsync(MimeMessage emailMessage, string? origen, string? referenciaId, int? idUsuarioDisparo)
|
||||
{
|
||||
var destinatario = emailMessage.To.Mailboxes.FirstOrDefault()?.Address ?? "desconocido";
|
||||
|
||||
var log = new EmailLog
|
||||
{
|
||||
FechaEnvio = DateTime.Now,
|
||||
DestinatarioEmail = destinatario,
|
||||
Asunto = emailMessage.Subject,
|
||||
Origen = origen,
|
||||
ReferenciaId = referenciaId,
|
||||
IdUsuarioDisparo = idUsuarioDisparo
|
||||
};
|
||||
|
||||
using var smtp = new SmtpClient();
|
||||
try
|
||||
{
|
||||
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
|
||||
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
|
||||
await smtp.SendAsync(email);
|
||||
_logger.LogInformation("Email consolidado enviado exitosamente a {Destinatario}", destinatarioEmail);
|
||||
await smtp.SendAsync(emailMessage);
|
||||
|
||||
log.Estado = "Enviado";
|
||||
_logger.LogInformation("Email enviado exitosamente a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
||||
}
|
||||
catch (Exception ex)
|
||||
// Capturamos excepciones específicas de MailKit para obtener errores más detallados.
|
||||
catch (SmtpCommandException scEx)
|
||||
{
|
||||
_logger.LogError(ex, "Error al enviar email consolidado a {Destinatario}", destinatarioEmail);
|
||||
// Este error ocurre cuando el servidor SMTP rechaza un comando.
|
||||
// Es el caso más común para direcciones de email inválidas que son rechazadas inmediatamente.
|
||||
_logger.LogError(scEx, "Error de comando SMTP al enviar a {Destinatario}. StatusCode: {StatusCode}", destinatario, scEx.StatusCode);
|
||||
log.Estado = "Fallido";
|
||||
log.Error = $"Error del servidor: ({scEx.StatusCode}) {scEx.Message}";
|
||||
throw;
|
||||
}
|
||||
catch (AuthenticationException authEx)
|
||||
{
|
||||
// Error específico de autenticación.
|
||||
_logger.LogError(authEx, "Error de autenticación con el servidor SMTP.");
|
||||
log.Estado = "Fallido";
|
||||
log.Error = "Error de autenticación. Revise las credenciales de correo.";
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) // Captura genérica para cualquier otro problema (conexión, etc.)
|
||||
{
|
||||
_logger.LogError(ex, "Error general al enviar email a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
||||
log.Estado = "Fallido";
|
||||
log.Error = ex.Message;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await smtp.DisconnectAsync(true);
|
||||
if (smtp.IsConnected)
|
||||
{
|
||||
await smtp.DisconnectAsync(true);
|
||||
}
|
||||
|
||||
// Guardar el log en la base de datos, sin importar el resultado del envío
|
||||
try
|
||||
{
|
||||
await _emailLogRepository.CreateAsync(log);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
_logger.LogError(logEx, "FALLO CRÍTICO: No se pudo guardar el log del email para {Destinatario}", destinatario);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user