Banred Helpdesk no usa webhooks ni conexiones push para recibir correos: opera con un modelo de extracción periódica (pull-based). Cada cierto tiempo, dos scripts PHP consultan activamente los buzones configurados, descargan los mensajes no leídos y los convierten en tickets nuevos o en respuestas a tickets existentes. Para que este proceso ocurra de forma continua y automática, ambos scripts deben ejecutarse como tareas programadas (cron jobs) en el servidor.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/banredco/helpdesk/llms.txt
Use this file to discover all available pages before exploring further.
Cron job para buzones Gmail API
El scriptcron/process-gmail.php gestiona todos los buzones cuyo connection_type es gmail.
Flujo de ejecución:
- Consulta la tabla
mailboxesfiltrando poris_active = 1yconnection_type = 'gmail'. - Por cada buzón encontrado, llama a
GmailMailboxProcessor::processMailbox($mailbox). - Dentro de
processMailbox, se invocaGmailService::getMessages($email)para obtener hasta 20 mensajes no leídos del buzón. - Para cada mensaje, se verifica en la tabla
email_importssi elgmail_message_idya fue procesado; si es así, se omite. - Se llama a
GmailService::parseMessage()yGmailService::getAttachments()para extraer remitente, asunto, cuerpo y adjuntos. - El mensaje se pasa a
EmailProcessor::process($mailbox, $data)para crear el ticket o añadir la respuesta. - Si el procesamiento es exitoso, se registra el
gmail_message_idenemail_importsconprocess_status = 'Procesado'. - Finalmente,
GmailService::markAsRead()marca el mensaje como leído en Gmail para que no sea procesado nuevamente. - Cualquier excepción durante el procesamiento de un mensaje se captura y se registra con
error_log(), sin interrumpir el procesamiento del resto de los mensajes.
Cron job para buzones IMAP
El scriptcron/process-imap.php sigue el mismo patrón para los buzones con connection_type = 'imap' y create_tickets = 1.
Diferencias respecto al flujo Gmail:
- La conexión se establece con
imap_open()usando host, puerto, usuario y contraseña del buzón. - Los mensajes no leídos se recuperan con
imap_search($connection, 'UNSEEN'). - El UID de cada mensaje se verifica en
email_imports(columnamessage_uid) para evitar duplicados. - Antes de procesar, el script inspecciona las cabeceras (
imap_fetchheader) para detectar y descartar respuestas automáticas. Filtra cabeceras comoauto-submitted: auto-replied,x-autoreply,precedence: bulkyx-ms-exchange-generated-message-source, entre otras. - También descarta mensajes cuyo asunto contenga cadenas de respuesta automática como
automatic reply,out of office,fuera de oficina,respuesta automáticaomail delivery failed. - El cuerpo se extrae navegando la estructura MIME del mensaje y se decodifica con
quoted_printable_decode(). - Los adjuntos se detectan recorriendo
$structure->partsde forma recursiva con la funciónextractAttachments(). - Al finalizar, el mensaje se marca como leído con
imap_setflag_full($connection, $emailNumber, '\\Seen').
Configuración en el servidor
Edita el crontab del usuario del servidor web para añadir ambas tareas. Se recomienda una ejecución cada minuto para garantizar una sincronización en tiempo casi real:>> /var/log/helpdesk-gmail.log 2>&1 acumula la salida estándar y los errores en archivos de log separados para cada canal, facilitando la revisión de incidentes.
Lógica de EmailProcessor
Independientemente del canal de entrada (Gmail o IMAP), todos los mensajes llegan aEmailProcessor::process($mailbox, $data). Este método aplica la siguiente lógica para decidir qué hacer con cada correo:
1. Detección de respuesta a ticket existente
El procesador aplica una expresión regular sobre el asunto del correo:
[Ticket #NNN], se extrae el número de ticket y se llama a processReply().
2. Procesamiento de respuesta (processReply)
- Busca en la tabla
ticketsel registro con eseticket_number. - Inserta un nuevo registro en
ticket_messagescon el cuerpo del mensaje (limpiado concleanReplyBody()para eliminar citas previas y firmas automáticas). - Si el ticket estaba en estado resuelto (4) o cerrado (5), lo reabre automáticamente a estado
1y registra la actividad como'Reabierto'. - Si el ticket estaba en otro estado, actualiza
activity_labela'Cliente respondió'y actualizalast_reply_at. - Crea una notificación interna mediante
Notification::create().
processNewTicket)
Si el asunto no contiene ningún patrón de ticket:
- Busca al cliente por
emailen la tablacustomers. Si no existe, lo crea automáticamente con el nombre y correo extraídos del encabezado del mensaje. - Calcula el siguiente
ticket_numberconMAX(ticket_number) + 1. - Inserta el ticket en la tabla
ticketsconstatus_id = 1(Nuevo),priority_id = 2(Media) ysource = 'email'. - Inserta el cuerpo del correo como primer mensaje en
ticket_messages. - Si hay adjuntos, los guarda en
uploads/tickets/e inserta los registros enticket_attachments. - Envía la notificación al cliente disparando
MailService::sendTemplate('ticket_created', $ticketId).
Cuando un cliente responde directamente al correo de notificación de un ticket, el asunto incluye automáticamente el patrón
[Ticket #123] porque así está configurada la plantilla de respuesta. Gracias a esto, el sistema detecta la respuesta y la ensarta en el hilo del ticket correcto sin intervención manual del agente.Sincronización manual
Si necesitas procesar los correos pendientes sin esperar al próximo ciclo del cron, los administradores pueden acceder a/admin/mail-sync.php desde el navegador para disparar la sincronización de forma inmediata. Esto es útil durante la configuración inicial, al resolver incidentes o cuando se necesita confirmar que un correo entrante fue procesado correctamente.