package services import ( "io" "net" "strings" "testing" "time" "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 \r\n" + "Subject: No puedo entrar\r\n" + "Message-Id: \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", 0) 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", 0) 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", 0) 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", 0); err != nil { t.Fatal(err) } otra, err := BajarNoLeidos(c, "INBOX", 0) 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 func TestVentanaDeAntiguedad(t *testing.T) { ahora := time.Now() casos := []struct { nombre string recibido time.Time horas int want bool }{ {"de hace una hora, ventana 12", ahora.Add(-1 * time.Hour), 12, true}, {"de hace 13 horas, ventana 12", ahora.Add(-13 * time.Hour), 12, false}, {"viejísimo, sin ventana", ahora.AddDate(-2, 0, 0), 0, true}, {"sin fecha del servidor", time.Time{}, 12, true}, } for _, c := range casos { if got := dentroDeLaVentana(c.recibido, c.horas); got != c.want { t.Errorf("%s: dentroDeLaVentana = %v, want %v", c.nombre, got, c.want) } } // El SINCE que se le pide al servidor tiene que ser MÁS amplio que la // ventana real: IMAP compara solo la fecha, y afinar de más pierde correos. c := criterioNoLeidos(12) if !c.Since.Before(ahora.Add(-12 * time.Hour)) { t.Errorf("Since = %v, debería ser anterior al corte real", c.Since) } if !criterioNoLeidos(0).Since.IsZero() { t.Error("sin ventana no debería mandarse SINCE") } }