Cómo enviar correo electrónico con Nodemailer

Aprenda a configurar un servidor SMTP y enviar correo electrónico desde su aplicación utilizando Nodemailer. También aprenda a usar EJS para crear plantillas HTML dinámicas para enviar correos electrónicos.

Para comenzar, necesitamos instalar el paquete nodemailer a través de NPM:

npm install nodemailer

Esto agregará Nodemailer a su aplicación. Si está utilizando una versión reciente de NPM, esto también debería agregar nodemailer como una dependencia en el package.json de tu aplicación archivo.

Elegir un proveedor de SMTP

Antes de seguir adelante, debemos asegurarnos de tener acceso a un proveedor de SMTP. Un proveedor SMTP es un servicio que proporciona acceso al servidor SMTP que necesitamos para enviar físicamente nuestros correos electrónicos. Mientras puedas crear un servidor SMTP por su cuenta, por lo general es más problemático de lo que vale debido al cumplimiento normativo y la sobrecarga técnica.

SMTP significa Protocolo simple de transferencia de correo. Es un protocolo de comunicación estándar de Internet que describe el protocolo utilizado para enviar correos electrónicos a través de Internet.

Cuando se trata de usar SMTP en su aplicación, el estándar es usar un servicio SMTP de terceros para manejar las partes técnicas y de cumplimiento por usted para que pueda concentrarse en su aplicación. Hay muchos proveedores de SMTP diferentes, cada uno con sus propias ventajas, desventajas y costos.

¿Nuestra recomendación? Matasellos. Es un servicio pago, sin embargo, tiene una excelente interfaz de usuario y una excelente documentación que le ahorra mucho tiempo y problemas. Si está tratando de evitar pagar, un servicio alternativo y comparable es Mailgun.

Antes de continuar, configure una cuenta con Postmark y luego siga este tutorial rápido para acceder a sus credenciales SMTP (las necesitaremos a continuación).

Alternativamente, configure una cuenta con Mailgun y luego siga este tutorial para acceder a sus credenciales SMTP.

Una vez que tenga listos su proveedor de SMTP y sus credenciales, sigamos avanzando.

Configuración de su servidor SMTP

Antes de comenzar a enviar correo electrónico, el primer paso es configurar un transporte SMTP. Un transporte es el término que utiliza Nodemailer para describir el método que utilizará para enviar tu correo electrónico.

import nodemailer from 'nodemailer';

const smtp = nodemailer.createTransport({
  host: '',
  port: 587,
  secure: process.env.NODE_ENV !== "development",
  auth: {
    user: '',
    pass: '',
  },
});

Primero, importamos nodemailer del nodemailer paquete que instalamos arriba. A continuación, definimos una variable const smtp y asignarlo a una llamada a nodemailer.createTransport() . Esta es la parte importante.

Aquí, estamos pasando un objeto de opciones que le dice a Nodemailer qué servicio SMTP queremos usar para enviar nuestro correo electrónico.

Espera, ¿no estamos enviando correos electrónicos usando nuestra aplicación?

Técnicamente, sí. Pero el envío de correo electrónico en Internet requiere un servidor SMTP en funcionamiento. Con Nodemailer, no estamos creando un servidor, sino un cliente SMTP . La diferencia es que un servidor actúa como el remitente real. (en el sentido técnico), mientras que el cliente se conecta al servidor para usarlo como un repetidor para realizar el envío real.

En nuestra aplicación, llamando nodemailer.createTransport() establece la conexión del cliente con nuestro proveedor SMTP.

Usando las credenciales que obtuvo de su proveedor SMTP anteriormente, actualicemos este objeto de opciones. Si bien es posible que no sean exactos, su proveedor de SMTP debe usar una terminología similar para describir cada una de las configuraciones que debemos aprobar:

{
  host: 'smtp.postmarkapp.com',
  port: 587,
  secure: process.env.NODE_ENV !== "development",
  auth: {
    user: 'postmark-api-key-123',
    pass: 'postmark-api-key-123',
  },
}

Aquí, queremos reemplazar host , port , y el user y pass bajo el auth anidado objeto.

host debería verse algo como smtp.postmarkapp.com . port debe establecerse en 587 (el puerto seguro para enviar correo electrónico con SMTP).

Vuelva a verificar y asegúrese de tener la configuración correcta y luego estamos listos para continuar con el envío.

Envío de correo electrónico

Enviar correo electrónico con Nodemailer es sencillo:todo lo que tenemos que hacer es llamar al sendMail método en el valor devuelto por nodemailer.createTransport() que almacenamos en el smtp variable anterior, así:

