Nodemailer is the most widely used module for sending email from Node.js. You install it with npm, create a transporter that holds your SMTP settings, and call sendMail. This guide shows a complete working example, the correct settings for ports 587 and 465, HTML and attachments, how to test the connection, how to handle errors, and how to avoid the mistakes that send Node.js mail to spam. It also explains why you cannot safely send email from JavaScript running in the browser.
Install Nodemailer and send your first email
Create a project, install the package and put your SMTP details in environment variables. The values below are placeholders. Use the host, user name and password from your SMTP provider's dashboard.
npm install nodemailer
// send.js
const nodemailer = require("nodemailer");
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, // e.g. smtp.example.com
port: 587,
secure: false, // false = STARTTLS on 587
requireTLS: true, // refuse to continue without encryption
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }
});
async function main() {
const info = await transporter.sendMail({
from: '"Example App" <[email protected]>',
to: "[email protected]",
subject: "Welcome",
text: "Thanks for signing up.",
html: "<p>Thanks for signing up.</p>"
});
console.log("Accepted:", info.accepted, "Message id:", info.messageId);
}
main().catch(console.error);
Run it with node send.js. If you use ES modules, import it with import nodemailer from "nodemailer".
The correct secure setting for each port
The most common Nodemailer error comes from mixing up the secure option and the port. Match them as follows.
| Port | secure | What happens |
|---|---|---|
| 587 | false | The connection starts in plain text and upgrades with STARTTLS. Add requireTLS: true so it never continues unencrypted |
| 465 | true | The connection is encrypted from the first byte (SSL/TLS) |
Setting secure: true on port 587 gives a "wrong version number" error, and so does the reverse. Our guide to email SSL and TLS explains why, and SMTP settings lists the values. For our relay, the host is smtp.migosmtp.com on port 587 with STARTTLS, and you create the credentials in the dashboard; see the SMTP relay page.
Check the connection before you send
Nodemailer can test the settings without sending a message:
transporter.verify()
.then(() => console.log("SMTP connection OK"))
.catch(err => console.error("SMTP problem:", err.code, err.message));
Run this at start-up or in a health check. If it fails, the error code tells you where to look.
Common Nodemailer errors
| Error code | Meaning | Fix |
|---|---|---|
| EAUTH | Login failed | Check the user name, password or key, and whether the provider wants an app password |
| ECONNECTION or ESOCKET | Could not connect, or the connection broke | Check host, port and the secure setting; check firewalls |
| ETIMEDOUT | The connection or response timed out | The port may be blocked; try the other submission port your provider offers |
| EENVELOPE | Invalid sender or recipient | Check the from and to addresses |
| Self-signed or hostname mismatch | Certificate does not match the host name | Use the exact host name from your provider. Do not turn off certificate checks |
Read SMTP connection and TLS troubleshooting and SMTP error codes for what the server's reply codes mean.
Attachments, Cc and Bcc
await transporter.sendMail({
from: '"Billing" <[email protected]>',
to: "[email protected]",
cc: "[email protected]",
bcc: "[email protected]",
subject: "Your invoice",
text: "Your invoice is attached.",
attachments: [{ filename: "invoice.pdf", path: "./invoice.pdf" }]
});
Nodemailer adds each address to the SMTP envelope and writes only the To and Cc headers, so Bcc recipients stay hidden. Read what CC means and what BCC means. Keep attachments small; see email size limits and attachments.
Sending many emails efficiently
Opening a new connection for every message is slow. For bursts, turn on connection pooling and let Nodemailer reuse connections:
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, port: 587, secure: false, requireTLS: true,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
pool: true, maxConnections: 3, maxMessages: 100
});
Respect your provider's rate limits, handle temporary failures with retries and back-off, and send one message per recipient. Our article on email integration patterns and rate limits and back-off cover this. For larger volumes, a queue in your application keeps sending from blocking your web requests.
Sending email from JavaScript in the browser
Many people search for "email JavaScript" hoping to send mail directly from a web page. Browsers cannot open SMTP connections, and if they could, you would have to place your SMTP password in code that every visitor can read. The safe pattern is to send a request from the page to your own server, or to a service that holds the secret on its side, and have that server send the mail. Treat anything that puts a secret in browser code as a risk, and protect forms against abuse with rate limits and a challenge, since an open form can be used to send spam in your name.
Deliverability for Node.js mail
- Authenticate your domain. Use SPF, DKIM and DMARC. See the SPF guide, DKIM guide and DMARC guide.
- Send from an address on that domain, not a free mailbox address.
- Include a plain-text part as well as HTML, as in the example above.
- Use a sending service, not a personal mailbox, for anything customers rely on. See Gmail SMTP limits.
- Keep secrets in environment variables and rotate them. See API key security.
- Use a test or sandbox environment in development, so you never email real customers by accident. See testing email in staging.
Never disable certificate verification to make an error go away. Options such as tls: { rejectUnauthorized: false } remove the protection that tells you that you are talking to the real server. Fix the host name or the certificate chain instead.
Structuring Nodemailer in a real application
Create the transporter once, when your application starts, and reuse it. Creating a new transporter for every request wastes connections and slows responses. Put it in a small module that exports a function such as sendEmail(to, subject, html, text), so the rest of your code never touches SMTP settings. This also gives you one place to add logging, retries, and a switch that turns real sending off in development and testing.
Keep the content of your messages separate from the sending code. Use a template engine or plain template strings to build the HTML, escape any value that comes from a user, and always generate a plain-text version as well. A message that has only an HTML part, or an HTML part with no text alternative, is more likely to be treated as spam, and is unreadable in clients that block HTML.
Handling failures and retries
Treat sending as work that can fail. A temporary failure, such as a timeout or a 4xx reply from the server, should be retried later with growing delays. A permanent failure, such as a 5xx reply that rejects an address, should not be retried; record it and stop sending to that address. Log the message id that sendMail returns, so you can find the message in your provider's logs when a customer says that it never arrived.
For anything important, such as password resets and receipts, move the call out of the web request. Add a job to a queue, return the response to the user at once, and let a worker send the message. If the SMTP server is slow or down, your site stays fast, and the queue keeps the messages until sending works again.
Using environment variables safely
Read the host, user name and password from process.env, and load them from a .env file in development with a package such as dotenv. Add the file to your .gitignore, so that it never reaches your repository. In production, set the variables in your hosting platform or secrets manager. Use a different set of credentials for development, staging and production, so that a test cannot send real mail from a production account. Fail loudly at start-up when a required variable is missing, so the problem shows up on deployment and not on the first customer email.
Sending HTML email that renders well
Email clients do not support modern web layouts. Use simple table-based layouts and inline styles for anything that must look right in every client, keep images small and always give them alt text, and make sure that the message is still understandable when images are blocked. Send a test to several clients before you ship a template. Our email templates feature can store and render templates for you, so the Node.js code only needs to supply the data.
Sending from Node.js in production? Create SMTP credentials, authenticate your domain and send your first message from your Node.js app in a few minutes. We are here if you hit a snag.
References and further reading
Frequently asked questions
How do I send an email with Nodemailer?
Install nodemailer with npm, create a transporter with your SMTP host, port, security mode and credentials, then call transporter.sendMail with from, to, subject and a text or HTML body.
What should the secure option be for port 587 and 465?
Use secure false with port 587, which upgrades with STARTTLS, and add requireTLS true. Use secure true with port 465, which is encrypted from the start. Mixing them causes a wrong version number error.
What does the Nodemailer EAUTH error mean?
The login failed. Check the user name and password or key, and whether your provider requires an app password or OAuth instead of a normal password.
Can I send email directly from browser JavaScript?
Not safely. Browsers cannot open SMTP connections, and putting credentials in browser code exposes them. Send a request to your own server or a service that holds the secret, and send the mail from there.
How do I send many emails with Nodemailer?
Use connection pooling, respect your provider's rate limits, retry temporary failures with back-off, send one message per recipient and use a queue so that sending does not block web requests.