Send email with Nodemailer SMTP when your Node.js application needs a portable, standards-based way to deliver transactional messages such as password resets, verification links, receipts, alerts, and invitations. This guide shows a complete Volanea SMTP setup using an API key stored in an environment variable—not hard-coded in application source.

Nodemailer is an SMTP client for Node.js. Your application creates a transport with the SMTP connection settings for your Volanea account, authenticates to the relay, and submits a message with sendMail(). Volanea then handles the onward delivery process according to your sender and account configuration.

What you need before sending

Before writing code, make sure you have the following:

  • A Node.js project.
  • A sending domain configured for your Volanea account.
  • A sender address on that domain, such as notifications@example.com.
  • The SMTP host, port, encryption mode, and username provided in your Volanea SMTP configuration.
  • A Volanea API key or SMTP password that your account’s SMTP configuration identifies as the SMTP authentication secret.
  • A recipient address you control for the first test.

Do not guess the SMTP hostname, port, username, or whether an API key is accepted as the SMTP password. Use the values shown in the SMTP configuration for your account. SMTP configuration is connection-specific: a correct API key paired with the wrong host, username, TLS mode, or port can still produce an authentication or connection failure.

Your from address matters separately from SMTP authentication. The SMTP credentials authorize your application to submit mail to the relay. The from address identifies the message sender and should use a domain you have configured for sending. For production transactional email, avoid using a consumer mailbox address as the sender identity.

Install Nodemailer

Nodemailer is the dependency used in this integration. From the root of your Node.js project, run:

npm install nodemailer

This guide uses Node’s built-in environment-file support, so it does not require a separate environment-variable package. Use a current supported Node.js release that supports the --env-file flag, or load the same environment variables through your host, container runtime, secret manager, or deployment platform.

Create a file named send-email.mjs. The .mjs extension lets Node run the example as an ES module without changing your package.json.

Add SMTP settings to an environment file

Create a .env file beside send-email.mjs:

# Copy the SMTP connection values provided for your Volanea account.
VOLANEA_SMTP_HOST=your-volanea-smtp-host
VOLANEA_SMTP_PORT=587
VOLANEA_SMTP_SECURE=false
VOLANEA_SMTP_USER=your-volanea-smtp-username

# Store the API key here only when your Volanea SMTP configuration
# specifies that the API key is the SMTP password.
VOLANEA_API_KEY=replace-with-your-api-key-or-smtp-password

# Use a sender address from a configured sending domain.
EMAIL_FROM="Acme Notifications <notifications@example.com>"
EMAIL_TO=you@example.net

Replace every placeholder with values from your account. The example uses port 587 with VOLANEA_SMTP_SECURE=false, which is the conventional Nodemailer configuration for a STARTTLS SMTP submission connection. If your Volanea SMTP configuration instead specifies implicit TLS on port 465, use:

VOLANEA_SMTP_PORT=465
VOLANEA_SMTP_SECURE=true

The secure setting does not mean “use encryption whenever possible.” In Nodemailer, it determines whether the connection begins inside TLS immediately. Therefore, secure: true belongs with an implicit-TLS endpoint such as port 465, while secure: false is normally used for a STARTTLS connection such as port 587. Setting secure: true on a STARTTLS port is a common cause of connection failures.

Never commit .env files containing live credentials. Add them to .gitignore:

.env

For deployed applications, configure the same variable names in your hosting provider’s secret manager or environment settings. The source code can remain unchanged across local, staging, and production environments while each environment supplies its own credentials and sender addresses.

Complete Nodemailer SMTP example

The following file validates its required configuration, verifies that it can authenticate to the SMTP relay, and sends one HTML and plain-text transactional email. Copy it into send-email.mjs.

import nodemailer from 'nodemailer';

function requireEnv(name) {
  const value = process.env[name];

  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }

  return value;
}

const smtpHost = requireEnv('VOLANEA_SMTP_HOST');
const smtpPort = Number(requireEnv('VOLANEA_SMTP_PORT'));
const smtpSecure = requireEnv('VOLANEA_SMTP_SECURE') === 'true';
const smtpUser = requireEnv('VOLANEA_SMTP_USER');
const apiKey = requireEnv('VOLANEA_API_KEY');
const from = requireEnv('EMAIL_FROM');
const to = requireEnv('EMAIL_TO');

