Updated 2026-10-08

How to Send Email from Node.js with Nodemailer and SMTP

To send email from Node.js with Nodemailer, call nodemailer.createTransport with host smtp.bulko.io, port 587, secure: false and auth.user and auth.pass from environment variables; Nodemailer upgrades the connection with STARTTLS. Set secure: true only when you use port 465.

Hostsmtp.bulko.io
Port587 · 465 · 2525
EncryptionSTARTTLS · SSL
Loginfrom dashboard

Nodemailer SMTP settings

FieldValue
hostsmtp.bulko.io
port587 (or 2525 if blocked; 465 with secure: true)
securefalse for 587 and 2525, true only for 465
requireTLStrue (optional, refuses to send if STARTTLS is not available)
auth.userprocess.env.SMTP_USERNAME (e.g. smtp_xxxxxxxxxxxxxxxx)
auth.passprocess.env.SMTP_PASSWORD
fromAn address on your domain, e.g. "Your App" <[email protected]>

Step-by-step setup

  1. Buy an SMTP package on the Bulko SMTP Relay page and copy the username and password from Dashboard -> SMTP Access.
  2. Install the library with npm install nodemailer. It works with both import nodemailer from "nodemailer" (ESM) and require("nodemailer") (CommonJS).
  3. Store the credentials as environment variables SMTP_USERNAME and SMTP_PASSWORD (in your process manager, container or a .env file loaded with node --env-file=.env).
  4. Create one transporter with host: "smtp.bulko.io", port: 587, secure: false and the auth object. Reuse it across requests instead of creating a new one per email.
  5. Understand secure: true means TLS from the first byte and is correct only for port 465. With false Nodemailer connects in plain text and upgrades with STARTTLS if the server offers it; add requireTLS: true to make that upgrade mandatory.
  6. Set from to an address on your own domain. For aligned SPF, DKIM and DMARC, verify the domain under Sending Domains in the Bulko dashboard.
  7. Call await transporter.verify() once at startup to catch wrong ports or passwords early, then send with await transporter.sendMail({...}).

If your hosting provider blocks outbound 587 and 465 (some cloud platforms do), use port: 2525 with the same secure: false settings. The SMTP ports guide explains the differences.

send-test.mjs

// send-test.mjs  (npm install nodemailer)
import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
  host: "smtp.bulko.io",
  port: 587,
  secure: false,      // false for 587 and 2525 (STARTTLS), true only for 465
  requireTLS: true,   // fail instead of sending without TLS
  auth: {
    user: process.env.SMTP_USERNAME,
    pass: process.env.SMTP_PASSWORD,
  },
});

// Port 465 (implicit TLS):
// const transporter = nodemailer.createTransport({
//   host: "smtp.bulko.io",
//   port: 465,
//   secure: true,
//   auth: { user: process.env.SMTP_USERNAME, pass: process.env.SMTP_PASSWORD },
// });

try {
  await transporter.verify(); // checks DNS, TLS and login without sending
  const info = await transporter.sendMail({
    from: '"Your App" <[email protected]>',
    to: "[email protected]",
    subject: "Nodemailer SMTP test",
    text: "It works.",
    html: "<p>It works.</p>",
  });
  console.log("Sent:", info.messageId, info.response);
} catch (err) {
  console.error("SMTP error:", err.responseCode, err.message);
  process.exitCode = 1;
}

Send a test email

Run SMTP_USERNAME=smtp_xxx SMTP_PASSWORD=yourpass node send-test.mjs with a real recipient. On success the script prints the messageId and a response starting with 250, which means the relay accepted the message. Check the inbox within a minute or two.

If verify() fails, the error has a code such as EAUTH, ETIMEDOUT or ESOCKET, and SMTP errors include responseCode. Match them with the table below.

Troubleshooting

ProblemFix
Error: Invalid login: 535 (code EAUTH)Wrong username or password, or the environment variables are undefined in the running process. Log Boolean(process.env.SMTP_PASSWORD) to check. See https://bulko.io/en/smtp-errors/535/
SSL routines: wrong version numbersecure: true was used with port 587 or 2525. Set secure: false for those ports, or switch to port 465 with secure: true.
Greeting never received or connection hangs on 465secure: false was used with port 465, so Nodemailer waits for a plain greeting that never comes. Set secure: true for 465.
ETIMEDOUT or ECONNREFUSEDOutbound SMTP ports are blocked by your host or firewall. Try port 2525 with secure: false.
554 5.7.1 in err.responseThe AI content filter rejected the message for phishing, malware, impersonation or scam patterns. Review content and links; see https://bulko.io/en/acceptable-use/

FAQ

When should secure be true in Nodemailer?

Only for port 465, where TLS starts immediately. For 587 and 2525 keep secure: false; the connection is still encrypted through STARTTLS, and requireTLS: true enforces it.

Does Nodemailer need a pool for bulk sending?

For many messages you can add pool: true to reuse connections. Stay within the default limit of 2,000 emails per day per SMTP account, or ask [email protected] to raise it.

Can I read incoming email with Nodemailer and this server?

No. Nodemailer only sends, and Bulko SMTP is outgoing only with no IMAP. Replies go to your From or Reply-To mailbox.

Does it work in Next.js, Express or NestJS?

Yes, in any server-side Node.js code. Do not run it in the browser or expose SMTP credentials to client bundles.

How much does it cost?

One-time packages: $5 for 5,000 emails, $15 for 20,000, $30 for 50,000, $60 for 150,000. Unused emails never expire, and there is no free SMTP trial.

Get SMTP credentials for Nodemailer

One-time payment by card or crypto, no subscription. Unused emails never expire. 2,000 emails per day per SMTP account.

SMTP Relay 5K: $5 · 5000SMTP Relay 20K: $15 · 20000SMTP Relay 50K: $30 · 50000SMTP Relay 150K: $60 · 150000
See SMTP relay plans

What you can send: Acceptable Use Policy.