Academia Deltamontero · Documentación
Desarrollolocal_deltamodules

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

PrincipioImplementación
ServidorSMTP 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).
Remitentenoreplyaddress = MOODLE_SMTP_FROM o, si no está, MOODLE_SMTP_USER (Office 365 rechaza otros remitentes), con nombre "Academia Deltamontero".
Cola y registroCada correo es una fila de local_deltamodules_mail, que también es el registro de envíos (se conserva 180 días).
LímitesPor 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).
ReintentosHasta 3 intentos (mailer::MAXTRIES), con espera de 5 minutos tras el primero y 10 tras el segundo.
Solo notificaciones en la plataformaconfigure-mail.php desactiva el procesador de mensajes email de Moodle en cada despliegue.
Desvío de pruebaMOODLE_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 escritasLos correos usan botones; el texto plano se genera sin enlaces escritos ni negritas (html_to_text pasaría las negritas a mayúsculas).
Pruebas localesCon $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).

TipoNombreCuándo se envíaMétodo
resetRecuperar contraseña"¿Olvidó su contraseña?" con correo configurado; enlace que vence en 1 horamailer::reset() (desde local_candidate_enrollment/recover.php)
welcomeBienvenida al personalAl crear la cuenta de un colaborador o incorporar a un postulante; usuario y enlace que vence en 7 díasmailer::welcome()
clientwelcomeCuenta de cliente creadaAl registrarse con el código de una empresamailer::client_welcome()
clientcodeCódigo para una empresa clienteDesde Clientes → Código de registromailer::client_code()
candidatecodeCódigo de prueba técnicaDesde Postulantes → Códigos de accesomailer::candidate_code()
candidatedonePrueba técnica recibidaAl enviar la prueba técnica (sin la nota)Observador on_test_submitted → candidate_done()
moduleMódulo asignadoMatrícula activa como estudiante de quien ya entró alguna vez a la AcademiaObservador on_enrolment → module_assigned()
completedMódulo terminado y certificadoAl completar un módulo, con enlace al certificado visibleObservador on_completed → module_completed()
reminderRecordatorio de módulos pendientesLos 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()
testCorreo de pruebaDesde la página de correoqueue() 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étodoQué 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ñaContenido
EstadoSi 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 correoEncender 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).
RegistroCorreos enviados, en cola o con error, con el motivo; reintentar uno o todos.
BienvenidasPersonal 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.

En esta página