Skip to content

Enviar correos con PHPMailer ​

Formularios de contacto, confirmación de pedidos, recuperación de contraseña, avisos al administrador... Enviar correos es un requisito en casi cualquier proyecto para clientes. Aprenderás a hacerlo de forma confiable con PHPMailer y SMTP, para que tus correos lleguen a la bandeja de entrada y no a spam.

Concepto ​

PHP trae una función mail(), pero en la práctica casi no sirve:

  • En Windows con XAMPP no funciona sin configuración extra.
  • En hosting compartido, los correos enviados con mail() suelen ir a spam porque no están autenticados.
  • No soporta fácilmente adjuntos, HTML ni autenticación.

La solución profesional es enviar a través de un servidor SMTP (el "cartero" de los correos), autenticándote con usuario y contraseña, igual que lo hace Outlook o Thunderbird. PHPMailer es la librería más usada para eso.

Opciones de SMTP:

OpciónUso típico
Correo del hosting (cPanel)contacto@dominiodelcliente.com. Lo más común en proyectos pequeños.
Gmail / Google WorkspacePruebas o negocios que ya usan Google. Requiere "contraseña de aplicación".
Servicios transaccionales (Brevo, Mailgun, Amazon SES, Resend, Postmark)Volumen alto, mejores tasas de entrega, estadísticas.
Mailpit / MailtrapDesarrollo: atrapan los correos para que los veas sin enviarlos a nadie.

Instalación ​

bash
composer require phpmailer/phpmailer

Si no puedes usar Composer, descarga el ZIP desde GitHub (PHPMailer/PHPMailer) y requiere manualmente los archivos src/PHPMailer.php, src/SMTP.php y src/Exception.php.

Configuración con .env ​

Nunca escribas la contraseña del correo en el código. Usa variables de entorno (ver Composer y Gestión de secretos):

bash
composer require vlucas/phpdotenv
env
# .env
MAIL_HOST=mail.midominio.com
MAIL_PORT=465
MAIL_USER=contacto@midominio.com
MAIL_PASS=contraseña-segura
MAIL_FROM=contacto@midominio.com
MAIL_FROM_NAME="Florería Lupita"
MAIL_ADMIN=duena@midominio.com

Ejemplo: tu primer correo ​

php
<?php
// enviar-prueba.php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

Dotenv\Dotenv::createImmutable(__DIR__)->load();

// true = lanzar excepciones en caso de error
$mail = new PHPMailer(true);

try {
    // --- Servidor SMTP ---
    $mail->isSMTP();
    $mail->Host       = $_ENV['MAIL_HOST'];
    $mail->SMTPAuth   = true;
    $mail->Username   = $_ENV['MAIL_USER'];
    $mail->Password   = $_ENV['MAIL_PASS'];
    // Puerto 465 = SSL implícito (SMTPS). Puerto 587 = STARTTLS.
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
    $mail->Port       = (int) $_ENV['MAIL_PORT'];
    $mail->CharSet    = PHPMailer::CHARSET_UTF8; // acentos y ñ correctos

    // Descomenta para ver la conversación con el servidor si algo falla
    // $mail->SMTPDebug = SMTP::DEBUG_SERVER;

    // --- Remitente y destinatarios ---
    $mail->setFrom($_ENV['MAIL_FROM'], $_ENV['MAIL_FROM_NAME']);
    $mail->addAddress('cliente@ejemplo.com', 'Ana López');
    $mail->addReplyTo('ventas@midominio.com', 'Ventas');
    // $mail->addCC('copia@ejemplo.com');
    // $mail->addBCC('oculto@ejemplo.com');

    // --- Contenido ---
    $mail->isHTML(true);
    $mail->Subject = '¡Bienvenida a nuestra tienda!';
    $mail->Body    = '<h1>Hola, Ana</h1><p>Gracias por registrarte. Tu cupón es <strong>BIENVENIDA10</strong>.</p>';
    // Versión en texto plano para clientes de correo sin HTML (mejora la entrega)
    $mail->AltBody = "Hola, Ana\nGracias por registrarte. Tu cupón es BIENVENIDA10.";

    $mail->send();
    echo 'Correo enviado';
} catch (Exception $e) {
    // ErrorInfo tiene el detalle técnico: regístralo, no lo muestres al usuario
    error_log('Error de correo: ' . $mail->ErrorInfo);
    echo 'No se pudo enviar el correo.';
}

Configuración según proveedor ​

