Una respuesta a un hilo existente saltea el clasificador; un correo nuevo no.
Por eso el sistema parecía funcionar solo con las respuestas: los correos
nuevos que el modelo consideraba "no soporte" desaparecían sin ticket, sin
acuse al cliente y sin más rastro que un renglón de log que nadie mira.
Dos cambios:
- El filtro solo opina sobre desconocidos. Si el remitente es un cliente o un
usuario del portal, siempre se abre el ticket. Para eso la resolución del
remitente pasa a correr antes del filtro. A un cliente registrado no se le
descarta el correo por lo que diga un modelo.
- Lo descartado se ve. "Revisar buzón ahora" informa cuántos dejó afuera el
filtro, con asunto, remitente y motivo. Guardado en memoria: es diagnóstico
de hace un rato, no algo que valga una tabla.
Además, si la respuesta automática está apagada, el log lo dice al crear el
ticket — era la otra explicación posible de "no me contestó" y no se distinguía.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sin ventana, la primera corrida convierte en tickets todo lo que haya sin leer,
que en una casilla de años es una avalancha. Ahora hay un desplegable de
antigüedad máxima —6h, 12h, 24h, 3 días o todo— y por defecto 12 horas.
El corte se hace en dos pasos porque IMAP no da para más: al servidor se le
pide SINCE con un día de margen (SINCE compara solo la fecha, no la hora) y el
corte fino por hora se aplica contra la fecha real de cada mensaje. Afinar el
SINCE sería perder los correos del borde.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Filtro (opcional, apagado por defecto): antes de abrir ticket, la IA lee el
correo y decide si es soporte o ruido —newsletters, notificaciones de
plataformas, facturas de proveedores, spam—. Falla abierto: si la IA no está
configurada o se cae, el ticket se abre igual. Ignorar a un cliente es peor que
tener un ticket de más. Hay un campo de contexto del negocio para los casos
raros de cada uno.
Los correos masivos se descartan por sus propios encabezados (List-Unsubscribe,
Precedence, Auto-Submitted) sin gastar una llamada de IA.
Y el motivo por el que el mismo correo se leía y se contestaba de nuevo: la
deduplicación era solo un SELECT previo, que no sirve si el flag de leído no
llegó a guardarse o si dos instancias leen el buzón a la vez. Ahora hay un
índice único parcial sobre message_id: la base rechaza el segundo, y ese rechazo
se trata como "ya estaba", no como error — sin ticket repetido y sin segunda
auto-respuesta.
Si algún correo no se pudo marcar como leído, "Revisar buzón ahora" lo dice en
vez de dejarlo solo en el log: es la señal de que van a volver a leerse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Los clientes escriben a soporte@ desde su correo de siempre. Hasta ahora eso
solo llegaba si un proveedor (SendGrid/Mailgun) nos hacía POST; si nadie lo
configuraba, los correos quedaban sin leer en el buzón.
Ahora el cron entra al buzón cada 2 minutos, baja los no leídos, abre ticket
(o los engancha al hilo si son respuesta) y los marca como leídos. La lógica
de ingesta se movió a services para que webhook e IMAP se comporten igual.
La contraseña del buzón se guarda cifrada (AES-GCM con APP_KEY) y no vuelve
al navegador.
De paso, dos bugs que impedían guardar la configuración: el formulario mandaba
id=0 (gorm.Model serializa "ID"), así que cada guardado creaba una fila nueva
en vez de editar la que se usa; y Updates con struct ignoraba los booleanos en
false, así que desactivar algo no tenía efecto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>