smtp.sendMail({ ... })

A continuación, debemos pasar la configuración de mensajes adecuada para enviar nuestro correo electrónico. El objeto de configuración del mensaje se pasa a smtp.sendMail() y contiene configuraciones como to , from , subject y html .

Como ejemplo rápido, pasemos la configuración mínima que necesitaremos para enviar un correo electrónico:

[...]

smtp.sendMail({
  to: '[email protected]',
  from: '[email protected]',
  subject: 'Testing Email Sends',
  html: '<p>Sending some HTML to test.</p>',
});

Bastante claro. Aquí pasamos un to , from , subject y html configuración para especificar a quién se dirige nuestro correo electrónico, de dónde proviene, un asunto para ayudar al destinatario a identificar el correo electrónico y algo de HTML para enviar en el cuerpo del correo electrónico.

¡Eso es todo! Bueno, eso es lo básico versión. Si echa un vistazo a la documentación de configuración de mensajes para Nodemailer, verá que hay varias opciones que puede pasar.

Para asegurarnos de que todo esto quede claro, veamos nuestro código de ejemplo completo hasta ahora:

import nodemailer from 'nodemailer';

const smtp = nodemailer.createTransport({
  host: 'smtp.someprovider.com',
  port: 587,
  secure: process.env.NODE_ENV !== "development",
  auth: {
    user: 'smtp-username',
    pass: 'smtp-password',
  },
});

smtp.sendMail({
  to: '[email protected]',
  from: '[email protected]',
  subject: 'Testing Email Sends',
  html: '<p>Sending some HTML to test.</p>',
});

Ahora, aunque esto técnicamente funcionará, si lo copiamos y pegamos palabra por palabra en un archivo sin formato, cuando ejecutemos el código, enviaremos nuestro correo electrónico de inmediato. Eso es probablemente un gran ups.

Modifiquemos ligeramente este código:

import nodemailer from 'nodemailer';

const smtp = nodemailer.createTransport({
  host: 'smtp.someprovider.com',
  port: 587,
  secure: process.env.NODE_ENV !== "development",
  auth: {
    user: 'smtp-username',
    pass: 'smtp-password',
  },
});

export default (options = {}) => {
  return smtp.sendMail(options);
}

¡Esperar! ¿Adónde fueron nuestras opciones de ejemplo?

Es muy poco probable que queramos enviar un correo electrónico tan pronto como se inicie nuestra aplicación. Para que podamos enviar un correo electrónico manualmente, aquí envolvemos nuestra llamada a smtp.sendMail() con otra función que toma un options objeto como argumento.

¿Puedes adivinar qué contiene ese objeto de opciones? Así es, nuestras opciones faltantes.

La diferencia entre este código y el anterior es que podemos importar este archivo en otra parte de nuestra aplicación, llamando a la función exportada en el punto donde queremos enviar nuestro correo electrónico.

Por ejemplo, supongamos que el código anterior vive en la ruta /lib/email/send.js en nuestra aplicación:

import sendEmail from '/lib/email/send.js';
import generateId from '/lib/generateId.js';

export default {
  createCustomer: (parent, args, context) => {
    const customerId = generateId();
    await Customers.insertOne({ _id: customerId, ...args.customer });
    
    await sendEmail({
      to: '[email protected]',
      from: '[email protected]',
      subject: 'You have a new customer!',
      text: 'Hooray! A new customer has signed up for the app.',
    });

    return true;
  },
};

Esto debería parecer familiar. Nuevamente, estamos usando exactamente el mismo objeto de configuración de mensajes de Nodemailer aquí. La única diferencia es que ahora, Nodemailer no enviará nuestro correo electrónico hasta que llamemos al sendEmail() función.

Impresionante. Entonces, ahora que sabemos cómo enviar correos electrónicos, demos un paso más y hagámoslo más utilizable en nuestra aplicación.

Creación de plantillas dinámicas con EJS

Si es un suscriptor Pro y tiene acceso al repositorio de este tutorial, notará que esta funcionalidad está integrada en el modelo en el que se basa el repositorio, el modelo CheatCode Node.js.

La diferencia entre ese código y los ejemplos que hemos visto hasta ahora es que incluye una función especial:la capacidad de definir plantillas HTML personalizadas y hacer que se compilen automáticamente con datos dinámicos pasados ​​cuando llamamos a sendEmail .

Echemos un vistazo a toda la configuración y analicemos.

/lib/email/send.js

import nodemailer from "nodemailer";
import fs from "fs";
import ejs from "ejs";
import { htmlToText } from "html-to-text";
import juice from "juice";
import settings from "../settings";

