package services import ( "encoding/json" "fmt" "strings" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapclient" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // CredencialesIMAP es el blob que viaja cifrado en UmindConexion.CredencialesEnc. type CredencialesIMAP struct { Host string `json:"host"` Puerto int `json:"puerto"` Usuario string `json:"usuario"` Password string `json:"password"` Encriptado string `json:"encriptado"` // ssl | starttls } // CifrarCredencialesIMAP / DescifrarCredencialesIMAP — mismo esquema AES-GCM // que los canales: un solo blob, nunca campos sueltos en claro. func CifrarCredencialesIMAP(c CredencialesIMAP) (string, error) { b, err := json.Marshal(c) if err != nil { return "", err } return CifrarSecretoUmind(string(b)) } func DescifrarCredencialesIMAP(enc string) (CredencialesIMAP, error) { var c CredencialesIMAP claro, err := DescifrarSecretoUmind(enc) if err != nil { return c, err } err = json.Unmarshal([]byte(claro), &c) return c, err } func conectarIMAPGenerico(cred CredencialesIMAP) (*imapclient.Client, error) { puerto := cred.Puerto if puerto == 0 { puerto = 993 } addr := fmt.Sprintf("%s:%d", cred.Host, puerto) var c *imapclient.Client var err error if strings.EqualFold(cred.Encriptado, "starttls") { c, err = imapclient.DialStartTLS(addr, nil) } else { c, err = imapclient.DialTLS(addr, nil) } if err != nil { return nil, fmt.Errorf("no se pudo conectar a %s: %w", addr, err) } if err := c.Login(cred.Usuario, cred.Password).Wait(); err != nil { c.Close() return nil, fmt.Errorf("login rechazado para %s: %w", cred.Usuario, err) } return c, nil } // LeerBandejaIMAP trae los últimos correos de la casilla, más recientes // primero. Es la tool "leer_bandeja" cuando la cuenta es IMAP: el dueño le // pide al agente "revisame los correos de hoy y resumime" y esto es lo que el // modelo recibe para resumir. // // Con Peek y sin marcar NADA: es la casilla personal del cliente y el estado // leído/no-leído es suyo — un asistente que le "lee" los correos no puede // dejárselos como leídos. func LeerBandejaIMAP(conexion *models.UmindConexion, consulta string, limite int) ([]CorreoResumen, error) { if limite <= 0 || limite > 10 { limite = 10 } cred, err := DescifrarCredencialesIMAP(conexion.CredencialesEnc) if err != nil { return nil, fmt.Errorf("credenciales de la cuenta corruptas: %w", err) } c, err := conectarIMAPGenerico(cred) if err != nil { return nil, err } defer c.Close() buzon, err := c.Select("INBOX", &imap.SelectOptions{ReadOnly: true}).Wait() if err != nil { return nil, fmt.Errorf("no se pudo abrir INBOX: %w", err) } if buzon.NumMessages == 0 { return []CorreoResumen{}, nil } // Los últimos N por número de secuencia: el orden de llegada. Se traen // algunos de más cuando hay filtro, porque el filtrado es local. traer := uint32(limite) if consulta != "" { traer = uint32(limite * 4) } desde := uint32(1) if buzon.NumMessages > traer { desde = buzon.NumMessages - traer + 1 } var seq imap.SeqSet seq.AddRange(desde, buzon.NumMessages) msgs, err := c.Fetch(seq, &imap.FetchOptions{ BodySection: []*imap.FetchItemBodySection{{Peek: true}}, }).Collect() if err != nil { return nil, fmt.Errorf("no se pudieron traer los correos: %w", err) } filtro := strings.ToLower(strings.TrimSpace(consulta)) var out []CorreoResumen // Al revés: los más recientes primero. for i := len(msgs) - 1; i >= 0 && len(out) < limite; i-- { var crudo []byte for _, b := range msgs[i].BodySection { crudo = b.Bytes break } if len(crudo) == 0 { continue } correo, err := parsearCorreo(crudo) if err != nil { continue } if filtro != "" && !strings.Contains(strings.ToLower(correo.Subject), filtro) && !strings.Contains(strings.ToLower(correo.From), filtro) && !strings.Contains(strings.ToLower(correo.Texto), filtro) { continue } extracto := correo.Texto if len([]rune(extracto)) > 300 { extracto = string([]rune(extracto)[:300]) + "…" } out = append(out, CorreoResumen{ De: correo.From, Asunto: correo.Subject, Extracto: extracto, }) } return out, nil } // ProbarConexionIMAPGenerica valida credenciales sin tocar nada del buzón. func ProbarConexionIMAPGenerica(cred CredencialesIMAP) error { c, err := conectarIMAPGenerico(cred) if err != nil { return err } defer c.Close() if _, err := c.Select("INBOX", &imap.SelectOptions{ReadOnly: true}).Wait(); err != nil { return fmt.Errorf("no se pudo abrir INBOX: %w", err) } _ = c.Logout().Wait() return nil }