if (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535) {
  throw new Error('VOLANEA_SMTP_PORT must be a valid TCP port number.');
}

const transporter = nodemailer.createTransport({
  host: smtpHost,
  port: smtpPort,
  secure: smtpSecure,
  auth: {
    user: smtpUser,
    pass: apiKey,
  },
});

async function main() {
  // Checks DNS resolution, TCP connection, TLS negotiation, and SMTP auth.
  await transporter.verify();

  const info = await transporter.sendMail({
    from,
    to,
    subject: 'Welcome to Acme',
    text: 'Your account is ready. You can now sign in to Acme.',
    html: `
      <h1>Your account is ready</h1>
      <p>You can now sign in to Acme.</p>
    `,
  });

  console.log('Message accepted by SMTP relay');
  console.log('Message ID:', info.messageId);
  console.log('Accepted recipients:', info.accepted);
  console.log('Rejected recipients:', info.rejected);
}

main().catch((error) => {
  console.error('Email send failed');
  console.error(error);
  process.exitCode = 1;
});

Run the example with:

node --env-file=.env send-email.mjs

A successful sendMail() call means the SMTP relay accepted the message for processing. It does not by itself prove that the recipient mailbox received the email, placed it in the inbox, or displayed it as intended. Delivery can still be affected by recipient-server policy, sender-domain alignment, mailbox filtering, a suppression, a malformed address, or later delivery events.

The info.messageId value is useful in application logs. Log it with your own event or order identifier, but do not log the API key, the entire SMTP configuration object, or full message bodies containing sensitive data.

How the SMTP transport works

Nodemailer’s createTransport() function creates a reusable SMTP transport. It does not send an email until you call sendMail(). In a long-running web server or worker process, create the transporter once during application initialization and reuse it for multiple messages rather than creating a fresh SMTP connection for every request.

The key transport settings are:

  • host: The Volanea SMTP server hostname supplied for your account.
  • port: The TCP submission port supplied for the selected security mode.
  • secure: true for immediate TLS, commonly paired with port 465; false for a STARTTLS-capable submission port such as 587.
  • auth.user: The exact SMTP username provided by Volanea.
  • auth.pass: The SMTP authentication secret. In this guide, it is read from VOLANEA_API_KEY because the requested integration uses an API key environment variable. Use it only when your SMTP configuration identifies the API key as the SMTP password; otherwise place the distinct SMTP password in that variable.

Why verify before the first send

await transporter.verify() is optional once your production integration is established, but it is very useful while setting up a new environment. It separates connection and authentication problems from message-specific problems.

For example, if verify() fails before sendMail() runs, investigate the host, port, TLS mode, firewall rules, username, and secret. If verify() succeeds but a send fails, inspect the message fields, sender address, recipient format, attachment handling, or server response instead.

Do not call verify() on every request in a high-volume application. That adds an extra SMTP operation before each send. A better approach is to verify during deployment checks, worker startup, a controlled health check, or a dedicated integration test, then let normal send operations use the established transport.

STARTTLS versus implicit TLS

SMTP submission uses TLS to protect the connection between your application and the relay. There are two common connection patterns:

  1. STARTTLS: The client connects normally, then upgrades the connection to TLS when the server advertises STARTTLS. With Nodemailer, this is generally secure: false.
  2. Implicit TLS: The client connects inside TLS from the first byte. With Nodemailer, this is secure: true.

The selected port and secure setting must agree. Do not disable certificate verification as a shortcut for TLS errors. A setting such as tls: { rejectUnauthorized: false } can hide a network interception, incorrect endpoint, local certificate problem, or misconfigured proxy. Fix the trust problem or use the correct Volanea SMTP endpoint instead.

Use the code in an application safely

The standalone script proves the connection. In an application, put the transport creation in a module that is imported by your server-side code, queue consumer, or job runner. Keep email delivery outside browser code.

SMTP credentials are secrets. Never expose VOLANEA_API_KEY, VOLANEA_SMTP_USER, the SMTP hostname, or a server-side sendMail() implementation in frontend JavaScript shipped to users. A browser cannot safely hold a credential that can submit email on behalf of your application.