const smtp = nodemailer.createTransport({
  host: settings?.smtp?.host,
  port: settings?.smtp?.port,
  secure: process.env.NODE_ENV !== "development",
  auth: {
    user: settings?.smtp?.username,
    pass: settings?.smtp?.password,
  },
});

export default ({ template: templateName, templateVars, ...restOfOptions }) => {
  const templatePath = `lib/email/templates/${templateName}.html`;
  const options = {
    ...restOfOptions,
  };

  if (templateName && fs.existsSync(templatePath)) {
    const template = fs.readFileSync(templatePath, "utf-8");
    const html = ejs.render(template, templateVars);
    const text = htmlToText(html);
    const htmlWithStylesInlined = juice(html);

    options.html = htmlWithStylesInlined;
    options.text = text;
  }

  return smtp.sendMail(options);
};

Hay muchos extras aquí, así que centrémonos primero en las cosas familiares.

Comenzando con la llamada a nodemailer.createTransport() , observe que estamos llamando exactamente al mismo código anterior. La única diferencia es que aquí, en lugar de pasar nuestra configuración directamente, confiamos en la convención de configuración integrada en CheatCode Node.js Boilerplate.

A continuación, queremos ver la parte inferior del archivo. Esa llamada a smtp.sendMail(options) debe parecer familiar. De hecho, este es exactamente el mismo patrón que vimos arriba cuando envolvimos nuestra llamada en la función que tomó el objeto de opciones.

Agregar la funcionalidad de plantillas

Ahora para la parte difícil. Notará que hemos agregado bastantes importaciones en la parte superior de nuestro archivo. Además de nodemailer , hemos añadido:

  • fs - No requiere instalación. Este es el paquete del sistema de archivos integrado en el núcleo de Node.js. Nos da acceso al sistema de archivos para cosas como leer y escribir archivos.
  • ejs - La biblioteca que usaremos para reemplazar el contenido dinámico dentro de nuestra plantilla de correo electrónico HTML.
  • html-to-text - Una biblioteca que usaremos para convertir automáticamente nuestro HTML compilado en texto para mejorar la accesibilidad de nuestros correos electrónicos para los usuarios.
  • juice - Una biblioteca utilizada para insertar automáticamente cualquier <style></style> etiquetas en nuestra plantilla de correo electrónico HTML.

Si no está utilizando CheatCode Node.js Boilerplate, continúe e instale las últimas tres dependencias ahora:

npm install ejs html-to-text juice

Ahora, veamos un poco más de cerca la función que se exporta al final de este ejemplo. Esta función es técnicamente idéntica a la función contenedora que vimos anteriormente, con una gran diferencia:ahora anticipamos un posible template y templateVars valor que se pasa además de la configuración del mensaje que hemos visto hasta ahora.

En lugar de simplemente tomar el options Sin embargo, usamos la desestructuración de objetos de JavaScript para "arrancar" las propiedades que queremos del objeto de opciones, algo así como uvas. Una vez que tengamos el template y templateVars properties (uvas), recopilamos el resto de opciones en una nueva variable llamada restOfOptions usando el ... Operador de propagación de JavaScript.

A continuación, justo dentro del cuerpo de la función en la parte superior de la función definimos una variable templatePath que apunta a la ubicación planificada de nuestras plantillas de correo electrónico HTML:/lib/email/templates/${templateName}.html .

Aquí, pasamos el templateName propiedad que desestructuramos del options objeto pasado a nuestra nueva función (nuevamente, la que ya está incluida en CheatCode Node.js Boilerplate). Es importante tener en cuenta :aunque estemos usando el nombre templateName aquí, ese valor se asigna al objeto de opciones que pasamos como template .

¿Por qué el cambio de nombre? Bueno, si miramos un poco más abajo, queremos asegurarnos de que el nombre de la variable template todavía es accesible para nosotros. Entonces, aprovechamos la capacidad de cambiar el nombre de las propiedades desestructuradas en JavaScript escribiendo { template: templateName } . Aquí, el : después de template le dice a JavaScript que queremos asignar el valor de esa variable a un nuevo nombre, en el ámbito de nuestra función actual.

Para ser claros:no cambiando o mutando permanentemente el objeto de opciones aquí. Solo estamos cambiando el nombre, dándole un alias, temporalmente dentro del cuerpo de esta función; en ningún otro lugar.

A continuación, una vez que tengamos la ruta de la plantilla, nos pondremos manos a la obra.

Primero, configuramos un nuevo options objeto que contiene la versión "desempaquetada" de nuestro restOfOptions variable utilizando el operador de propagación de JavaScript. Hacemos esto aquí porque en este punto, solo podemos saber con certeza que el objeto de opciones pasado a nuestra función contiene las opciones de configuración de mensajes de Nodemailer.

