Send email with Node.js using Volanea’s SMTP relay when your application needs to deliver password resets, receipts, verification links, alerts, invitations, and other transactional messages. This guide uses Nodemailer because it is a well-established Node.js SMTP client and keeps the integration portable across Express, Next.js route handlers, workers, queue consumers, and standalone scripts.
This is a server-side integration. Do not place your Volanea API key or SMTP credentials in browser JavaScript, mobile applications, public repositories, or client-exposed environment variables. Your application server should send the email after it has authenticated the user action and assembled the message data.
What you will build
By the end of this guide, you will have a Node.js script that:
- Installs a Node.js SMTP dependency.
- Loads Volanea credentials from environment variables.
- Creates an SMTP transport with TLS settings appropriate for the configured port.
- Sends one HTML and plain-text transactional email.
- Logs the provider response without exposing credentials.
- Fails clearly when a required environment variable is missing.
The example deliberately uses SMTP rather than a fictional provider-specific Node.js SDK. SMTP is a standard sending interface, and Nodemailer provides the Node.js client layer. Use the current Volanea SMTP hostname, port, username, and credential values shown in your Volanea account or in the email API reference and setup guides when filling in the environment variables below.
Prerequisites
Before writing code, make sure the sending side is ready. A successful SMTP connection is not the same thing as a successful production sending setup: the sender identity, domain authentication, credentials, and application workflow all matter.
You need:
- Node.js 18 or later. This guide uses modern JavaScript syntax and runs as an ECMAScript module.
- A Volanea account with SMTP access. Obtain the SMTP connection settings and a credential intended for server-side sending.
- A sending domain or sender address that Volanea allows you to use. Do not use an arbitrary
Fromaddress just because it has a valid-looking format. - A recipient address you control for testing. A personal inbox is useful for confirming rendering and inbox placement, but do not repeatedly test against real customers.
- A local project directory. You can use this script by itself, or move the
sendTransactionalEmailfunction into an application service later.
For a first test, use an ordinary transactional message such as an account-verification notice. Avoid marketing copy, unrequested bulk sends, purchased lists, or a large recipient list while validating the integration. Transactional sending should be triggered by a user action or system event, not by a loop that blindly emails every record in a database.
Install the Node.js dependency
Create a new directory and initialize an npm project if you do not already have one:
mkdir volanea-node-email
cd volanea-node-email
npm init -y
Install Nodemailer and dotenv:
npm install nodemailer dotenv
nodemailer creates and manages the SMTP connection. dotenv loads values from a local .env file during development. In most production hosts, environment variables are configured by the platform or deployment system instead; keeping dotenv in the project is still convenient for local runs.
Set your package to use ECMAScript modules by adding "type": "module" to package.json:
{
"name": "volanea-node-email",
"version": "1.0.0",
"type": "module",
"scripts": {
"send:test": "node send-email.js"
}
}
If your project already uses CommonJS, you can use require() syntax instead. The mail transport, environment-variable approach, and await transporter.sendMail() call are otherwise the same. Do not mix CommonJS require() with import syntax unless your Node.js project has been configured to support that arrangement.
Configure environment variables
Create a file named .env in the project root. Populate the SMTP host, port, username, and credential with the exact values provided for your Volanea SMTP configuration. Do not guess a hostname or reuse credentials from another email provider.
# Copy the SMTP server hostname shown in Volanea.
VOLANEA_SMTP_HOST=your-volanea-smtp-host
# Use the port shown in your Volanea SMTP settings, commonly 587 or 465.
VOLANEA_SMTP_PORT=587
# Copy the SMTP username shown in Volanea.
VOLANEA_SMTP_USER=your-volanea-smtp-username
# Store the Volanea SMTP credential or API key here, as applicable to your SMTP setup.
VOLANEA_API_KEY=replace-with-your-secret
# Use an approved sender identity.
EMAIL_FROM="Acme App <notifications@example.com>"
# Use an inbox you control while testing.
EMAIL_TO=you@example.com
Add .env to .gitignore before you make your first commit:
node_modules/
.env
The VOLANEA_API_KEY name is intentional: it makes the secret’s purpose visible in deployment configuration. SMTP authentication may require a username plus a password-style credential, depending on the connection details assigned to your account. The example sends the value of VOLANEA_API_KEY as the SMTP password field; use this only when the Volanea SMTP configuration identifies that secret as the credential for SMTP authentication. If your account provides a distinct SMTP password, store that value in this environment variable instead.
Never hard-code the secret in send-email.js. Hard-coded keys tend to reach source control, logs, screenshots, support tickets, and build artifacts. Environment variables are not automatically safe, but they are substantially easier to rotate and keep outside your application source.
Complete working Node.js example
Create a file named send-email.js in the project root and paste in the following code.
import 'dotenv/config';
import nodemailer from 'nodemailer';
const requiredEnvironmentVariables = [
'VOLANEA_SMTP_HOST',
'VOLANEA_SMTP_PORT',
'VOLANEA_SMTP_USER',
'VOLANEA_API_KEY',
'EMAIL_FROM',
'EMAIL_TO',
];
for (const name of requiredEnvironmentVariables) {
if (!process.env[name]) {
throw new Error(`Missing required environment variable: ${name}`);
}
}
const port = Number(process.env.VOLANEA_SMTP_PORT);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error('VOLANEA_SMTP_PORT must be a valid TCP port number.');
}
const transporter = nodemailer.createTransport({
host: process.env.VOLANEA_SMTP_HOST,
port,
// Port 465 normally uses TLS immediately. Port 587 normally upgrades
// the connection with STARTTLS after connecting.
secure: port === 465,
auth: {
user: process.env.VOLANEA_SMTP_USER,
pass: process.env.VOLANEA_API_KEY,
},
});
async function sendTransactionalEmail() {
// Verifies that the host is reachable and that SMTP authentication works
// before attempting to submit a message.
await transporter.verify();
const info = await transporter.sendMail({
from: process.env.EMAIL_FROM,
to: process.env.EMAIL_TO,
subject: 'Your Volanea Node.js test email',
text: [
'Hello,',
'',
'This transactional email was sent from Node.js through Volanea SMTP.',
'If you received it, your basic SMTP integration is working.',
].join('\n'),
html: `
<!doctype html>
<html lang="en">
<body style="margin:0;padding:24px;background:#f6f7f9;font-family:Arial,sans-serif;color:#1f2937;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width:600px;background:#ffffff;border-radius:8px;">
<tr>
<td style="padding:32px;">
<h1 style="margin:0 0 16px;font-size:24px;line-height:32px;">Node.js email test</h1>
<p style="margin:0 0 16px;font-size:16px;line-height:24px;">Hello,</p>
<p style="margin:0;font-size:16px;line-height:24px;">This transactional email was sent from Node.js through Volanea SMTP. If you received it, your basic SMTP integration is working.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`,
});
console.log('Email accepted by SMTP server.');
console.log({
messageId: info.messageId,
accepted: info.accepted,
rejected: info.rejected,
response: info.response,
});
}
sendTransactionalEmail().catch((error) => {
console.error('Unable to send email.');
console.error(error);
process.exitCode = 1;
});
Run the script:
npm run send:test
If the connection and submission succeed, the terminal prints the message identifier, accepted recipients, rejected recipients, and SMTP response. Treat that output as evidence that the SMTP server accepted the message for processing. It does not, by itself, prove that the recipient’s mailbox provider delivered the message to the inbox. Delivery, bounces, complaints, and later engagement are separate stages in an email’s lifecycle.
How the example works
dotenv/config loads local development settings
The first import loads .env before the script reads process.env. This is useful locally because you can run one command without exporting variables in every terminal session.
In production, prefer your deployment platform’s secret manager, encrypted environment configuration, or workload identity mechanism. Do not upload .env alongside static assets or embed it in a container image that may be distributed outside your controlled environment.
Required-variable checks fail early
The requiredEnvironmentVariables array prevents a confusing SMTP error caused by an empty hostname, undefined password, or missing sender. Failing before a network call gives developers an immediate configuration error and prevents accidental attempts to send with incomplete data.
This is especially useful in deployment pipelines. A staging environment may have credentials while a preview environment does not, and without an explicit check, the missing secret can appear later as a vague connection or authentication failure.
SMTP port determines the initial TLS behavior
The example uses secure: port === 465. Port 465 is generally configured for an immediate TLS connection, while port 587 is generally configured for SMTP submission with STARTTLS. Nodemailer handles the relevant SMTP negotiation after the connection is established.
Do not set secure: true merely because you want encryption. With a 587 configuration, that setting can make Nodemailer try to establish TLS at the wrong point in the connection. Use the port supplied in your Volanea SMTP settings, and let the code align the secure option with that port.
transporter.verify() separates setup failures from send failures
transporter.verify() checks that Nodemailer can establish a connection and authenticate. It is useful during initial integration and in a controlled health-check workflow because a credentials or network issue appears before the script attempts to submit a message.
Do not run a fresh verification for every high-volume production message if it creates unnecessary connection work. In a long-running application, create and reuse the transport appropriately for your process model. In serverless environments, test behavior under your actual runtime because connections may not survive between invocations.
Send both plain text and HTML
The message includes both text and html. Plain text improves compatibility with mail clients that do not render HTML and provides a readable fallback when HTML is disabled. It can also make it easier to inspect the essential message content during testing.
Keep the two versions semantically consistent. A recipient should not get different instructions, different prices, or a different call to action depending on which version their mail client displays.
Use the integration in an application
The standalone script proves the basic connection. In a production application, move the transport creation and send function into a server-only module. Then call it from a trusted backend path after the business event has been committed or queued.
For example, a registration flow should generally create the user record and verification token first. Then it should either enqueue the email or send it through a controlled service function. The mail body should contain the generated token or verification URL, not a value supplied directly by an untrusted browser request.
A minimal reusable function can look like this:
export async function sendVerificationEmail({ to, verificationUrl }) {
if (!to || !verificationUrl) {
throw new Error('A recipient and verification URL are required.');
}
return transporter.sendMail({
from: process.env.EMAIL_FROM,
to,
subject: 'Verify your email address',
text: `Verify your email address: ${verificationUrl}`,
html: `<p>Verify your email address by opening <a href="${verificationUrl}">this link</a>.</p>`,
});
}
Keep this module on the server. A frontend should call your own authenticated backend endpoint, while the backend decides whether an email should be sent. If a browser can call SMTP with your credentials, anyone who can inspect the application can reuse those credentials to send mail as your domain.
Sender identity and deliverability checks
Before sending to customers, confirm that the sender identity is set up correctly. The exact DNS records and verification steps depend on your Volanea configuration, so use the values generated for your sending domain rather than copying a record from another provider or tutorial.
At a minimum, review these operational questions:
- Is the domain in
EMAIL_FROMapproved for sending? - Does the visible
Fromaddress belong to that sending domain? - Have the required domain-authentication DNS records been published exactly as generated?
- Does the message include a valid plain-text alternative?
- Is the recipient expecting this message because of a product action?
- Can the user safely understand who sent the message and why?
- Does the message avoid unnecessary tracking parameters, misleading subject lines, and excessive image-only content?
Email deliverability is not solved by one successful test. Authentication, recipient engagement, complaint rates, bounce handling, content quality, list quality, and sending consistency affect whether future messages are accepted and placed well. Treat transactional email as application infrastructure with monitoring and ownership, not as an unobserved side effect.
For signup forms or user-import workflows, validate addresses before you add them to recurring sending paths. The free email address verification tool can help catch obvious invalid or mistyped addresses before they create avoidable bounces.
Error handling and retry strategy
A send operation can fail before Volanea receives the message, while Volanea is processing it, or after the recipient’s mail provider receives it. Your application should distinguish these cases rather than treating every exception as a reason to immediately retry.
A useful approach is:
- Generate a stable internal event ID for the business event, such as
password-reset:user_123:request_456. - Persist the event or place it in a durable queue before sending where the workflow requires reliability.
- Attempt the SMTP submission.
- Record the SMTP result, message identifier, and internal event ID.
- Retry only failures that are plausibly temporary, with exponential backoff and a bounded attempt count.
- Do not retry known bad recipients, invalid sender configurations, or authentication failures until the underlying configuration is fixed.
SMTP may report temporary and permanent errors differently, but do not rely only on a numeric pattern without examining the error context returned by your mail library and SMTP provider. Authentication errors will not be fixed by retries. A transient network timeout may be retriable, but it also introduces a duplicate-send risk if the server accepted the message just before the client lost the connection.
For workflows where duplicate emails are costly, such as receipts or password-reset messages, store a send state and use your application’s event identifier to make retry decisions. A queue worker should not resend indefinitely just because it has not received a clean response.
Common errors
Authentication fails with an SMTP 535 error
An SMTP 535 response commonly indicates that authentication was rejected. Check the exact SMTP username and secret configured in VOLANEA_SMTP_USER and VOLANEA_API_KEY, then confirm that the credential is enabled for the SMTP configuration you are using.
Do not add quotes around a secret in a deployment dashboard unless the dashboard requires them. In a local .env file, quoting can be appropriate for values with spaces, but copying smart quotes, trailing spaces, line breaks, or the wrong credential type is a common cause of failed authentication. Rotate the credential if it may have been exposed.
Connection timeout, ECONNREFUSED, or ENOTFOUND
These errors indicate that Node.js could not reach the configured SMTP endpoint. Verify the hostname character by character, verify the port, and check whether your hosting provider permits outbound SMTP traffic on that port.
Some cloud and corporate networks restrict outbound SMTP to reduce abuse. If your application works locally but not in production, compare outbound firewall rules, network egress policies, container configuration, and the environment variables deployed to that service. Do not switch randomly between ports or hosts; use the exact current Volanea SMTP connection details.
TLS errors or wrong version number
A TLS mismatch usually means the connection mode does not match the SMTP port. In this guide, secure is true only for port 465 and false for other ports such as 587. If the SMTP settings specify a different supported port or a special TLS requirement, follow those settings rather than forcing the default pattern.
Avoid disabling certificate validation with settings such as rejectUnauthorized: false. That may hide a misconfiguration during development but weakens the connection and can expose credentials. Fix the hostname, certificate, proxy, or network trust issue instead.
await is missing and errors disappear
transporter.sendMail() returns a promise. If you call it without await or without returning the promise, a serverless handler or Node.js process can finish before the operation completes, and errors may not reach the code that should handle them.
Correct:
const info = await transporter.sendMail(message);
Incorrect:
transporter.sendMail(message);
return { ok: true };
In a route handler, make the handler async and await the send operation or enqueue a durable job before returning. In command-line scripts, attach a .catch() handler as shown in the complete example so a failed promise sets a non-zero process exit code.
The HTML email arrives blank or malformed
Email clients support a constrained subset of HTML and CSS. Use table-based layout for important structure, inline critical styles, include a plain-text alternative, and test in the mail clients your recipients use.
Do not assume CSS from your web application will render in an inbox. External stylesheets, advanced layout properties, scripts, and browser-dependent features may be removed or ignored. Keep transactional emails visually simple and place the essential information near the top.
The message is rejected because of the sender address
A sender rejection often means the value in EMAIL_FROM is not an approved identity, its domain is not configured, or the address is malformed. Use an address on the sending domain that your Volanea configuration authorizes.
The display-name format must remain valid. For example, Acme App <notifications@example.com> is a conventional mailbox format, while a string containing multiple addresses, unsupported line breaks, or untrusted user input should never be inserted into from.
A recipient is rejected but the script still reports an SMTP response
SMTP can accept a submission while rejecting one or more recipients, especially with multiple recipients. Inspect info.accepted and info.rejected rather than logging only info.response.
For critical one-to-one transactional sends, use a single intended recipient per message whenever possible. It simplifies privacy, error handling, user-specific content, audit records, and retry decisions.
Your application sends duplicate emails after a timeout
A timeout does not always mean the SMTP server failed to accept the message. The connection may have dropped after submission but before your app received the response. Retrying blindly can send a duplicate.
Use a durable job record and an internal idempotency strategy around the business event. Record attempts, delay retries, and make the user-facing action safe if the same email arrives twice. For a password reset, for example, a token should be single-purpose, expire quickly, and tolerate a user opening either of two identical messages.
Production practices for transactional email
Once the test script works, the next concern is operational reliability. A production email flow should be observable, bounded, and connected to the rest of the application’s state.
Send from a queue for important workflows
A request-response path is acceptable for a simple low-volume notification, but a queue is often safer for receipts, account security messages, subscription changes, and other important events. The web request can commit the business event, enqueue an email task, and return promptly. A worker then handles SMTP submission, retries, and logging.
This separation prevents a temporary SMTP or network issue from making an otherwise successful user action appear to fail. It also gives your team a specific place to inspect delayed jobs, retry attempts, dead-letter messages, and send outcomes.
Keep application data out of logs
Log a safe internal event ID, recipient identifier where appropriate, SMTP response category, and message ID. Avoid logging complete email bodies, raw authentication headers, reset URLs, one-time codes, or the API key.
Email content may contain personal data, invoices, account activity, and security links. Logging all of it often creates an unnecessary second copy of sensitive information in a system with broader access than the application database.
Escape user-controlled content
Do not interpolate a user’s name, comment, address, or product title directly into an HTML string without escaping it. HTML email is still HTML, and unescaped content can break the message structure or create an unsafe rendering outcome.
Use a template engine with automatic escaping, or write a small explicit escaping function for any dynamic text values. URLs require separate validation: escaping a string does not make an attacker-controlled destination safe for a password reset or account action.
Separate transactional and promotional intent
A password reset, security alert, receipt, or account notice is not the same as a newsletter. Keep message purpose clear, do not turn essential service mail into a disguised promotion, and ensure marketing consent is handled separately from product-required communication.
This distinction helps your application logic, recipient expectations, unsubscribe handling, and deliverability. It also makes incident investigation easier because every message type has a documented trigger and owner.
Next steps
After your first message is accepted, build the rest of the operational loop:
- Webhooks: Configure a server endpoint to receive delivery-related events where available, such as delivered, bounced, complained, or deferred outcomes. Verify incoming webhook signatures using the current Volanea documentation, store a provider event identifier to deduplicate retries, and return a successful response only after your handler has safely recorded or queued the event.
- Templates: Move reusable transactional layouts out of application string literals when your workflow benefits from centrally managed templates. Keep a clear contract for template variables, render a preview with representative data, and maintain a plain-text fallback. Templates should make visual changes safer without allowing arbitrary user input to become executable or unescaped HTML.
- Domain authentication: Complete and verify the DNS configuration for the sender domain using the exact records generated for your account. This is foundational for sender trust and deliverability.
- Monitoring: Add alerting for authentication failures, rising bounce rates, unexpected rejection rates, and queue backlogs. A dashboard is useful, but an alert that reaches an on-call owner is what turns a delivery problem into an actionable incident.
- Cost planning: As volume grows, review transactional email pricing alongside the number of messages, environments, and operational requirements your application actually has.
FAQ
Can I send email with Node.js directly from the browser?
No. Keep Volanea SMTP credentials and API keys on a server you control. A browser bundle exposes secrets to users and attackers, who could then send mail using your account.
Why does the example include both text and html?
The text version is a readable fallback for clients that cannot or do not render HTML. Providing both also helps ensure the essential transactional information remains available in more inbox environments.
Should I use port 465 or port 587?
Use the SMTP port shown in your current Volanea SMTP settings. In the example, port 465 enables immediate TLS with secure: true; port 587 uses secure: false so Nodemailer can negotiate SMTP submission security appropriately.
Does an SMTP success response mean the recipient received the email?
No. It means the SMTP server accepted the message for processing. Final delivery can still be affected by later recipient-server decisions, bounces, filtering, suppression rules, and mailbox conditions.
Can I reuse the same Nodemailer transport for multiple emails?
Usually, yes, in a long-running server process. Create the transport once in a server-only module and reuse it according to your application lifecycle. Test carefully in serverless runtimes, where process reuse and open connections behave differently.