A typical request flow looks like this:

  1. A user performs an action, such as creating an account or requesting a password reset.
  2. Your server validates the action and records the necessary application state.
  3. Your server or background job constructs a narrowly scoped email message.
  4. The server calls transporter.sendMail().
  5. Your application logs the result without logging secrets or sensitive message contents.
  6. Delivery and engagement handling occurs asynchronously through your email operations process.

For password resets, do not send an email before the reset token is stored durably. For receipts, do not rely on email acceptance as proof that a payment is successful; treat the payment record as the source of truth and make the email a follow-up notification. For high-value messages, use a queue so temporary SMTP or network failures can be retried without making a web request fail unnecessarily.

Reuse a transporter in a server process

Here is a simple module structure for a long-running Node.js service:

// mailer.mjs
import nodemailer from 'nodemailer';

export const transporter = nodemailer.createTransport({
  host: process.env.VOLANEA_SMTP_HOST,
  port: Number(process.env.VOLANEA_SMTP_PORT),
  secure: process.env.VOLANEA_SMTP_SECURE === 'true',
  auth: {
    user: process.env.VOLANEA_SMTP_USER,
    pass: process.env.VOLANEA_API_KEY,
  },
});

export async function sendWelcomeEmail({ to, name }) {
  return transporter.sendMail({
    from: process.env.EMAIL_FROM,
    to,
    subject: 'Welcome to Acme',
    text: `Hi ${name}, your account is ready.`,
    html: `<p>Hi ${name}, your account is ready.</p>`,
  });
}

Validate environment variables at process startup rather than allowing an empty configuration to reach a production send. The full example earlier includes a requireEnv() helper for this purpose. For a larger codebase, centralize configuration validation so every email-producing service uses the same validated sender, transport, and security policy.

Treat recipient data as untrusted input

Do not concatenate raw user input into HTML without escaping it. The name interpolation in the compact example above is suitable only when that value is trusted or has been escaped before insertion. If a name, comment, order note, or other field originates from a user, encode it before including it in the HTML version of an email.

Likewise, do not let a public HTTP request directly control every mail option. Restrict fields such as from, replyTo, headers, attachments, and recipient lists according to your application’s authorization rules. A generic “send email” endpoint can quickly become an abuse path if an attacker can submit arbitrary recipients or high-volume requests.

Compose a transactional email correctly

A transactional message should have a specific, user-expected purpose. A password-reset email should contain the reset instruction and an expiration context. A receipt should identify the purchase and provide support information. A verification email should make the next action obvious.

Provide both text and html whenever practical. The plain-text part improves accessibility, supports clients that do not render HTML, and offers a reasonable fallback when HTML processing is unavailable. Nodemailer generates the MIME structure needed to carry both representations.

The core message fields in the example are:

const message = {
  from: 'Acme Notifications <notifications@example.com>',
  to: 'person@example.net',
  subject: 'Welcome to Acme',
  text: 'Your account is ready.',
  html: '<p>Your account is ready.</p>',
};

You can use a single recipient string for one recipient. When your application has a legitimate transactional reason to notify multiple people, Nodemailer also supports arrays and comma-separated address lists. Keep recipient handling deliberate: sending an account-specific message to multiple unrelated recipients can disclose private information.

Sender identity and reply handling

The visible from address should be stable, recognizable, and aligned with the message’s purpose. For example:

  • notifications@example.com for product alerts and account activity.
  • receipts@example.com for billing confirmations.
  • support@example.com for messages that expect replies.

Use a sender address on your configured domain. If replies should go somewhere different, Nodemailer supports a replyTo field:

const info = await transporter.sendMail({
  from: 'Acme Billing <receipts@example.com>',
  replyTo: 'support@example.com',
  to: 'customer@example.net',
  subject: 'Your Acme receipt',
  text: 'Thanks for your purchase.',
  html: '<p>Thanks for your purchase.</p>',
});

Only set replyTo when it serves a real user expectation. A reply address that does not receive or triage replies can create a poor support experience.

Attachments and sensitive content

