Correo de la Academia
Cola de envío con límites para Office 365 (mailer), tipos de correo, enlaces de un solo uso para crear contraseña, tarea send_mail y página de administración.
Todos los correos que envía la Academia pasan por classes/mailer.php. Las notificaciones de Moodle (foros, calificaciones, mensajes…) no salen por correo: se quedan en la campana.
Principios
| Principio | Implementación |
|---|---|
| Servidor | SMTP de Office 365 (smtp.office365.com:587, STARTTLS) configurado por moodle-config.sh desde las variables MOODLE_SMTP_*. Sin MOODLE_SMTP_HOST no se envía nada y cada pantalla funciona como antes (por ejemplo, la recuperación de contraseña con cédula). |
| Remitente | noreplyaddress = MOODLE_SMTP_FROM o, si no está, MOODLE_SMTP_USER (Office 365 rechaza otros remitentes), con nombre "Academia Deltamontero". |
| Cola y registro | Cada correo es una fila de local_deltamodules_mail, que también es el registro de envíos (se conserva 180 días). |
| Límites | Por minuto (mail_perminute, entre 1 y 30, por defecto 20) y por día (mail_perday, entre 1 y 10.000, por defecto 2.000). |
| Reintentos | Hasta 3 intentos (mailer::MAXTRIES), con espera de 5 minutos tras el primero y 10 tras el segundo. |
| Solo notificaciones en la plataforma | configure-mail.php desactiva el procesador de mensajes email de Moodle en cada despliegue. |
| Desvío de prueba | MOODLE_MAIL_DIVERT_TO → configuración mail_divert_to: todo va a esa dirección sin cambiar el asunto. El desvío nativo de Moodle (divertallemailsto) queda vacío. |
| Sin direcciones escritas | Los correos usan botones; el texto plano se genera sin enlaces escritos ni negritas (html_to_text pasaría las negritas a mayúsculas). |
| Pruebas locales | Con $CFG->noemailever no sale nada y el envío se considera correcto. |
Tipos de correo
mailer::KINDS define cada tipo; cada uno se apaga o enciende en la página de correo (configuración mail_off_<tipo>; el tipo test no se puede apagar).
| Tipo | Nombre | Cuándo se envía | Método |
|---|---|---|---|
reset | Recuperar contraseña | "¿Olvidó su contraseña?" con correo configurado; enlace que vence en 1 hora | mailer::reset() (desde local_candidate_enrollment/recover.php) |
welcome | Bienvenida al personal | Al crear la cuenta de un colaborador o incorporar a un postulante; usuario y enlace que vence en 7 días | mailer::welcome() |
clientwelcome | Cuenta de cliente creada | Al registrarse con el código de una empresa | mailer::client_welcome() |
clientcode | Código para una empresa cliente | Desde Clientes → Código de registro | mailer::client_code() |
candidatecode | Código de prueba técnica | Desde Postulantes → Códigos de acceso | mailer::candidate_code() |
candidatedone | Prueba técnica recibida | Al enviar la prueba técnica (sin la nota) | Observador on_test_submitted → candidate_done() |
module | Módulo asignado | Matrícula activa como estudiante de quien ya entró alguna vez a la Academia | Observador on_enrolment → module_assigned() |
completed | Módulo terminado y certificado | Al completar un módulo, con enlace al certificado visible | Observador on_completed → module_completed() |
reminder | Recordatorio de módulos pendientes | Los lunes, a quien tiene módulos sin terminar sin entrar hace 14 días (como máximo cada 14 días) | Tarea mail_reminders → reminders() |
test | Correo de prueba | Desde la página de correo | queue() directo y sample() |
Los avisos automáticos ignoran super administradores, cursos ocultos y el curso PTS (salvo candidatedone, que es solo para él). module también ignora a quien tiene el módulo desactivado. Varios tipos llevan una clave dedupe (por ejemplo module:<userid>:<courseid>, completed:…, candidatedone:<attemptid>, clientwelcome:<userid>) para no repetirse.
Flujo de envío
queue($to, $kind, $subject, $inner, $opts)
Descarta el correo si el correo no está habilitado (enabled(): servidor configurado y sin mail_paused), si el tipo está apagado, si el destinatario está borrado, suspendido o es nologin, si la dirección no es válida o si ya existe la misma clave dedupe. Envuelve el contenido en la plantilla (layout()), genera el texto plano e inserta la fila con estado 0 (en cola).
Opción immediate: true envía en la misma petición (la página espera el resultado); 'after' envía al terminar la página (deliver_after: cierra la sesión, entrega la respuesta al navegador y luego envía); sin ella, lo envía la tarea.
deliver($rec)
Respeta los límites (allowance()), toma un bloqueo por correo (lock_config, recurso local_deltamodules_mail) para no enviarlo dos veces, vuelve a leer la fila y comprueba que la cuenta siga activa. Si el correo tiene purpose (welcome o reset), crea en ese momento el enlace de un solo uso y reemplaza el marcador {{DELTA_PWLINK}} (mailer::PWLINK).
send_mail()
Usa get_mailer() de Moodle (PHPMailer), codificación quoted-printable, cabeceras Auto-Submitted: auto-generated y X-Auto-Response-Suppress: All, y el logo pix/mail-logo.png incrustado (cid:deltalogo). El motivo de un fallo se toma de la excepción o del evento \core\event\email_failed (observador on_email_failed).
Resultado
Enviado: estado 1 y timesent. Fallido: se guarda lasterror (prefijo [cron] o [página]) y el estado vuelve a 0 hasta llegar a 3 intentos, luego 2 (con error). retry($id) vuelve a poner en cola un correo con error.
Tarea send_mail
Cada minuto, mailer::process() envía los pendientes dentro de los límites (los nuevos y los reintentos cuyo tiempo de espera ya pasó), borra del registro lo de más de 180 días y los enlaces vencidos hace más de 30 días.
Enlaces de un solo uso
Tabla local_deltamodules_pwtoken. El registro de correos nunca guarda un enlace válido: guarda el marcador.
| Método | Qué hace |
|---|---|
create_token($userid, $purpose) | Anula los enlaces anteriores del mismo tipo, genera 32 bytes aleatorios (64 caracteres hexadecimales) y guarda solo su sha256. Vigencia según mailer::TTL: welcome 7 días, reset 1 hora. |
find_token($raw) | Válido si el formato es correcto, existe, no se usó, no venció y la cuenta está activa, no suspendida y con autenticación manual. |
consume($t) | Marca como usados todos los enlaces de esa persona. |
La página pública setpassword.php?token=… guarda el código en la sesión y lo quita de la dirección al instante; si es válido, pide la contraseña nueva con la política del sitio (lista de requisitos que se marca mientras se escribe), la guarda, consume el enlace, cierra las otras sesiones de la persona, confirma con su usuario y la lleva al inicio de sesión (no inicia sesión por ella). El botón se desactiva al pulsarlo para evitar un doble envío.
Plantilla visual
mailer::layout() arma una tarjeta de 560 px: encabezado azul con degradado, título, logo a la derecha, línea roja, contenido centrado y pie con el correo de soporte (supportemail). Fuente 'Century Gothic','Nunito','Segoe UI',Arial,sans-serif (mailer::FONT). Ayudantes: button(), greet(), p(), badge(), codebox(), note(), closing(). El saludo usa short_name(): primer nombre y primer apellido, respetando apellidos compuestos con partículas (de, del, la…).
Página de administración: mail.php
Usuarios → Cuentas → Correos de la Academia, capacidad local/deltamodules:managepeople.
| Pestaña | Contenido |
|---|---|
| Estado | Si el correo está configurado, pausar o reanudar, enviar un correo de prueba, límites por minuto y por día, estadísticas y diagnóstico de la conexión. |
| Tipos de correo | Encender o apagar cada tipo y enviar un ejemplo de cada uno (mailer::sample(): asunto con [Ejemplo], datos de muestra, enlace de contraseña no válido). |
| Registro | Correos enviados, en cola o con error, con el motivo; reintentar uno o todos. |
| Bienvenidas | Personal que todavía no entra, para enviarles la bienvenida (no repite si ya se le envió en las últimas 24 horas). |
mailer::diagnose() revisa paso a paso DNS, conexión por IPv6 (informativa), conexión al puerto, saludo SMTP, STARTTLS (con la versión TLS negociada) e inicio de sesión AUTH LOGIN, y traduce los errores típicos de Office 365 (SMTP AUTH desactivado, credenciales rechazadas). Nunca muestra la clave; el super administrador puede probar con un usuario y una clave escritos solo para la prueba (no se guardan) y ve la longitud de la clave guardada y si contiene caracteres que Coolify o Docker podrían alterar.
La imagen prefiere IPv4 (línea precedence ::ffff:0:0/96 100 en /etc/gai.conf del Dockerfile) porque Office 365 también publica direcciones IPv6 y el servidor no tiene salida IPv6.
Clientes
Empresas cliente como cohortes cli_*, sus módulos, sus usuarios, los códigos de registro, el autorregistro y la expiración del acceso.
Asistente de IA
Cómo se integra el chat de IA (local_ai_manager y block_ai_chat) con el código propio - perfil del usuario, agente de acciones, niveles de cuota, autor de preguntas y Configuración IA.