ProveedorHostPuertoSeguridad
cPanel típicomail.tudominio.com465ENCRYPTION_SMTPS
Gmailsmtp.gmail.com587ENCRYPTION_STARTTLS
Outlook / Microsoft 365smtp.office365.com587ENCRYPTION_STARTTLS
Brevosmtp-relay.brevo.com587ENCRYPTION_STARTTLS
Mailpit (local)localhost1025sin cifrado, SMTPAuth = false

Gmail

Gmail no acepta tu contraseña normal. Activa la verificación en dos pasos y genera una contraseña de aplicación en tu cuenta de Google (Seguridad > Contraseñas de aplicaciones). Úsala como MAIL_PASS.

Una clase reutilizable ​

php
<?php
// src/Correo.php
declare(strict_types=1);

namespace App;

use PHPMailer\PHPMailer\PHPMailer;

final class Correo
{
    /**
     * Crea un PHPMailer ya configurado con los datos del .env
     */
    public static function nuevo(): PHPMailer
    {
        $mail = new PHPMailer(true);
        $mail->isSMTP();
        $mail->Host       = $_ENV['MAIL_HOST'];
        $mail->Port       = (int) $_ENV['MAIL_PORT'];
        $mail->SMTPAuth   = ($_ENV['MAIL_USER'] ?? '') !== '';
        $mail->Username   = $_ENV['MAIL_USER'] ?? '';
        $mail->Password   = $_ENV['MAIL_PASS'] ?? '';
        $mail->SMTPSecure = match ((int) $_ENV['MAIL_PORT']) {
            465     => PHPMailer::ENCRYPTION_SMTPS,
            587     => PHPMailer::ENCRYPTION_STARTTLS,
            default => '', // p. ej. Mailpit en desarrollo
        };
        $mail->CharSet = PHPMailer::CHARSET_UTF8;
        $mail->setFrom($_ENV['MAIL_FROM'], $_ENV['MAIL_FROM_NAME'] ?? '');
        $mail->isHTML(true);
        return $mail;
    }

    /**
     * Renderiza una plantilla PHP con variables y devuelve el HTML
     */
    public static function plantilla(string $nombre, array $datos = []): string
    {
        extract($datos, EXTR_SKIP); // convierte ['nombre' => 'Ana'] en $nombre
        ob_start();                 // captura la salida en lugar de imprimirla
        require __DIR__ . "/../plantillas/correos/$nombre.php";
        return ob_get_clean();
    }
}

Plantilla de correo (los clientes de correo sólo entienden HTML sencillo con estilos en línea):

php
<?php // plantillas/correos/pedido.php ?>
<div style="font-family:Arial,sans-serif;max-width:600px;margin:auto;color:#333">
    <h2 style="color:#2563eb">¡Gracias por tu compra, <?= htmlspecialchars($nombre) ?>!</h2>
    <p>Tu pedido <strong>#<?= (int) $pedidoId ?></strong> fue recibido.</p>
    <table style="width:100%;border-collapse:collapse">
        <?php foreach ($productos as $p): ?>
        <tr>
            <td style="padding:8px;border-bottom:1px solid #eee"><?= htmlspecialchars($p['nombre']) ?> × <?= (int) $p['cantidad'] ?></td>
            <td style="padding:8px;border-bottom:1px solid #eee;text-align:right">$<?= number_format($p['precio'] * $p['cantidad'], 2) ?></td>
        </tr>
        <?php endforeach; ?>
    </table>
    <p style="font-size:18px;text-align:right"><strong>Total: $<?= number_format($total, 2) ?></strong></p>
    <p style="font-size:12px;color:#888">Si tienes dudas, responde a este correo.</p>
</div>

Uso:

php
<?php
use App\Correo;

$mail = Correo::nuevo();
$mail->addAddress('ana@correo.com', 'Ana');
$mail->Subject = 'Confirmación de pedido #1001';
$mail->Body = Correo::plantilla('pedido', [
    'nombre'    => 'Ana',
    'pedidoId'  => 1001,
    'productos' => [['nombre' => 'Café', 'cantidad' => 2, 'precio' => 180]],
    'total'     => 360,
]);
$mail->AltBody = 'Tu pedido #1001 fue recibido. Total: $360.00';
$mail->send();

Caso real: formulario de contacto ​

php
<?php
// contacto.php
declare(strict_types=1);
session_start();
require __DIR__ . '/vendor/autoload.php';
Dotenv\Dotenv::createImmutable(__DIR__)->load();

