fix(soporte): el buzón IMAP nunca se leía — la búsqueda pedía UIDs a una búsqueda por secuencia
Usé c.Search, que devuelve números de secuencia, y después le pedí los UIDs al resultado. go-imap devuelve nil ahí, así que la lista salía siempre vacía: el cron conectaba bien, buscaba bien, y se iba sin leer nada ni escribir un solo error en el log. Va con UIDSearch. Con eso se arreglan las dos cosas: no leía y no creaba tickets. Además: - Fetch con Peek: el correo queda marcado como leído solo si se llegó a procesar. Antes, un fallo a mitad de camino lo perdía para siempre. - "Probar conexión" ahora informa cuántos mensajes hay en la carpeta y cuántos sin leer; "Revisar buzón ahora" informa cuántos encontró y cuántos convirtió en ticket, y avisa si la lectura está desactivada. - Un correo sin asunto abre ticket como "(sin asunto)" en vez de descartarse en silencio, y los descartes quedan en el log. Los tres tests nuevos corren contra un servidor IMAP en memoria: bajar los no leídos, que marcar como leído los saque de la próxima corrida, y que bajarlos sin procesarlos no los marque. El primero falla si se vuelve a poner Search. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d1bbb4a508
commit
5875f9e36b
@@ -44,6 +44,7 @@ require (
|
|||||||
github.com/chai2010/webp v1.4.0
|
github.com/chai2010/webp v1.4.0
|
||||||
github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe
|
github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe
|
||||||
github.com/chromedp/chromedp v0.16.0
|
github.com/chromedp/chromedp v0.16.0
|
||||||
|
github.com/emersion/go-imap/v2 v2.0.0-beta.8
|
||||||
github.com/go-sql-driver/mysql v1.8.1
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
@@ -55,6 +56,7 @@ require (
|
|||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
|
github.com/valyala/fasthttp v1.56.0
|
||||||
go.mongodb.org/mongo-driver v1.17.9
|
go.mongodb.org/mongo-driver v1.17.9
|
||||||
golang.org/x/net v0.53.0
|
golang.org/x/net v0.53.0
|
||||||
golang.org/x/oauth2 v0.23.0
|
golang.org/x/oauth2 v0.23.0
|
||||||
@@ -75,7 +77,6 @@ require (
|
|||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/emersion/go-imap/v2 v2.0.0-beta.8 // indirect
|
|
||||||
github.com/emersion/go-message v0.18.2 // indirect
|
github.com/emersion/go-message v0.18.2 // indirect
|
||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
||||||
@@ -137,7 +138,6 @@ require (
|
|||||||
github.com/tinylib/msgp v1.6.1 // indirect
|
github.com/tinylib/msgp v1.6.1 // indirect
|
||||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 // indirect
|
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 // indirect
|
||||||
github.com/valyala/fasthttp v1.56.0 // indirect
|
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/imapclient"
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver/imapmemserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// servidorDePrueba levanta un IMAP en memoria con un correo sin leer en INBOX.
|
||||||
|
func servidorDePrueba(t *testing.T, mensajes ...string) (*imapclient.Client, io.Closer) {
|
||||||
|
t.Helper()
|
||||||
|
mem := imapmemserver.New()
|
||||||
|
user := imapmemserver.NewUser("soporte", "secreta")
|
||||||
|
if err := user.Create("INBOX", nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mem.AddUser(user)
|
||||||
|
|
||||||
|
srv := imapserver.New(&imapserver.Options{
|
||||||
|
NewSession: func(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
|
||||||
|
return mem.NewSession(), nil, nil
|
||||||
|
},
|
||||||
|
InsecureAuth: true,
|
||||||
|
})
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go srv.Serve(ln)
|
||||||
|
|
||||||
|
c, err := imapclient.DialInsecure(ln.Addr().String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := c.Login("soporte", "secreta").Wait(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, m := range mensajes {
|
||||||
|
ac := c.Append("INBOX", int64(len(m)), nil)
|
||||||
|
if _, err := ac.Write([]byte(m)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ac.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := ac.Wait(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c, ln
|
||||||
|
}
|
||||||
|
|
||||||
|
const correoCrudo = "From: Juan <juan@cliente.com>\r\n" +
|
||||||
|
"Subject: No puedo entrar\r\n" +
|
||||||
|
"Message-Id: <uno@cliente.com>\r\n" +
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"La contrasena no me sirve.\r\n"
|
||||||
|
|
||||||
|
// Este test existe por un bug concreto: la búsqueda usaba Search (números de
|
||||||
|
// secuencia) y después pedía UIDs, así que la lista salía siempre vacía y el
|
||||||
|
// buzón no se leía nunca, sin un solo error en el log.
|
||||||
|
func TestBajarNoLeidosDevuelveLosCorreos(t *testing.T) {
|
||||||
|
c, ln := servidorDePrueba(t, correoCrudo)
|
||||||
|
defer ln.Close()
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
correos, err := BajarNoLeidos(c, "INBOX")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BajarNoLeidos: %v", err)
|
||||||
|
}
|
||||||
|
if len(correos) != 1 {
|
||||||
|
t.Fatalf("esperaba 1 correo sin leer, hay %d", len(correos))
|
||||||
|
}
|
||||||
|
if correos[0].UID == 0 {
|
||||||
|
t.Error("el UID vino en cero: sin él no se puede marcar como leído")
|
||||||
|
}
|
||||||
|
if got := correos[0].Correo.Subject; got != "No puedo entrar" {
|
||||||
|
t.Errorf("Subject = %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(correos[0].Correo.Texto, "contrasena no me sirve") {
|
||||||
|
t.Errorf("Texto = %q", correos[0].Correo.Texto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y este, por el otro lado del mismo problema: si marcar como leído no funciona,
|
||||||
|
// el mismo correo abre un ticket nuevo cada 2 minutos para siempre.
|
||||||
|
func TestMarcarLeidoSacaElCorreoDeLaProximaCorrida(t *testing.T) {
|
||||||
|
c, ln := servidorDePrueba(t, correoCrudo)
|
||||||
|
defer ln.Close()
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
correos, err := BajarNoLeidos(c, "INBOX")
|
||||||
|
if err != nil || len(correos) != 1 {
|
||||||
|
t.Fatalf("BajarNoLeidos: %v (%d correos)", err, len(correos))
|
||||||
|
}
|
||||||
|
if err := MarcarLeido(c, correos[0].UID); err != nil {
|
||||||
|
t.Fatalf("MarcarLeido: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
otra, err := BajarNoLeidos(c, "INBOX")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("segunda pasada: %v", err)
|
||||||
|
}
|
||||||
|
if len(otra) != 0 {
|
||||||
|
t.Errorf("el correo ya leído volvió a aparecer (%d)", len(otra))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek: bajarlo sin procesarlo no lo debe marcar, o un fallo a mitad de camino
|
||||||
|
// perdería el correo para siempre.
|
||||||
|
func TestBajarNoLeidosNoMarcaSolo(t *testing.T) {
|
||||||
|
c, ln := servidorDePrueba(t, correoCrudo)
|
||||||
|
defer ln.Close()
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
if _, err := BajarNoLeidos(c, "INBOX"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
otra, err := BajarNoLeidos(c, "INBOX")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(otra) != 1 {
|
||||||
|
t.Errorf("el correo debería seguir sin leer, hay %d", len(otra))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = imap.FlagSeen
|
||||||
@@ -36,33 +36,60 @@ func RevisarBuzonSoporte() {
|
|||||||
}
|
}
|
||||||
defer imapEnCurso.Unlock()
|
defer imapEnCurso.Unlock()
|
||||||
|
|
||||||
n, err := revisarBuzon(cfg)
|
encontrados, procesados, err := revisarBuzon(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[SoporteIMAP] %v", err)
|
log.Printf("[SoporteIMAP] %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if encontrados > 0 {
|
||||||
log.Printf("[SoporteIMAP] %d correo(s) procesado(s)", n)
|
log.Printf("[SoporteIMAP] %d sin leer, %d procesado(s)", encontrados, procesados)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RevisarBuzonSoporteConDetalle es la versión que usa el botón "Revisar buzón
|
||||||
|
// ahora": devuelve los números para poder mostrarlos, en vez de solo loguearlos.
|
||||||
|
func RevisarBuzonSoporteConDetalle() (encontrados, procesados int, err error) {
|
||||||
|
cfg, err := models.GetSoporteWebhookActivo()
|
||||||
|
if err != nil || cfg == nil {
|
||||||
|
return 0, 0, fmt.Errorf("no hay configuración de soporte guardada")
|
||||||
|
}
|
||||||
|
if !cfg.ImapActivo {
|
||||||
|
return 0, 0, fmt.Errorf("la lectura del buzón está desactivada: tildá \"Leer el buzón cada 2 minutos\" y guardá")
|
||||||
|
}
|
||||||
|
if cfg.ImapHost == "" {
|
||||||
|
return 0, 0, fmt.Errorf("falta el servidor IMAP en la configuración")
|
||||||
|
}
|
||||||
|
if !imapEnCurso.TryLock() {
|
||||||
|
return 0, 0, fmt.Errorf("hay una revisión en curso, probá en unos segundos")
|
||||||
|
}
|
||||||
|
defer imapEnCurso.Unlock()
|
||||||
|
return revisarBuzon(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
// ProbarConexionImap valida credenciales sin procesar nada — lo usa el botón
|
// ProbarConexionImap valida credenciales sin procesar nada — lo usa el botón
|
||||||
// "Probar" de la vista de configuración.
|
// "Probar" de la vista de configuración.
|
||||||
func ProbarConexionImap(cfg *models.SoporteWebhookConfig) error {
|
func ProbarConexionImap(cfg *models.SoporteWebhookConfig) (string, error) {
|
||||||
c, err := conectarImap(cfg)
|
c, err := conectarImap(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
carpeta := cfg.ImapCarpeta
|
carpeta := cfg.ImapCarpeta
|
||||||
if carpeta == "" {
|
if carpeta == "" {
|
||||||
carpeta = "INBOX"
|
carpeta = "INBOX"
|
||||||
}
|
}
|
||||||
if _, err := c.Select(carpeta, &imap.SelectOptions{ReadOnly: true}).Wait(); err != nil {
|
datos, err := c.Select(carpeta, &imap.SelectOptions{ReadOnly: true}).Wait()
|
||||||
return fmt.Errorf("no se pudo abrir la carpeta %q: %w", carpeta, err)
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("no se pudo abrir la carpeta %q: %w", carpeta, err)
|
||||||
|
}
|
||||||
|
sinLeer := 0
|
||||||
|
if r, err := c.UIDSearch(&imap.SearchCriteria{
|
||||||
|
NotFlag: []imap.Flag{imap.FlagSeen},
|
||||||
|
}, &imap.SearchOptions{ReturnAll: true}).Wait(); err == nil {
|
||||||
|
sinLeer = len(r.AllUIDs())
|
||||||
}
|
}
|
||||||
_ = c.Logout().Wait()
|
_ = c.Logout().Wait()
|
||||||
return nil
|
return fmt.Sprintf("Conexión correcta. %s: %d mensajes, %d sin leer.", carpeta, datos.NumMessages, sinLeer), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func conectarImap(cfg *models.SoporteWebhookConfig) (*imapclient.Client, error) {
|
func conectarImap(cfg *models.SoporteWebhookConfig) (*imapclient.Client, error) {
|
||||||
@@ -92,30 +119,63 @@ func conectarImap(cfg *models.SoporteWebhookConfig) (*imapclient.Client, error)
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func revisarBuzon(cfg *models.SoporteWebhookConfig) (int, error) {
|
func revisarBuzon(cfg *models.SoporteWebhookConfig) (int, int, error) {
|
||||||
c, err := conectarImap(cfg)
|
c, err := conectarImap(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
carpeta := cfg.ImapCarpeta
|
correos, err := BajarNoLeidos(c, cfg.ImapCarpeta)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
if len(correos) == 0 {
|
||||||
|
return 0, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
procesados := 0
|
||||||
|
for _, correo := range correos {
|
||||||
|
if IngestarCorreoSoporte(cfg, correo.Correo) {
|
||||||
|
procesados++
|
||||||
|
}
|
||||||
|
// Se marca leído aunque se haya ignorado por duplicado: si no, se
|
||||||
|
// vuelve a bajar en cada corrida para siempre.
|
||||||
|
if err := MarcarLeido(c, correo.UID); err != nil {
|
||||||
|
log.Printf("[SoporteIMAP] no se pudo marcar leído el uid=%v: %v", correo.UID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(correos), procesados, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CorreoConUID es un correo del buzón junto con su UID, que es lo que hace
|
||||||
|
// falta después para marcarlo como leído.
|
||||||
|
type CorreoConUID struct {
|
||||||
|
UID imap.UID
|
||||||
|
Correo CorreoSoporte
|
||||||
|
}
|
||||||
|
|
||||||
|
// BajarNoLeidos abre la carpeta, busca los mensajes sin leer y los devuelve ya
|
||||||
|
// parseados. No los marca: eso pasa recién cuando se procesaron.
|
||||||
|
func BajarNoLeidos(c *imapclient.Client, carpeta string) ([]CorreoConUID, error) {
|
||||||
if carpeta == "" {
|
if carpeta == "" {
|
||||||
carpeta = "INBOX"
|
carpeta = "INBOX"
|
||||||
}
|
}
|
||||||
if _, err := c.Select(carpeta, nil).Wait(); err != nil {
|
if _, err := c.Select(carpeta, nil).Wait(); err != nil {
|
||||||
return 0, fmt.Errorf("no se pudo abrir la carpeta %q: %w", carpeta, err)
|
return nil, fmt.Errorf("no se pudo abrir la carpeta %q: %w", carpeta, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
buscados, err := c.Search(&imap.SearchCriteria{
|
// UIDSearch y no Search: Search devuelve números de secuencia, y de ahí no
|
||||||
|
// salen UIDs — la búsqueda "encontraba" mensajes y la lista quedaba vacía.
|
||||||
|
buscados, err := c.UIDSearch(&imap.SearchCriteria{
|
||||||
NotFlag: []imap.Flag{imap.FlagSeen},
|
NotFlag: []imap.Flag{imap.FlagSeen},
|
||||||
}, &imap.SearchOptions{ReturnAll: true}).Wait()
|
}, &imap.SearchOptions{ReturnAll: true}).Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("búsqueda de no leídos falló: %w", err)
|
return nil, fmt.Errorf("búsqueda de no leídos falló: %w", err)
|
||||||
}
|
}
|
||||||
uids := buscados.AllUIDs()
|
uids := buscados.AllUIDs()
|
||||||
if len(uids) == 0 {
|
if len(uids) == 0 {
|
||||||
return 0, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
// ponytail: tope por corrida para no atragantarse con un buzón que nunca
|
// ponytail: tope por corrida para no atragantarse con un buzón que nunca
|
||||||
// se leyó. Los que sobran quedan sin leer y entran en la corrida siguiente.
|
// se leyó. Los que sobran quedan sin leer y entran en la corrida siguiente.
|
||||||
@@ -124,14 +184,15 @@ func revisarBuzon(cfg *models.SoporteWebhookConfig) (int, error) {
|
|||||||
uids = uids[:maxPorCorrida]
|
uids = uids[:maxPorCorrida]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Peek para que el correo quede leído solo si llegamos a procesarlo.
|
||||||
msgs, err := c.Fetch(imap.UIDSetNum(uids...), &imap.FetchOptions{
|
msgs, err := c.Fetch(imap.UIDSetNum(uids...), &imap.FetchOptions{
|
||||||
BodySection: []*imap.FetchItemBodySection{{}},
|
BodySection: []*imap.FetchItemBodySection{{Peek: true}},
|
||||||
}).Collect()
|
}).Collect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("descarga de correos falló: %w", err)
|
return nil, fmt.Errorf("descarga de correos falló: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
procesados := 0
|
var out []CorreoConUID
|
||||||
for _, m := range msgs {
|
for _, m := range msgs {
|
||||||
var crudo []byte
|
var crudo []byte
|
||||||
for _, b := range m.BodySection {
|
for _, b := range m.BodySection {
|
||||||
@@ -146,20 +207,18 @@ func revisarBuzon(cfg *models.SoporteWebhookConfig) (int, error) {
|
|||||||
log.Printf("[SoporteIMAP] no se pudo leer un correo (uid=%v): %v", m.UID, err)
|
log.Printf("[SoporteIMAP] no se pudo leer un correo (uid=%v): %v", m.UID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if IngestarCorreoSoporte(cfg, correo) {
|
out = append(out, CorreoConUID{UID: m.UID, Correo: correo})
|
||||||
procesados++
|
|
||||||
}
|
|
||||||
// Se marca leído aunque se haya ignorado por duplicado: si no, se
|
|
||||||
// vuelve a bajar en cada corrida para siempre.
|
|
||||||
if err := c.Store(imap.UIDSetNum(m.UID), &imap.StoreFlags{
|
|
||||||
Op: imap.StoreFlagsAdd,
|
|
||||||
Silent: true,
|
|
||||||
Flags: []imap.Flag{imap.FlagSeen},
|
|
||||||
}, nil).Close(); err != nil {
|
|
||||||
log.Printf("[SoporteIMAP] no se pudo marcar leído el uid=%v: %v", m.UID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return procesados, nil
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarcarLeido pone el flag \Seen, que es lo que evita releer el mismo correo.
|
||||||
|
func MarcarLeido(c *imapclient.Client, uid imap.UID) error {
|
||||||
|
return c.Store(imap.UIDSetNum(uid), &imap.StoreFlags{
|
||||||
|
Op: imap.StoreFlagsAdd,
|
||||||
|
Silent: true,
|
||||||
|
Flags: []imap.Flag{imap.FlagSeen},
|
||||||
|
}, nil).Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsearCorreo saca remitente, asunto y cuerpo de texto de un mensaje RFC822.
|
// parsearCorreo saca remitente, asunto y cuerpo de texto de un mensaje RFC822.
|
||||||
|
|||||||
@@ -26,9 +26,15 @@ var ticketRefRe = regexp.MustCompile(`(?i)\[Ticket #(\d+)\]`)
|
|||||||
// ticket existente. Devuelve true si se procesó algo (false = ignorado por
|
// ticket existente. Devuelve true si se procesó algo (false = ignorado por
|
||||||
// duplicado o por venir vacío).
|
// duplicado o por venir vacío).
|
||||||
func IngestarCorreoSoporte(cfg *models.SoporteWebhookConfig, e CorreoSoporte) bool {
|
func IngestarCorreoSoporte(cfg *models.SoporteWebhookConfig, e CorreoSoporte) bool {
|
||||||
if e.From == "" || e.Subject == "" {
|
if strings.TrimSpace(e.From) == "" {
|
||||||
|
log.Printf("[Soporte] Correo sin remitente, ignorado (asunto=%q)", e.Subject)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
// Un correo sin asunto es raro pero existe, y perderlo en silencio es peor
|
||||||
|
// que abrir un ticket con un título feo.
|
||||||
|
if strings.TrimSpace(e.Subject) == "" {
|
||||||
|
e.Subject = "(sin asunto)"
|
||||||
|
}
|
||||||
|
|
||||||
// Deduplicación: el proveedor puede reintentar la entrega, y el poller IMAP
|
// Deduplicación: el proveedor puede reintentar la entrega, y el poller IMAP
|
||||||
// puede releer un correo si falló el marcado como leído.
|
// puede releer un correo si falló el marcado como leído.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -305,15 +306,23 @@ func ProbarImapSoporte(c *fiber.Ctx) error {
|
|||||||
if err != nil || cfg == nil {
|
if err != nil || cfg == nil {
|
||||||
return c.Status(400).JSON(fiber.Map{"error": "Guardá la configuración antes de probar"})
|
return c.Status(400).JSON(fiber.Map{"error": "Guardá la configuración antes de probar"})
|
||||||
}
|
}
|
||||||
if err := services.ProbarConexionImap(cfg); err != nil {
|
detalle, err := services.ProbarConexionImap(cfg)
|
||||||
|
if err != nil {
|
||||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return c.JSON(fiber.Map{"ok": true, "message": "Conexión IMAP correcta"})
|
return c.JSON(fiber.Map{"ok": true, "message": detalle})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RevisarBuzonAhora dispara una lectura del buzón sin esperar al cron.
|
// RevisarBuzonAhora dispara una lectura del buzón sin esperar al cron.
|
||||||
// POST /app/api/soporte-webhook/revisar-buzon
|
// POST /app/api/soporte-webhook/revisar-buzon
|
||||||
func RevisarBuzonAhora(c *fiber.Ctx) error {
|
func RevisarBuzonAhora(c *fiber.Ctx) error {
|
||||||
services.RevisarBuzonSoporte()
|
encontrados, procesados, err := services.RevisarBuzonSoporteConDetalle()
|
||||||
return c.JSON(fiber.Map{"ok": true, "message": "Buzón revisado, mirá la lista de tickets"})
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
if encontrados == 0 {
|
||||||
|
return c.JSON(fiber.Map{"ok": true, "message": "Conectó bien, pero no hay correos sin leer en el buzón"})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"ok": true, "message": fmt.Sprintf(
|
||||||
|
"%d correo(s) sin leer, %d convertido(s) en ticket. El resto ya estaba registrado.", encontrados, procesados)})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user