Brazil's biggest MU Online portal — since 2003
Tutorial Intermediate Website

How to set up SMTP for account recovery in MU Online

Set up SMTP email delivery on your MU Online server website and implement a secure token-based password recovery flow, with PHPMailer, expiration and abuse protection.

GA Gabriel · Updated on Jul 12, 2026 · ⏱ 20 min read
Quick answer

Nothing frustrates a player more than losing access to their account with no way to recover it. On MU Online servers, email password recovery is the standard solution — but it only works if the site can send emails reliably, and that depends on a well-configured SMTP. This tutorial covers both halve

Nothing frustrates a player more than losing access to their account with no way to recover it. On MU Online servers, email password recovery is the standard solution — but it only works if the site can send emails reliably, and that depends on a well-configured SMTP. This tutorial covers both halves of the problem: first getting the server to send emails that actually reach the inbox (not the spam folder), and then building a secure password recovery flow, with a single-use token, expiration and abuse protection. The table names, columns and the password hash format appear as examples and vary by version of your MuServer, so confirm each one against your database. If you are still building the foundation, see the how to create a MU Online server guide.

Prerequisites

Sending email brings together three things that must be aligned: the site, an SMTP provider and your domain's DNS.

  • A working PHP server website with access to the database where the accounts live (MEMB_INFO, for example).
  • PHP 7.4 or higher with openssl enabled (required for TLS over SMTP).
  • Composer installed, to pull in the PHPMailer library.
  • An account with an SMTP provider — this can be Gmail (testing), or a transactional service such as Brevo, Mailgun or Amazon SES (production).
  • Access to your domain's DNS panel, to create the SPF, DKIM and DMARC records.
  • A dedicated sender address, such as [email protected].
SMTP providerBest forTypical limit (varies)
Gmail (app password)Testing, small server~500 emails/day
Brevo / SendinblueSmall/medium productionFree plan ~300/day
MailgunProduction with volumePaid per thousand
Amazon SESHigh volume, low costScales on demand

Why not use the native mail() function

The first instinct is to use PHP's mail() function, but it is the number-one cause of emails landing in the spam folder on private servers. It sends directly through the local server, almost always without SMTP authentication, without aligned SPF and from an IP with a bad reputation. The result is predictable: Gmail and Outlook reject it or mark it as junk. The solution is to send through an authenticated SMTP server using a dedicated library — PHPMailer is the most established one. It handles the TLS negotiation, the authentication and the correct header formatting.

Install it via Composer:

composer require phpmailer/phpmailer

Configuring the SMTP credentials

Keep the credentials out of versioned code, in a separate configuration file with restricted permissions. Never commit SMTP passwords to a repository.

<?php
// config/smtp.php  (outside the public root if possible)
return [
    'host'       => 'smtp-relay.brevo.com', // varies by provider
    'porta'      => 587,                     // 587 STARTTLS or 465 SSL
    'seguranca'  => 'tls',                   // 'tls' or 'ssl'
    'usuario'    => 'your-smtp-username',
    'senha'      => 'YOUR_PASSWORD_OR_API_KEY',
    'remetente'  => '[email protected]',
    'nome_de'    => 'ViciadosMU Server',
];

Sending function with PHPMailer

Centralize sending in a single reusable function. That way, both password recovery and any other email (welcome, VIP notice) use the same tested path.

<?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;
        // Text version for clients without HTML
        $mail->AltBody = strip_tags($corpoHtml);

        $mail->send();
        return true;
    } catch (Exception $e) {
        error_log('[SMTP] Send failed: ' . $mail->ErrorInfo);
        return false;
    }
}

Test it in isolation before integrating anything. Create a quick script that calls enviarEmail('[email protected]', 'SMTP Test', '<b>It worked!</b>') and confirm you receive it. If it lands in spam already at this test, the problem is in the DNS, not in the code.

Configuring SPF, DKIM and DMARC

This is the step that most impacts deliverability and the one most often ignored. These three DNS records tell email servers that your domain authorizes that SMTP to send on its behalf.

  1. SPF — a TXT record that lists who can send through your domain. The SMTP provider supplies the exact value; something like v=spf1 include:spf.brevo.com ~all.
  2. DKIM — a cryptographic signature. The provider generates a key pair and you publish the public key in a TXT record (or CNAME) on the indicated subdomain.
  3. DMARC — a policy that says what to do with emails that fail SPF/DKIM. Start lenient: v=DMARC1; p=none; rua=mailto:[email protected], and later tighten it to p=quarantine.

After publishing, wait for propagation (it can take hours) and validate with SPF/DKIM checking tools. Only move to production once all three pass.

Preparing the database for the recovery token

The recovery flow needs a place to store the temporary tokens. Create a dedicated table — do not mix it with the game's accounts table.

-- Example (varies by version): recovery tokens table
CREATE TABLE WEB_PASSWORD_RESET (
    id          INT IDENTITY(1,1) PRIMARY KEY,
    conta       VARCHAR(10)  NOT NULL,
    token_hash  CHAR(64)     NOT NULL,   -- sha256 of the token
    expira_em   DATETIME     NOT NULL,
    usado       BIT          NOT NULL DEFAULT 0,
    ip_origem   VARCHAR(45)  NULL,
    criado_em   DATETIME     NOT NULL DEFAULT GETDATE()
);

Storing only the token_hash (and not the raw token) is a deliberate security decision: even with access to the database, no one can assemble the recovery link.

Stage 1: request recovery