use App\Correo;
use PHPMailer\PHPMailer\Exception;

$errores = [];
$enviado = false;
$datos = ['nombre' => '', 'email' => '', 'mensaje' => ''];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Honeypot: campo oculto que los humanos no ven y los bots sí llenan
    if (!empty($_POST['sitio_web'])) {
        exit; // es un bot: no hacemos nada
    }

    // Límite simple: un envío cada 60 segundos por sesión
    if (time() - ($_SESSION['ultimo_contacto'] ?? 0) < 60) {
        $errores['general'] = 'Espera un minuto antes de enviar otro mensaje.';
    }

    $datos = [
        'nombre'  => trim($_POST['nombre'] ?? ''),
        'email'   => trim($_POST['email'] ?? ''),
        'mensaje' => trim($_POST['mensaje'] ?? ''),
    ];

    if (mb_strlen($datos['nombre']) < 2)  $errores['nombre']  = 'Escribe tu nombre.';
    if (!filter_var($datos['email'], FILTER_VALIDATE_EMAIL)) $errores['email'] = 'Email inválido.';
    if (mb_strlen($datos['mensaje']) < 10) $errores['mensaje'] = 'El mensaje es muy corto.';

    if (!$errores) {
        try {
            $mail = Correo::nuevo();
            $mail->addAddress($_ENV['MAIL_ADMIN']);
            // Reply-To al visitante: al pulsar "Responder", le escribes a él.
            // El From sigue siendo TU dominio (si pones el email del visitante, irá a spam).
            $mail->addReplyTo($datos['email'], $datos['nombre']);
            $mail->Subject = 'Nuevo mensaje de contacto de ' . $datos['nombre'];
            $mail->Body = '<p><strong>Nombre:</strong> ' . htmlspecialchars($datos['nombre']) . '</p>'
                . '<p><strong>Email:</strong> ' . htmlspecialchars($datos['email']) . '</p>'
                . '<p>' . nl2br(htmlspecialchars($datos['mensaje'])) . '</p>';
            $mail->AltBody = "Nombre: {$datos['nombre']}\nEmail: {$datos['email']}\n\n{$datos['mensaje']}";
            $mail->send();

            $_SESSION['ultimo_contacto'] = time();
            $enviado = true;
        } catch (Exception $e) {
            error_log('Contacto: ' . $e->getMessage());
            $errores['general'] = 'No pudimos enviar tu mensaje. Escríbenos por WhatsApp.';
        }
    }
}
?>
<?php if ($enviado): ?>
    <p>¡Gracias! Te responderemos pronto.</p>
<?php else: ?>
    <?php if (isset($errores['general'])): ?><p style="color:red"><?= htmlspecialchars($errores['general']) ?></p><?php endif; ?>
    <form method="post">
        <!-- Honeypot oculto con CSS -->
        <input type="text" name="sitio_web" style="display:none" tabindex="-1" autocomplete="off">

        <input name="nombre" placeholder="Nombre" value="<?= htmlspecialchars($datos['nombre']) ?>">
        <small><?= $errores['nombre'] ?? '' ?></small>

        <input name="email" type="email" placeholder="Email" value="<?= htmlspecialchars($datos['email']) ?>">
        <small><?= $errores['email'] ?? '' ?></small>

        <textarea name="mensaje" placeholder="¿En qué te ayudamos?"><?= htmlspecialchars($datos['mensaje']) ?></textarea>
        <small><?= $errores['mensaje'] ?? '' ?></small>

        <button>Enviar</button>
    </form>
<?php endif; ?>

Adjuntos e imágenes incrustadas ​

php
<?php
// Adjuntar un archivo del servidor
$mail->addAttachment(__DIR__ . '/facturas/F-1001.pdf', 'Factura-1001.pdf');

// Adjuntar contenido generado en memoria (por ejemplo, un PDF creado con Dompdf)
$mail->addStringAttachment($contenidoPdf, 'cotizacion.pdf', PHPMailer::ENCODING_BASE64, 'application/pdf');

// Incrustar una imagen y referenciarla en el HTML con cid:
$mail->addEmbeddedImage(__DIR__ . '/img/logo.png', 'logo');
$mail->Body = '<img src="cid:logo" alt="Logo" width="120"><p>...</p>';

Probar en desarrollo con Mailpit ​

Mailpit es un servidor SMTP falso con una bandeja web. Descarga el ejecutable desde su repositorio de GitHub (axllent/mailpit), ejecútalo y configura:

env
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USER=
MAIL_PASS=

Abre http://localhost:8025 y verás todos los correos que envía tu aplicación, sin riesgo de escribirle a clientes reales.

Que no lleguen a spam ​

  • Envía siempre desde el dominio del sitio (From: contacto@dominio.com), autenticado por SMTP.
  • Configura en el DNS del dominio los registros SPF, DKIM y DMARC. En cPanel: "Email Deliverability" los configura casi automáticamente.
  • Incluye siempre AltBody (versión texto).
  • Evita asuntos en mayúsculas o llenos de signos ("¡¡¡OFERTA GRATIS!!!").
  • Para envíos masivos (newsletters) usa un servicio especializado, no el SMTP del hosting.

Error común

Poner el email del visitante como remitente (setFrom($_POST['email'])). Tu servidor no está autorizado a enviar en nombre de gmail.com, así que el correo será rechazado o irá a spam. Usa tu dominio en setFrom y el del visitante en addReplyTo.

Error común

Insertar datos del usuario en el HTML del correo sin escapar. Un mensaje con código HTML malicioso se renderizará en el cliente de correo del administrador. Usa htmlspecialchars también en los correos.

Ejercicios ​

  1. Crea un script que envíe a un correo de prueba un mensaje con un adjunto (cualquier PDF) y una imagen incrustada.
  2. Crea la plantilla plantillas/correos/bienvenida.php y una función enviarBienvenida(string $email, string $nombre): bool que devuelva true o false sin lanzar excepciones.
  3. Modifica el formulario de contacto para que, además de avisar al administrador, envíe un correo de confirmación automática al visitante.
Ver solución
php
<?php
// 1
require __DIR__ . '/vendor/autoload.php';
Dotenv\Dotenv::createImmutable(__DIR__)->load();
use App\Correo;

$mail = Correo::nuevo();
$mail->addAddress('pruebas@midominio.com');
$mail->Subject = 'Prueba con adjunto';
$mail->addAttachment(__DIR__ . '/docs/manual.pdf');
$mail->addEmbeddedImage(__DIR__ . '/img/logo.png', 'logo');
$mail->Body = '<img src="cid:logo" width="100" alt="Logo"><p>Te adjunto el manual.</p>';
$mail->AltBody = 'Te adjunto el manual.';
$mail->send();
echo 'Enviado';
php
<?php // plantillas/correos/bienvenida.php ?>
<div style="font-family:Arial,sans-serif">
    <h2>¡Hola, <?= htmlspecialchars($nombre) ?>!</h2>
    <p>Tu cuenta ya está activa. Puedes iniciar sesión cuando quieras.</p>
</div>
php
<?php
// 2
use App\Correo;
use PHPMailer\PHPMailer\Exception;

function enviarBienvenida(string $email, string $nombre): bool
{
    try {
        $mail = Correo::nuevo();
        $mail->addAddress($email, $nombre);
        $mail->Subject = "Bienvenido, $nombre";
        $mail->Body = Correo::plantilla('bienvenida', ['nombre' => $nombre]);
        $mail->AltBody = "Hola, $nombre. Tu cuenta ya está activa.";
        return $mail->send();
    } catch (Exception $e) {
        error_log("Bienvenida a $email falló: " . $e->getMessage());
        return false;
    }
}
php
<?php
// 3: dentro del try del formulario, tras enviar al admin
$confirmacion = Correo::nuevo();
$confirmacion->addAddress($datos['email'], $datos['nombre']);
$confirmacion->Subject = 'Recibimos tu mensaje';
$confirmacion->Body = '<p>Hola, ' . htmlspecialchars($datos['nombre'])
    . '. Recibimos tu mensaje y te responderemos en menos de 24 horas.</p>';
$confirmacion->AltBody = "Hola, {$datos['nombre']}. Recibimos tu mensaje.";
$confirmacion->send();

Resumen ​

  • Evita mail(): usa PHPMailer con SMTP autenticado.
  • Guarda credenciales en .env, nunca en el código.
  • Puerto 465 con ENCRYPTION_SMTPS, 587 con ENCRYPTION_STARTTLS; CharSet en UTF-8.
  • Remitente siempre de tu dominio; el visitante va en addReplyTo.
  • Incluye AltBody, escapa los datos del usuario y configura SPF, DKIM y DMARC.
  • En desarrollo usa Mailpit para no enviar correos reales.

Academia Web — aprende, copia y construye.