Para determinar si estamos enviando nuestro correo electrónico usando una plantilla, escribimos un if declaración para decir "si hay un templateName presente y fs.existsSync(templatePath) devuelve verdadero para el templatePath escribimos arriba, supongamos que tenemos una plantilla para compilar".

Si templateName o el fs.existsSync() Si la comprobación fallara, omitiríamos la compilación de cualquier plantilla y entregaríamos nuestro options objeto directamente a smtp.sendMail() .

Sin embargo, si hacemos tiene una plantilla y existe en la ruta, a continuación, usamos fs.readFileSync() para obtener los contenidos sin procesar de la plantilla HTML y almacenarlos en el template variable. A continuación, usamos el ejs.render() , pasando la plantilla HTML en la que queremos reemplazar el contenido, seguido del templateVars objeto que contiene los reemplazos para ese archivo.

Porque estamos escribiendo nuestro código para admitir cualquier plantilla (no una específica), echemos un vistazo rápido a una plantilla HTML de ejemplo para asegurarnos de que esto no sea confuso:

/lib/email/templates/reset-password.html

<html>
  <head>
    <title>Reset Password</title>
  </head>
  <style>
    body {
      color: #000;
      font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
      font-size: 16px;
      line-height: 24px;
    }
  </style>
  <body>
    <p>Hello,</p>
    <p>A password reset was requested for this email address (<%= emailAddress %>). If you requested this reset, click the link below to reset your password:</p>
    <p><a href="<%= resetLink %>">Reset Your Password</a></p>
  </body>
</html>

Aquí, tenemos un archivo HTML sin formato con un <style></style> etiqueta que contiene algunos colores genéricos y estilos de fuente y un breve <body></body> que contiene el contenido de nuestro correo electrónico.

Tenga en cuenta que dentro tenemos algunas etiquetas HTML extrañas y no estándar como <%= emailAddress => . Se conocen como etiquetas EJS y están diseñadas para ser marcadores de posición donde EJS "escupirá" los valores correspondientes de nuestro templateVars objeto en la plantilla.

En otras palabras, si nuestro templateVars el objeto se ve así:

{
  emailAddress: '[email protected]',
  resetLink: 'https://justatest.com',
}

Esperaríamos recuperar HTML como este de EJS:

<body>
  <p>Hello,</p>
  <p>A password reset was requested for this email address ([email protected]). If you requested this reset, click the link below to reset your password:</p>
  <p><a href="https://justatest.com">Reset Your Password</a></p>
</body>

Ahora, de vuelta en nuestro código JavaScript, después de recuperar nuestro html cadena de ejs.render() , se lo pasamos al htmlToText() que importamos para recuperar una cadena de texto sin formato sin HTML (nuevamente, esto se usa para la accesibilidad:los clientes de correo electrónico recurren al text versión de un correo electrónico en caso de que haya un problema con la versión HTML).

Finalmente, tomamos el html una vez más y pásalo a juice() para alinear el <style></style> etiqueta que vimos en la parte superior. La inserción es el proceso de agregar estilos contenidos en un <style></style> etiquetar directamente a un elemento HTML a través de su style atributo. Esto se hace para garantizar que los estilos sean compatibles con todos los clientes de correo electrónico que, lamentablemente, están por todas partes.

Una vez que tengamos nuestro htmlWithStylesInlined compilado y nuestro text , como paso final, en la parte inferior de nuestro if declaración, asignamos options.html y options.text a nuestro htmlWithStylesInlined y nuestro text valores, respectivamente.

¡Hecho! Ahora, cuando llamamos a nuestra función, podemos pasar un template nombre (correspondiente al nombre del archivo HTML en el /lib/email/templates directorio) junto con algo de templateVars para enviar un correo electrónico HTML renderizado dinámicamente a nuestros usuarios.

Echemos un vistazo al uso de esta función para terminar:

await sendEmail({
  to: args.emailAddress,
  from: settings?.support?.email,
  subject: "Reset Your Password",
  template: "reset-password",
  templateVars: {
    emailAddress: args.emailAddress,
    resetLink,
  },
});

Casi idéntico a lo que vimos antes, pero nota:esta vez pasamos un template nombre y templateVars para señalar a nuestra función que queremos usar el reset-password.html plantilla y reemplazar sus etiquetas EJS con los valores en el templateVars objeto.

¿Tener sentido? Si no es así, siéntete libre de compartir un comentario a continuación y te ayudaremos.