package services import ( "fmt" "log" "strings" "sync" "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 { // Sin base no hay configuración que leer: consultarla igual revienta con un // nil, y como el envío corre en su propia goroutine se lleva puesto todo el // proceso. Es lo que hacía que la suite de este paquete no se pudiera // correr entera. if app.Http.Database.DB == nil { return fmt.Errorf("no hay base de datos para leer la configuración de correo") } 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 } if err := EnviarPorSMTP(DatosSMTP{ Host: cfg.SmtpHost, Puerto: port, Encriptado: cfg.SmtpEncryption, Usuario: cfg.SmtpUsername, Password: cfg.SmtpPassword, Desde: from, DesdeNombre: fromName, }, to, subject, htmlBody); err != nil { return fmt.Errorf("SMTP de soporte (%s:%d): %w", cfg.SmtpHost, port, err) } return nil } // ProbarEnvioSoporte manda un correo de prueba por el mismo camino que usa el // acuse automático, y devuelve el error tal cual. Es la única forma de saber // por qué no llega: el acuse real se manda en segundo plano. func ProbarEnvioSoporte(destino string) error { if strings.TrimSpace(destino) == "" { return fmt.Errorf("indicá a qué dirección mandar la prueba") } cuerpo := `

Esto es una prueba del envío de soporte.

Si te llegó, el acuse automático de los tickets también va a salir por acá.

` return soporteSendMail(destino, "Prueba de envío de soporte", cuerpo) } // SendSoporteAutoRespuesta envía acuse de recibo automático al crear un ticket por email func SendSoporteAutoRespuesta(ticket *models.ProyectoTicket) { if ticket == nil || ticket.EmailFrom == "" { return } subject := fmt.Sprintf("[Ticket #%d] Recibimos tu solicitud: %s", ticket.ID, ticket.Titulo) mensaje := "Hemos recibido tu solicitud y te responderemos a la brevedad." if app.Http.Database.DB != nil { if cfg, err := models.GetSoporteWebhookActivo(); err == nil && cfg.MensajeAuto != "" { mensaje = cfg.MensajeAuto } } htmlBody := fmt.Sprintf(`
U

Ticket recibido

Soporte U-site

Hola %s,

%s

Ticket: #%d

Asunto: %s

Mensaje: %s

Este es un mensaje automático. No respondas a este correo.

`, ticket.AutorNombre, mensaje, ticket.ID, ticket.Titulo, ticket.Descripcion) go func() { if err := soporteSendMail(ticket.EmailFrom, subject, htmlBody); err != nil { log.Printf("[Soporte] Error enviando auto-respuesta a %s: %v", ticket.EmailFrom, err) guardarErrorAcuse(fmt.Sprintf("el acuse al cliente %s no salió: %v", ticket.EmailFrom, err)) } else { log.Printf("[Soporte] Auto-respuesta enviada a %s (ticket #%d)", ticket.EmailFrom, ticket.ID) guardarErrorAcuse("") } }() } // SendSoporteNotifAdmin notifica a los admins sobre un nuevo ticket de email func SendSoporteNotifAdmin(ticket *models.ProyectoTicket) { if ticket == nil { return } adminEmail := "" if app.Http.Database.DB != nil { if cfg, err := models.GetSmtpConfig(); err == nil { adminEmail = cfg.FromAddress } } if adminEmail == "" { return } subject := fmt.Sprintf("🎫 Nuevo ticket por email: %s", ticket.Titulo) htmlBody := fmt.Sprintf(`
🎫

Nuevo ticket por email

De: %s <%s>

Asunto: %s

Mensaje: %s

Ver ticket
`, ticket.AutorNombre, ticket.EmailFrom, ticket.Titulo, ticket.Descripcion, absAppURL(""), ticket.ID) go func() { if err := soporteSendMail(adminEmail, subject, htmlBody); err != nil { log.Printf("[Soporte] Error notificando admin: %v", err) } }() } // El acuse se manda en segundo plano, así que su error no puede devolverse al // que creó el ticket. Se guarda acá para poder mostrarlo en la pantalla de // configuración, que es donde alguien lo va a ver. var ( muErrorAcuse sync.Mutex ultimoErrAcuse string ) func guardarErrorAcuse(msg string) { muErrorAcuse.Lock() defer muErrorAcuse.Unlock() ultimoErrAcuse = msg } // UltimoErrorAcuse devuelve el último fallo al mandarle el acuse a un cliente // ("" si el último salió bien). func UltimoErrorAcuse() string { muErrorAcuse.Lock() defer muErrorAcuse.Unlock() return ultimoErrAcuse }