Nodemailer supports attachments, but attachments increase message size and can affect deliverability or recipient filtering. Prefer secure, authenticated download links for sensitive invoices, exports, or documents when that fits your product. If you do send an attachment, validate its source and size, use a safe generated filename, and avoid passing arbitrary filesystem paths derived from a user request.

Do not put passwords, full payment-card data, government identifiers, or permanent account credentials into email. Email is not a secure database or a private messaging channel. Use short-lived links and require the user to authenticate before showing sensitive account data.

Common errors

This section covers frequent failures when sending with Nodemailer over SMTP.

Authentication failures: 535, EAUTH, or invalid login

A Nodemailer error with a code such as EAUTH, or an SMTP response beginning with 535, usually means the relay rejected the provided credentials.

Check the following in order:

  1. Confirm VOLANEA_SMTP_USER exactly matches the SMTP username provided in your account configuration.
  2. Confirm VOLANEA_API_KEY contains the current secret with no accidental spaces, quotes, or line breaks.
  3. Confirm that your Volanea SMTP configuration supports using that API key as the SMTP password. If it lists a separate SMTP password, use that password as the value of VOLANEA_API_KEY instead.
  4. Check whether a key was rotated, revoked, scoped differently, or copied incorrectly.
  5. Confirm your deployed environment has the intended variables, not merely your local .env file.

Do not print the secret while debugging. You can safely log whether a variable is present, its length, or a one-way identifier managed by your secret system, but never log its full value.

TLS or connection errors: ETIMEDOUT, ECONNREFUSED, or handshake failures

Timeout and connection-refused errors usually occur before SMTP authentication. Verify the SMTP host, selected port, security mode, and outbound firewall policy for the machine running your code.

A common mismatch is setting secure: true with a STARTTLS port, or setting secure: false with an endpoint that requires immediate TLS. Use the exact host, port, and TLS mode supplied in your account configuration. If a cloud provider blocks outbound SMTP ports, use an approved submission port offered by your SMTP configuration or adjust the platform’s egress policy.

For certificate errors, do not set rejectUnauthorized: false to force the connection through. Confirm that the hostname is correct, the server has access to normal DNS resolution, no TLS-inspecting proxy is replacing certificates, and the operating system’s CA certificates are current.

Async and await mistakes

sendMail() returns a promise. If you forget to await it, your process may exit before the email is submitted, or your web handler may return before a failure is observed.

Incorrect:

transporter.sendMail(message);
console.log('Email sent');

Correct:

const info = await transporter.sendMail(message);
console.log('SMTP relay accepted:', info.messageId);

Wrap the awaited call in try/catch, or call an async function and attach a .catch() handler as shown in the complete example. In frameworks with serverless execution, always return or await the promise from the request handler so the runtime keeps the function alive until the SMTP operation finishes.

Missing required environment variable

This error comes from the guard function in the sample. It means Node did not receive the configuration value.

For local execution, ensure the file is named .env, it is in the directory from which you run the command, and you use:

node --env-file=.env send-email.mjs

In a deployment, configure the variables through the platform’s environment or secrets interface. Do not assume a local .env file is automatically uploaded to production. Restart or redeploy the application after changing environment settings when your platform requires it.

Wrong content type or malformed HTML

With Nodemailer SMTP, you normally provide text and html as strings and Nodemailer creates the appropriate MIME parts. Do not manually set a top-level Content-Type header just to force HTML. A conflicting custom header can produce malformed MIME output or cause mail clients to display raw markup.

Use:

html: '<h1>Welcome</h1><p>Your account is ready.</p>',
text: 'Welcome\n\nYour account is ready.',

Avoid using html to pass an object, a React component, a response object, or an unrendered template file. Render the final HTML string first, then pass that string to Nodemailer.

The message is accepted but does not arrive

SMTP acceptance is not inbox placement. First, verify that the recipient address is correct and check spam, promotions, quarantine, and mailbox rules. Then confirm that the from address uses your configured sending domain and that the message content is appropriate for the transactional action.

