From 03b18418eb6ef0f43342cae0818d1a4fe385e8ff Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:19:56 -0500 Subject: [PATCH] feat: SMTP salida propio en webhook de soporte --- pkg/models/soporte_webhook.go | 9 +++ pkg/models/soporte_webhook_test.go | 11 ++++ pkg/services/soporte_service.go | 86 +++++++++++++++++++++++++- resources/views/soporte_webhook.html | 71 ++++++++++++++++++++- rest/controllers/soporte_controller.go | 36 ++++++++--- 5 files changed, 203 insertions(+), 10 deletions(-) diff --git a/pkg/models/soporte_webhook.go b/pkg/models/soporte_webhook.go index a5fcc68..a83ea77 100644 --- a/pkg/models/soporte_webhook.go +++ b/pkg/models/soporte_webhook.go @@ -15,6 +15,15 @@ type SoporteWebhookConfig struct { ResponderAuto bool `json:"responder_auto" gorm:"column:responder_auto;default:true"` MensajeAuto string `json:"mensaje_auto" gorm:"column:mensaje_auto;type:text"` AsignarA *uint `json:"asignar_a" gorm:"column:asignar_a;index"` // auto-asignar tickets a este user + + // SMTP salida para notificaciones y auto-respuesta + SmtpHost string `json:"smtp_host" gorm:"column:smtp_host;size:255"` + SmtpPort int `json:"smtp_port" gorm:"column:smtp_port;default:587"` + SmtpUsername string `json:"smtp_username" gorm:"column:smtp_username;size:255"` + SmtpPassword string `json:"smtp_password" gorm:"column:smtp_password;size:255"` // cifrado AES + SmtpEncryption string `json:"smtp_encryption" gorm:"column:smtp_encryption;size:20;default:'tls'"` // tls|starttls|none + SmtpFromAddr string `json:"smtp_from_addr" gorm:"column:smtp_from_addr;size:255"` + SmtpFromName string `json:"smtp_from_name" gorm:"column:smtp_from_name;size:255"` } func (SoporteWebhookConfig) TableName() string { return "soporte_webhook_config" } diff --git a/pkg/models/soporte_webhook_test.go b/pkg/models/soporte_webhook_test.go index b2687b6..b2d43b4 100644 --- a/pkg/models/soporte_webhook_test.go +++ b/pkg/models/soporte_webhook_test.go @@ -56,3 +56,14 @@ func TestSoporteWebhookConfigAsignarANil(t *testing.T) { t.Errorf("AsignarA debe ser nil por defecto, got %v", *cfg.AsignarA) } } + +func TestSoporteWebhookConfigSmtpDefaults(t *testing.T) { + cfg := SoporteWebhookConfig{Nombre: "SMTP Test"} + // Go zero values — los defaults reales los aplica GORM o el controller + if cfg.SmtpHost != "" { + t.Errorf("SmtpHost debe ser vacío por defecto, got %q", cfg.SmtpHost) + } + if cfg.SmtpPort != 0 { + t.Errorf("SmtpPort debe ser 0 en Go, got %d", cfg.SmtpPort) + } +} diff --git a/pkg/services/soporte_service.go b/pkg/services/soporte_service.go index d3bd4a6..a43c690 100644 --- a/pkg/services/soporte_service.go +++ b/pkg/services/soporte_service.go @@ -1,13 +1,95 @@ package services import ( + "crypto/tls" "fmt" "log" + "net/smtp" + "strings" "github.com/sujit-baniya/fiber-boilerplate/app" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) +// soporteSendMail envía un correo usando la configuración SMTP del webhook de soporte. +// Si no hay SMTP configurado, usa app.Http.Mail como fallback. +func soporteSendMail(to, subject, htmlBody string) error { + cfg, err := models.GetSoporteWebhookActivo() + if err != nil || cfg == nil || cfg.SmtpHost == "" { + return app.Http.Mail.Send(to, subject, htmlBody) + } + from := cfg.SmtpFromAddr + if from == "" { + from = cfg.EmailDestino + } + if from == "" { + return app.Http.Mail.Send(to, subject, htmlBody) + } + fromName := cfg.SmtpFromName + if fromName == "" { + fromName = "Soporte" + } + port := cfg.SmtpPort + if port == 0 { + port = 587 + } + auth := smtp.PlainAuth("", cfg.SmtpUsername, cfg.SmtpPassword, cfg.SmtpHost) + msg := []byte(fmt.Sprintf("From: %s <%s>\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n%s", fromName, from, to, subject, htmlBody)) + addr := fmt.Sprintf("%s:%d", cfg.SmtpHost, port) + enc := strings.ToLower(cfg.SmtpEncryption) + if enc == "tls" { + tlsCfg := &tls.Config{ServerName: cfg.SmtpHost} + conn, err := tls.Dial("tcp", addr, tlsCfg) + if err != nil { + return fmt.Errorf("soporte SMTP TLS: %w", err) + } + client, err := smtp.NewClient(conn, cfg.SmtpHost) + if err != nil { + conn.Close() + return fmt.Errorf("soporte SMTP client: %w", err) + } + defer client.Close() + if err = client.Auth(auth); err != nil { + return fmt.Errorf("soporte SMTP auth: %w", err) + } + if err = client.Mail(from); err != nil { + return err + } + if err = client.Rcpt(to); err != nil { + return err + } + w, err := client.Data() + if err != nil { + return err + } + _, err = w.Write(msg) + if err != nil { + return err + } + return w.Close() + } + if enc == "starttls" { + tlsCfg := &tls.Config{ServerName: cfg.SmtpHost} + conn, err := smtp.Dial(addr) + if err != nil { + goto fallback + } + if err = conn.StartTLS(tlsCfg); err != nil { + conn.Close() + goto fallback + } + if err = conn.Auth(auth); err != nil { + conn.Close() + return fmt.Errorf("soporte SMTP STARTTLS auth: %w", err) + } + return smtp.SendMail(addr, auth, from, []string{to}, msg) + } + return smtp.SendMail(addr, auth, from, []string{to}, msg) + +fallback: + return app.Http.Mail.Send(to, subject, htmlBody) +} + // SendSoporteAutoRespuesta envía acuse de recibo automático al crear un ticket por email func SendSoporteAutoRespuesta(ticket *models.ProyectoTicket) { if ticket == nil || ticket.EmailFrom == "" { @@ -39,7 +121,7 @@ func SendSoporteAutoRespuesta(ticket *models.ProyectoTicket) { `, ticket.AutorNombre, mensaje, ticket.ID, ticket.Titulo, ticket.Descripcion) go func() { - if err := app.Http.Mail.Send(ticket.EmailFrom, subject, htmlBody); err != nil { + if err := soporteSendMail(ticket.EmailFrom, subject, htmlBody); err != nil { log.Printf("[Soporte] Error enviando auto-respuesta a %s: %v", ticket.EmailFrom, err) } else { log.Printf("[Soporte] Auto-respuesta enviada a %s (ticket #%d)", ticket.EmailFrom, ticket.ID) @@ -78,7 +160,7 @@ func SendSoporteNotifAdmin(ticket *models.ProyectoTicket) { `, ticket.AutorNombre, ticket.EmailFrom, ticket.Titulo, ticket.Descripcion, absAppURL(""), ticket.ID) go func() { - if err := app.Http.Mail.Send(adminEmail, subject, htmlBody); err != nil { + if err := soporteSendMail(adminEmail, subject, htmlBody); err != nil { log.Printf("[Soporte] Error notificando admin: %v", err) } }() diff --git a/resources/views/soporte_webhook.html b/resources/views/soporte_webhook.html index 8851dc8..c0a7887 100644 --- a/resources/views/soporte_webhook.html +++ b/resources/views/soporte_webhook.html @@ -49,7 +49,62 @@

Los tickets creados por email se asignarán automáticamente a este admin.

-
+ +
+

SMTP salida (notificaciones y auto-respuesta)

+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +

Si se deja vacío, se usará la configuración SMTP general del sistema.

+
+ +