Cómo configurar SMTP para la recuperación de cuenta en MU Online
Configura el envío de correo por SMTP en el sitio de tu servidor de MU Online e implementa un flujo seguro de recuperación de contraseña por token, con PHPMailer, expiración y protección contra abuso.
Nada frustra más a un jugador que perder el acceso a su cuenta y no tener cómo recuperarla. En servidores de MU Online, la recuperación de contraseña por correo es la solución estándar, pero solo funciona si el sitio consigue enviar correos de forma confiable, y eso depende de un SMTP bien configura
Nada frustra más a un jugador que perder el acceso a su cuenta y no tener cómo recuperarla. En servidores de MU Online, la recuperación de contraseña por correo es la solución estándar, pero solo funciona si el sitio consigue enviar correos de forma confiable, y eso depende de un SMTP bien configurado. Este tutorial cubre las dos mitades del problema: primero hacer que el servidor envíe correos que realmente lleguen a la bandeja de entrada (no al spam), y después construir un flujo de recuperación de contraseña seguro, con token de un solo uso, expiración y protección contra abuso. Los nombres de tablas, columnas y el formato de hash de contraseña aparecen como ejemplo y varían según la versión de tu MuServer, así que confirma cada uno en tu base de datos. Si todavía estás armando la base, mira la guía de cómo crear un servidor de MU Online.
Requisitos previos
El envío de correo junta tres cosas que deben estar alineadas: el sitio, un proveedor SMTP y el DNS de tu dominio.
- Sitio del servidor en PHP funcionando con acceso a la base donde están las cuentas (
MEMB_INFO, por ejemplo). - PHP 7.4 o superior con
opensslhabilitado (necesario para TLS en el SMTP). - Composer instalado, para traer la biblioteca PHPMailer.
- Una cuenta en un proveedor SMTP — puede ser Gmail (pruebas), o un servicio transaccional como Brevo, Mailgun o Amazon SES (producción).
- Acceso al panel de DNS del dominio, para crear los registros SPF, DKIM y DMARC.
- Una dirección remitente dedicada, como
[email protected].
| Proveedor SMTP | Indicado para | Límite típico (varía) |
|---|---|---|
| Gmail (contraseña de app) | Pruebas, servidor pequeño | ~500 correos/día |
| Brevo / Sendinblue | Producción pequeña/media | Plan gratis ~300/día |
| Mailgun | Producción con volumen | Pago por millar |
| Amazon SES | Alto volumen, bajo costo | Escala bajo demanda |
Por qué no usar la función mail() nativa
El primer impulso es usar la función mail() de PHP, pero es la causa número uno de correos en la carpeta de spam en servidores privados. Envía directo por el servidor local, casi siempre sin autenticación SMTP, sin SPF alineado y desde una IP con mala reputación. El resultado es previsible: Gmail y Outlook rechazan o marcan como basura. La solución es enviar a través de un servidor SMTP autenticado usando una biblioteca dedicada; PHPMailer es la más consolidada. Se encarga de la negociación TLS, de la autenticación y del formateo correcto de la cabecera.
Instala vía Composer:
composer require phpmailer/phpmailer
Configurando las credenciales SMTP
Mantén las credenciales fuera del código versionado, en un archivo de configuración separado con permiso restringido. Nunca hagas commit de contraseñas de SMTP en el repositorio.
<?php
// config/smtp.php (fuera de la raíz pública si es posible)
return [
'host' => 'smtp-relay.brevo.com', // varía por proveedor
'porta' => 587, // 587 STARTTLS o 465 SSL
'seguranca' => 'tls', // 'tls' o 'ssl'
'usuario' => 'tu-usuario-smtp',
'senha' => 'TU_CONTRASEÑA_O_API_KEY',
'remetente' => '[email protected]',
'nome_de' => 'Servidor ViciadosMU',
];
Función de envío con PHPMailer
Centraliza el envío en una única función reutilizable. Así, tanto la recuperación de contraseña como cualquier otro correo (bienvenida, aviso de VIP) usan el mismo camino probado.
<?php
// src/Mailer.php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require __DIR__ . '/../vendor/autoload.php';
function enviarEmail(string $para, string $assunto, string $corpoHtml): bool
{
$cfg = require __DIR__ . '/../config/smtp.php';
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = $cfg['host'];
$mail->SMTPAuth = true;
$mail->Username = $cfg['usuario'];
$mail->Password = $cfg['senha'];
$mail->SMTPSecure = $cfg['seguranca']; // tls | ssl
$mail->Port = $cfg['porta'];
$mail->CharSet = 'UTF-8';
$mail->setFrom($cfg['remetente'], $cfg['nome_de']);
$mail->addAddress($para);
$mail->isHTML(true);
$mail->Subject = $assunto;
$mail->Body = $corpoHtml;
// Versión texto para clientes sin HTML
$mail->AltBody = strip_tags($corpoHtml);
$mail->send();
return true;
} catch (Exception $e) {
error_log('[SMTP] Fallo al enviar: ' . $mail->ErrorInfo);
return false;
}
}
Pruébalo de forma aislada antes de integrar cualquier cosa. Crea un script rápido que llame a enviarEmail('[email protected]', 'Prueba SMTP', '<b>¡Funcionó!</b>') y confirma la recepción. Si cae en el spam ya en esta prueba, el problema está en el DNS, no en el código.
Configurando SPF, DKIM y DMARC
Esta es la etapa que más impacta en la entrega y la que más se ignora. Estos tres registros DNS le dicen a los servidores de correo que tu dominio autoriza a ese SMTP a enviar en su nombre.
- SPF — un registro
TXTque lista quién puede enviar por tu dominio. El proveedor SMTP entrega el valor exacto; algo comov=spf1 include:spf.brevo.com ~all. - DKIM — una firma criptográfica. El proveedor genera un par de claves y tú publicas la clave pública en un registro
TXT(oCNAME) en el subdominio indicado. - DMARC — una política que dice qué hacer con los correos que fallan SPF/DKIM. Empieza suave:
v=DMARC1; p=none; rua=mailto:[email protected], y después endurece ap=quarantine.
Después de publicar, espera la propagación (puede llevar horas) y valida con herramientas de chequeo de SPF/DKIM. Solo pasa a producción cuando los tres pasen.
Preparando la base de datos para el token de recuperación
El flujo de recuperación necesita un lugar donde guardar los tokens temporales. Crea una tabla dedicada; no la mezcles con la tabla de cuentas del juego.
-- Ejemplo (varía por versión): tabla de tokens de recuperación
CREATE TABLE WEB_PASSWORD_RESET (
id INT IDENTITY(1,1) PRIMARY KEY,
conta VARCHAR(10) NOT NULL,
token_hash CHAR(64) NOT NULL, -- sha256 del token
expira_em DATETIME NOT NULL,
usado BIT NOT NULL DEFAULT 0,
ip_origem VARCHAR(45) NULL,
criado_em DATETIME NOT NULL DEFAULT GETDATE()
);
Guardar solo el token_hash (y no el token puro) es una decisión de seguridad deliberada: incluso con acceso a la base, nadie puede armar el enlace de recuperación.
Etapa 1: solicitar recuperación
El jugador informa el correo. El sistema busca la cuenta asociada, genera un token aleatorio fuerte, guarda el hash y envía el enlace por correo. Un detalle importante: la respuesta al usuario debe ser la misma exista o no la cuenta, para no revelar cuáles correos están registrados.
<?php
// solicitar-recuperacao.php
require 'src/Mailer.php';
require 'src/db.php';
function solicitarRecuperacao(string $email): void
{
$pdo = getPDO();
// Buscar la cuenta por el correo (la columna varía por versión)
$stmt = $pdo->prepare("SELECT memb_id FROM MEMB_INFO WHERE mail_addr = ?");
$stmt->execute([$email]);
$conta = $stmt->fetchColumn();
// Responde siempre igual, exista o no la cuenta
if ($conta) {
$token = bin2hex(random_bytes(32)); // token puro (solo en el correo)
$tokenHash = hash('sha256', $token);
$expira = date('Y-m-d H:i:s', time() + 1800); // 30 min
$ins = $pdo->prepare(
"INSERT INTO WEB_PASSWORD_RESET (conta, token_hash, expira_em, ip_origem)
VALUES (?, ?, ?, ?)"
);
$ins->execute([$conta, $tokenHash, $expira, $_SERVER['REMOTE_ADDR'] ?? null]);
$link = "https://tuserver.com/redefinir?conta=" . urlencode($conta)
. "&token=" . $token;
$html = "<p>Recibimos una solicitud para restablecer tu contraseña.</p>
<p><a href=\"$link\">Haz clic aquí para crear una nueva contraseña</a>.</p>
<p>El enlace expira en 30 minutos. Si no fuiste tú, ignora este correo.</p>";
enviarEmail($email, 'Recuperación de contraseña - ViciadosMU', $html);
}
// Mensaje neutro para el usuario, siempre
echo 'Si este correo está registrado, recibirás las instrucciones en instantes.';
}
Etapa 2: validar el token y restablecer la contraseña
Cuando el jugador hace clic en el enlace, el sistema valida el token contra el hash, comprueba expiración y uso, y solo entonces permite definir la nueva contraseña. Al final, el token se marca como usado; nunca reutilizable.
<?php
// redefinir.php
require 'src/db.php';
function redefinirSenha(string $conta, string $token, string $novaSenha): array
{
$pdo = getPDO();
if (strlen($novaSenha) < 6) {
return ['ok' => false, 'msg' => 'La contraseña debe tener al menos 6 caracteres'];
}
$stmt = $pdo->prepare(
"SELECT id, expira_em, usado FROM WEB_PASSWORD_RESET
WHERE conta = ? AND token_hash = ?"
);
$stmt->execute([$conta, hash('sha256', $token)]);
$reg = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$reg) {
return ['ok' => false, 'msg' => 'Enlace inválido'];
}
if ($reg['usado']) {
return ['ok' => false, 'msg' => 'Este enlace ya fue utilizado'];
}
if (strtotime($reg['expira_em']) < time()) {
return ['ok' => false, 'msg' => 'Este enlace expiró. Solicítalo de nuevo.'];
}
// El formato de contraseña VARÍA POR VERSIÓN — confirma el de tu MuServer
// Ejemplo común: MD5 en mayúsculas
$senhaCript = strtoupper(md5($novaSenha));
$pdo->beginTransaction();
$pdo->prepare("UPDATE MEMB_INFO SET memb_pw = ? WHERE memb_id = ?")
->execute([$senhaCript, $conta]);
$pdo->prepare("UPDATE WEB_PASSWORD_RESET SET usado = 1 WHERE id = ?")
->execute([$reg['id']]);
$pdo->commit();
return ['ok' => true, 'msg' => '¡Contraseña cambiada con éxito! Ya puedes entrar al juego.'];
}
Usar una transacción garantiza que la contraseña solo se considera cambiada si el token también se invalida en el mismo movimiento, sin ventana para reuso.
Protegiendo contra el abuso
Un formulario de recuperación abierto es una invitación al abuso: alguien puede disparar cientos de correos o intentar adivinar tokens. Algunas defensas simples resuelven la mayor parte:
- Límite de solicitudes por IP y por cuenta — como máximo, por ejemplo, 3 pedidos cada 15 minutos.
- Token largo y aleatorio — 32 bytes de
random_bytesson imposibles de adivinar por fuerza bruta. - CAPTCHA en el formulario de solicitud, para contener bots.
- Expiración corta — 15 a 30 minutos es suficiente para que el jugador actúe.
- Invalidación de tokens antiguos — al generar uno nuevo, marca los anteriores de la misma cuenta como usados.
- Log de intentos, guardando IP y hora, para investigar picos sospechosos.
Errores comunes y soluciones
| Error | Causa probable | Solución |
|---|---|---|
| El correo cae siempre en spam | Falta de SPF/DKIM/DMARC | Publica los tres registros DNS y valida antes de producción |
SMTP connect() failed | Puerto/seguridad equivocados o firewall | Revisa el puerto 587/465 y libera la salida en el firewall del servidor |
Could not authenticate | Usuario o contraseña SMTP incorrectos | Genera contraseña de app (Gmail) o API key (transaccional) y vuelve a probar |
openssl ausente | Extensión deshabilitada en PHP | Habilita extension=openssl en el php.ini y reinicia |
| El enlace de recuperación no funciona | Comparando token puro con hash | Aplica hash('sha256', $token) en la validación |
| Token reutilizable | Falta marcar usado = 1 | Invalida el token dentro de la misma transacción del cambio de contraseña |
| Filtración de cuentas existentes | Mensajes diferentes por correo | Responde siempre el mismo mensaje neutro |
Probando el flujo completo
- Configura el SMTP y envía un correo de prueba aislado; confirma que llega a la bandeja de entrada, no al spam.
- Publica SPF, DKIM y DMARC y vuelve a validar la prueba.
- Solicita recuperación con un correo registrado y confirma la recepción del enlace.
- Solicita con un correo inexistente y verifica que el mensaje mostrado sea idéntico.
- Haz clic en el enlace, define una nueva contraseña e inicia sesión en el juego con ella.
- Intenta usar el mismo enlace de nuevo — debe ser rechazado como "ya utilizado".
- Espera la expiración y prueba un enlace vencido — debe pedir una nueva solicitud.
Lista de verificación de lanzamiento
- PHPMailer instalado vía Composer y
opensslhabilitado - Credenciales SMTP en archivo fuera del versionamiento
- Correo de prueba llegando a la bandeja de entrada (no spam)
- SPF, DKIM y DMARC publicados y validados
- Tabla
WEB_PASSWORD_RESETcreada - Token guardado solo como hash
sha256 - Expiración de token configurada (15-30 min)
- Token invalidado tras el primer uso (transacción)
- Mensaje neutro que no revela cuentas existentes
- Rate limiting y CAPTCHA en el formulario de solicitud
- Formato de contraseña confirmado para tu versión del MuServer
- Flujo completo probado de punta a punta
Preguntas frecuentes
¿Puedo usar Gmail como SMTP del servidor?
Puedes para pruebas y servidores pequeños, pero Gmail impone límites diarios y exige contraseña de aplicación. Para producción, un proveedor transaccional (Brevo, Mailgun, Amazon SES) entrega mejor y no bloquea tu cuenta.
¿Por qué los correos de mi sitio caen en spam?
Casi siempre por falta de SPF, DKIM y DMARC en el dominio, o por enviar desde una IP con mala reputación. Configurar esos tres registros DNS es el paso que más mejora la entrega.
¿El token de recuperación necesita expirar?
Sí. Un token sin expiración se convierte en una llave permanente para invadir la cuenta. Lo estándar es expirar entre 15 y 60 minutos e invalidarlo en cuanto se usa una vez.
¿Necesito guardar el token en texto plano en la base de datos?
No, y no deberías. Guarda solo el hash del token. Si la base se filtra, nadie puede reconstruir los enlaces de recuperación a partir de los hashes.
¿SMTP con SSL o TLS, cuál elegir?
Usa el puerto 587 con STARTTLS en la mayoría de los proveedores modernos, o el puerto 465 con SSL implícito cuando el proveedor lo exija. Evita el puerto 25 sin cifrado, que suele estar bloqueado y es inseguro.