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
@@ -36,33 +36,60 @@ func RevisarBuzonSoporte() {
|
||||
}
|
||||
defer imapEnCurso.Unlock()
|
||||
|
||||
n, err := revisarBuzon(cfg)
|
||||
encontrados, procesados, err := revisarBuzon(cfg)
|
||||
if err != nil {
|
||||
log.Printf("[SoporteIMAP] %v", err)
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
log.Printf("[SoporteIMAP] %d correo(s) procesado(s)", n)
|
||||
if encontrados > 0 {
|
||||
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
|
||||
// "Probar" de la vista de configuración.
|
||||
func ProbarConexionImap(cfg *models.SoporteWebhookConfig) error {
|
||||
func ProbarConexionImap(cfg *models.SoporteWebhookConfig) (string, error) {
|
||||
c, err := conectarImap(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
defer c.Close()
|
||||
carpeta := cfg.ImapCarpeta
|
||||
if carpeta == "" {
|
||||
carpeta = "INBOX"
|
||||
}
|
||||
if _, err := c.Select(carpeta, &imap.SelectOptions{ReadOnly: true}).Wait(); err != nil {
|
||||
return fmt.Errorf("no se pudo abrir la carpeta %q: %w", carpeta, err)
|
||||
datos, err := c.Select(carpeta, &imap.SelectOptions{ReadOnly: true}).Wait()
|
||||
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()
|
||||
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) {
|
||||
@@ -92,30 +119,63 @@ func conectarImap(cfg *models.SoporteWebhookConfig) (*imapclient.Client, error)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func revisarBuzon(cfg *models.SoporteWebhookConfig) (int, error) {
|
||||
func revisarBuzon(cfg *models.SoporteWebhookConfig) (int, int, error) {
|
||||
c, err := conectarImap(cfg)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, 0, err
|
||||
}
|
||||
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 == "" {
|
||||
carpeta = "INBOX"
|
||||
}
|
||||
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},
|
||||
}, &imap.SearchOptions{ReturnAll: true}).Wait()
|
||||
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()
|
||||
if len(uids) == 0 {
|
||||
return 0, nil
|
||||
return nil, nil
|
||||
}
|
||||
// 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.
|
||||
@@ -124,14 +184,15 @@ func revisarBuzon(cfg *models.SoporteWebhookConfig) (int, error) {
|
||||
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{
|
||||
BodySection: []*imap.FetchItemBodySection{{}},
|
||||
BodySection: []*imap.FetchItemBodySection{{Peek: true}},
|
||||
}).Collect()
|
||||
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 {
|
||||
var crudo []byte
|
||||
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)
|
||||
continue
|
||||
}
|
||||
if IngestarCorreoSoporte(cfg, 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)
|
||||
}
|
||||
out = append(out, CorreoConUID{UID: m.UID, Correo: correo})
|
||||
}
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user