Also consider whether the recipient is suppressed because of a prior bounce, complaint, unsubscribe context, or manual block. A reliable email system should distinguish “submitted to SMTP,” “accepted by the relay,” and later delivery outcomes. Do not repeatedly retry the same address without understanding why it failed; repeated sends to invalid or rejecting recipients can harm operational reliability.

from address rejected or sender not permitted

If the relay rejects the sender, compare the exact from domain in your code with the domain configured for sending. A typo, an unconfigured subdomain, or an attempt to send as a domain your account is not authorized to use can trigger a rejection.

Keep sender addresses in server-side configuration where possible. This prevents each application feature from inventing a new address and makes it easier to review all identities used by your product.

Production reliability practices

The first successful test is the start of the integration, not the end. Transactional email is part of a product workflow, so design for partial failures, retries, observability, and user impact.

Use a queue for important messages

For noncritical development notifications, sending directly inside a request handler can be acceptable. For production receipts, reset links, login alerts, or onboarding messages, a queue is generally more resilient.

A durable queue lets your application commit its core business transaction first, then send the email through a worker. If SMTP is temporarily unavailable, the worker can retry according to a controlled policy. This avoids tying user-facing request latency to an external SMTP connection and avoids losing a notification because a process stopped at the wrong time.

Use bounded retries with backoff. Treat permanent errors, such as a malformed recipient address or sender rejection, differently from temporary network failures. Retrying an invalid address many times does not make it valid.

Prevent duplicate transactional messages

Retries introduce the risk of duplicate messages. Your application should track its own logical send intent—for example, password-reset:user-123:token-456 or receipt:order-987—and record whether it has already been submitted.

For a password reset, it is often acceptable to invalidate a previous token and send a newly generated one. For receipts, you may want exactly one send record per order event, with a controlled operator workflow for resends. Design this behavior at the application level rather than relying solely on a successful SMTP response.

Log the right information

Good logs make delivery incidents diagnosable without leaking data. Record:

  • Your internal event or job ID.
  • A recipient identifier that follows your privacy policy.
  • The sender identity used.
  • The SMTP result, including messageId, accepted recipients, and rejected recipients.
  • Error code and response details when a send fails.
  • Retry count and next scheduled attempt for queued messages.

Do not log API keys, SMTP passwords, raw password-reset URLs, full email bodies, or attachment contents. If you need to diagnose an HTML rendering problem, use a controlled test environment and redact account-specific values.

Next steps

Once a basic SMTP send works, move from a one-off test to an observable and maintainable email flow.

First, use delivery-event webhooks so your application can react to asynchronous outcomes such as deliveries, bounces, complaints, and engagement events where configured. A webhook receiver should verify incoming requests, process events idempotently, return quickly, and hand substantial work to a queue. The goal is to update your own records without treating a single webhook request as the only source of truth.

Second, use reusable templates for messages that need consistent branding, localization, or frequent copy updates. Keep transactional templates focused on the user action that caused the message. Test rendered output with realistic data, including long names, missing optional fields, and mobile-sized layouts.

For the API reference and setup material covering the broader sending workflow, see the Volanea email API documentation. If you need to check an address before allowing a user to depend on it, use the email address verification tool.

FAQ

Can I use a Volanea API key as the Nodemailer SMTP password?

Use the API key as auth.pass only when your Volanea SMTP configuration identifies that API key as the SMTP authentication secret. If your account provides a separate SMTP password, store that secret in VOLANEA_API_KEY instead and leave the code unchanged.

Should secure be true for port 587?

Usually no. In Nodemailer, port 587 is commonly configured with secure: false so the connection can use STARTTLS. Use secure: true only with an implicit-TLS SMTP endpoint, commonly port 465, when that matches your account’s SMTP configuration.

Does sendMail() mean the recipient received the email?

No. A successful result means the SMTP relay accepted the message for onward processing. Recipient-server acceptance, mailbox placement, bounces, and other delivery outcomes happen later.

Can I send email directly from a React or browser application?

No. Keep SMTP credentials and Nodemailer on a trusted server, server-side route, worker, or backend function. Sending from browser code would expose the credentials to users.

Why should I include both text and html?

The HTML part provides a richer presentation, while the plain-text part provides a compatible and accessible fallback. Supplying both also makes the message more useful across a wider range of email clients.