Skip to content

Correos con nodemailer ​

nodemailer es la librería estándar para enviar correos desde Node.js por SMTP. Este snippet crea un servicio reutilizable con plantillas HTML, versión en texto plano, adjuntos y un modo de pruebas que no envía correos reales.

Instalación ​

bash
npm install nodemailer
env
SMTP_HOST=smtp.tudominio.com
SMTP_PORT=587
SMTP_SECURE=false        # true para el puerto 465
SMTP_USER=no-responder@tudominio.com
SMTP_PASS=contraseña
MAIL_FROM="Mi Negocio <no-responder@tudominio.com>"

Servicio de correo ​

js
import nodemailer from 'nodemailer';
import 'dotenv/config';

let transporter;

/** Crea el transporte una sola vez (y una cuenta de prueba si no hay SMTP configurado) */
async function obtenerTransporte() {
  if (transporter) return transporter;

  if (!process.env.SMTP_HOST) {
    // Modo desarrollo: cuenta falsa de Ethereal (los correos se ven en una URL)
    const prueba = await nodemailer.createTestAccount();
    transporter = nodemailer.createTransport({
      host: 'smtp.ethereal.email', port: 587,
      auth: { user: prueba.user, pass: prueba.pass },
    });
    console.log('📧 Usando Ethereal (correos de prueba)');
  } else {
    transporter = nodemailer.createTransport({
      host: process.env.SMTP_HOST,
      port: Number(process.env.SMTP_PORT ?? 587),
      secure: process.env.SMTP_SECURE === 'true',
      auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
      pool: true,          // reutiliza conexiones si envías muchos
      maxConnections: 3,
    });
    await transporter.verify(); // falla rápido si las credenciales están mal
  }
  return transporter;
}

const escapar = (t = '') => String(t).replace(/[&<>"']/g, (c) => (
  { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
));

/** Diseño base compatible con clientes de correo (tablas + estilos en línea) */
function plantilla({ titulo, contenido, boton }) {
  const btn = boton
    ? `<p style="margin:28px 0"><a href="${boton.url}" style="background:#4f46e5;color:#fff;padding:12px 22px;border-radius:8px;text-decoration:none;font-weight:bold">${escapar(boton.texto)}</a></p>`
    : '';
  return `<!DOCTYPE html><html lang="es"><body style="margin:0;background:#f1f5f9;font-family:Arial,sans-serif">
  <table width="100%" cellpadding="0" cellspacing="0" style="padding:24px 0"><tr><td align="center">
    <table width="600" style="max-width:600px;background:#fff;border-radius:12px;overflow:hidden">
      <tr><td style="background:#4f46e5;color:#fff;padding:20px 28px;font-size:20px;font-weight:bold">Mi Negocio</td></tr>
      <tr><td style="padding:28px;color:#1e293b;font-size:15px;line-height:1.6">
        <h2 style="margin-top:0">${escapar(titulo)}</h2>${contenido}${btn}
      </td></tr>
      <tr><td style="padding:16px 28px;background:#f8fafc;color:#64748b;font-size:12px">© ${new Date().getFullYear()} Mi Negocio · Ciudad de México</td></tr>
    </table>
  </td></tr></table></body></html>`;
}

/**
 * Envía un correo.
 * @param {{ para: string|string[], asunto: string, titulo?: string, html: string,
 *           boton?: {texto: string, url: string}, adjuntos?: object[], responderA?: string }} opciones
 */
export async function enviarCorreo({ para, asunto, titulo, html, boton, adjuntos = [], responderA }) {
  const t = await obtenerTransporte();
  const info = await t.sendMail({
    from: process.env.MAIL_FROM ?? 'Mi Negocio <no-responder@ejemplo.com>',
    to: para,
    replyTo: responderA,
    subject: asunto,
    html: plantilla({ titulo: titulo ?? asunto, contenido: html, boton }),
    text: html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(), // versión texto
    attachments: adjuntos, // [{ filename: 'factura.pdf', path: './facturas/1.pdf' }]
  });

  const vistaPrevia = nodemailer.getTestMessageUrl(info);
  if (vistaPrevia) console.log('Vista previa del correo:', vistaPrevia);
  return info;
}

// Correos frecuentes listos para usar
export const correos = {
  bienvenida: (usuario) => enviarCorreo({
    para: usuario.email,
    asunto: '¡Bienvenido a Mi Negocio!',
    html: `<p>Hola ${escapar(usuario.nombre)}, gracias por registrarte.</p>`,
    boton: { texto: 'Ir a mi cuenta', url: `${process.env.APP_URL}/cuenta` },
  }),

  restablecer: (usuario, token) => enviarCorreo({
    para: usuario.email,
    asunto: 'Restablece tu contraseña',
    html: '<p>Recibimos una solicitud para cambiar tu contraseña. El enlace caduca en 1 hora.</p><p>Si no fuiste tú, ignora este mensaje.</p>',
    boton: { texto: 'Crear nueva contraseña', url: `${process.env.APP_URL}/restablecer?token=${token}` },
  }),

  confirmacionPedido: (pedido) => enviarCorreo({
    para: pedido.email,
    asunto: `Pedido #${pedido.id} confirmado`,
    html: `<p>Recibimos tu pedido por <b>${new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(pedido.total)}</b>.</p>`,
  }),
};

Uso en una ruta ​

js
import { correos, enviarCorreo } from '../utils/correo.js';

// Tras registrar: no hagas esperar al usuario por el correo
correos.bienvenida(usuario).catch((err) => console.error('No se envió la bienvenida', err));

// Formulario de contacto
router.post('/contacto', asyncHandler(async (req, res) => {
  const { nombre, email, mensaje } = req.body; // valídalo con zod antes
  await enviarCorreo({
    para: process.env.CONTACTO_DESTINO,
    asunto: `Nuevo mensaje de ${nombre}`,
    html: `<p><b>De:</b> ${nombre} (${email})</p><p>${mensaje}</p>`,
    responderA: email,
  });
  res.json({ ok: true, mensaje: 'Gracias, te responderemos pronto' });
}));

Escapa siempre

En el ejemplo de contacto, pasa nombre y mensaje por la función escapar() (expórtala) antes de meterlos en el HTML; si no, alguien puede inyectar enlaces o HTML en los correos que recibe tu cliente.

Envíos masivos o críticos

Para cientos de correos (boletines, notificaciones) usa una cola (BullMQ + Redis) o un servicio transaccional con API (Resend, Brevo, Amazon SES, Postmark). Mejoran la entregabilidad y te dan estadísticas de apertura y rebotes.

Error común

Con Gmail, usar tu contraseña normal. Google la rechaza: necesitas activar la verificación en dos pasos y crear una contraseña de aplicación. Y Gmail limita a unos 500 correos diarios.

Academia Web — aprende, copia y construye.