The player provides their email. The system looks up the associated account, generates a strong random token, stores the hash and sends the link by email. One important detail: the response to the user must be the same whether or not the account exists, so as not to reveal which emails are registered.

<?php
// solicitar-recuperacao.php
require 'src/Mailer.php';
require 'src/db.php';

function solicitarRecuperacao(string $email): void
{
    $pdo = getPDO();

    // Look up the account by email (column varies by version)
    $stmt = $pdo->prepare("SELECT memb_id FROM MEMB_INFO WHERE mail_addr = ?");
    $stmt->execute([$email]);
    $conta = $stmt->fetchColumn();

    // Always respond the same way, whether or not the account exists
    if ($conta) {
        $token     = bin2hex(random_bytes(32));       // raw token (only in the email)
        $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://yourserver.com/redefinir?conta=" . urlencode($conta)
              . "&token=" . $token;

        $html = "<p>We received a request to reset your password.</p>
                 <p><a href=\"$link\">Click here to create a new password</a>.</p>
                 <p>The link expires in 30 minutes. If this wasn't you, ignore this email.</p>";

        enviarEmail($email, 'Password recovery - ViciadosMU', $html);
    }
    // Neutral message for the user, always
    echo 'If this email is registered, you will receive the instructions shortly.';
}

Stage 2: validate the token and reset the password

When the player clicks the link, the system validates the token against the hash, checks expiration and use, and only then allows setting the new password. At the end, the token is marked as used — never reusable again.

<?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' => 'The password must be at least 6 characters'];
    }

    $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' => 'Invalid link'];
    }
    if ($reg['usado']) {
        return ['ok' => false, 'msg' => 'This link has already been used'];
    }
    if (strtotime($reg['expira_em']) < time()) {
        return ['ok' => false, 'msg' => 'This link has expired. Request a new one.'];
    }

    // Password format VARIES BY VERSION — check your MuServer's
    // Common example: uppercase MD5
    $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' => 'Password changed successfully! You can now log in to the game.'];
}

Using a transaction guarantees that the password is only considered changed if the token is also invalidated in the same move — no window for reuse.

Protecting against abuse

An open recovery form is an invitation to abuse: someone can fire off hundreds of emails or try to guess tokens. A few simple defenses handle most of it:

  • Rate limit per IP and per account — at most, for example, 3 requests every 15 minutes.
  • A long, random token — 32 bytes from random_bytes are impossible to brute-force.
  • CAPTCHA on the request form, to hold off bots.
  • Short expiration — 15 to 30 minutes is enough for the player to act.
  • Invalidation of old tokens — when generating a new one, mark the previous ones for the same account as used.
  • Attempt logging, storing IP and timestamp, to investigate suspicious spikes.

Common errors and fixes

ErrorLikely causeFix
Email always lands in spamMissing SPF/DKIM/DMARCPublish the three DNS records and validate before production
SMTP connect() failedWrong port/security or firewallCheck port 587/465 and open the outbound rule in the server firewall
Could not authenticateWrong SMTP username or passwordGenerate an app password (Gmail) or API key (transactional) and retest
openssl missingExtension disabled in PHPEnable extension=openssl in php.ini and restart
Recovery link does not workComparing raw token with a hashApply hash('sha256', $token) in the validation
Reusable tokenMissing usado = 1 updateInvalidate the token inside the same transaction as the password change
Leak of existing accountsDifferent messages per emailAlways respond with the same neutral message

Testing the full flow

  1. Configure SMTP and send an isolated test email — confirm it reaches the inbox, not spam.
  2. Publish SPF, DKIM and DMARC and re-validate the test.
  3. Request recovery with a registered email and confirm the link arrives.
  4. Request with a nonexistent email and verify the displayed message is identical.
  5. Click the link, set a new password and log in to the game with it.
  6. Try to use the same link again — it must be refused as "already used".
  7. Wait for the expiration and test an expired link — it must ask for a new request.

Launch checklist

  • PHPMailer installed via Composer and openssl enabled
  • SMTP credentials in a file outside version control
  • Test email arriving in the inbox (not spam)
  • SPF, DKIM and DMARC published and validated
  • WEB_PASSWORD_RESET table created
  • Token stored only as a sha256 hash
  • Token expiration configured (15-30 min)
  • Token invalidated after first use (transaction)
  • Neutral message that does not reveal existing accounts
  • Rate limiting and CAPTCHA on the request form
  • Password format confirmed for your MuServer version
  • Full flow tested end to end

Frequently asked questions

Can I use Gmail as the server's SMTP?

You can for testing and small servers, but Gmail imposes daily limits and requires an app password. For production, a transactional provider (Brevo, Mailgun, Amazon SES) delivers better and won't lock your account.

Why do my site's emails land in spam?

Almost always due to missing SPF, DKIM and DMARC on the domain, or sending from an IP with a bad reputation. Setting up these three DNS records is the single step that most improves deliverability.

Does the recovery token need to expire?

Yes. A token with no expiration becomes a permanent key to break into the account. The standard is to expire it in 15 to 60 minutes and invalidate it as soon as it is used once.

Do I need to store the token in plain text in the database?

No, and you shouldn't. Store only the hash of the token. If the database leaks, no one can reconstruct the recovery links from the hashes.

SMTP with SSL or TLS, which should I choose?

Use port 587 with STARTTLS on most modern providers, or port 465 with implicit SSL when the provider requires it. Avoid port 25 without encryption, which is usually blocked and insecure.

GA
Guides & builds editor

Gabriel covers gameplay, class builds, PvP and progression. He tests every strategy on a live server before publishing.

Keep reading

Related articles