How to Verify Email Delivery Success with Nodemailer in Sails.js: Check Validity & Handle Failures
Email communication is a critical component of modern web applications, powering user onboarding, password resets, transactional notifications, and more. However, ensuring emails actually reach recipients is often overlooked—until users complain they never received a critical message.
In Sails.js, a popular Node.js MVC framework, Nodemailer is the go-to library for sending emails. But sending an email is just the first step: verifying delivery success, catching invalid emails before they’re sent, and handling failures (like bounces or SMTP errors) are equally important.
This guide will walk you through building a robust email system in Sails.js using Nodemailer. You’ll learn how to:
- Validate email addresses before sending.
- Send emails and interpret "success" correctly (hint: it’s not just "sent").
- Track delivery status and handle bounces.
- Log and manage failures (temporary errors, permanent bounces, etc.).
By the end, you’ll have a system that ensures reliable email delivery and keeps you informed of issues before users do.
Table of Contents#
- Prerequisites
- Setting Up Nodemailer in Sails.js
- 2.1 Install Dependencies
- 2.2 Configure SMTP Transport
- Email Validity Check: Before Sending
- 3.1 Basic Format Validation
- 3.2 Advanced Validation (Optional)
- Sending Emails with Nodemailer: Immediate Success vs. Delivery
- 4.1 Creating an Email Service
- 4.2 Interpreting "Immediate Success"
- Verifying Actual Delivery Success
- 5.1 Understanding SMTP Acceptance vs. Delivery
- 5.2 Using Delivery Status Notifications (DSN)
- 5.3 Webhooks for Bounce/Complaint Handling
- Handling Email Failures
- 6.1 Validation Failures
- 6.2 SMTP Errors (Transient/Permanent)
- 6.3 Delivery Failures (Bounces)
- Logging & Monitoring
- Testing Your Setup
- Conclusion
- References
Prerequisites#
Before diving in, ensure you have:
- Basic familiarity with Sails.js (MVC structure, services, controllers).
- Node.js (v14+ recommended) and npm installed.
- A Sails.js project (new or existing).
- An SMTP provider (e.g., SendGrid, Mailgun, Gmail, or your own SMTP server).
- Optional: An email service account with webhook support (for bounce notifications).
Setting Up Nodemailer in Sails.js#
2.1 Install Dependencies#
First, install Nodemailer (the core email-sending library) and email-validator (for email format checks) via npm:
npm install nodemailer email-validator --saveIf using a third-party SMTP service (e.g., SendGrid), you may need additional transports, but Nodemailer’s built-in smtp transport works for most cases.
2.2 Configure SMTP Transport#
In Sails.js, store SMTP configuration in config/env/production.js (or development.js for local testing) to keep credentials secure. Avoid hardcoding secrets—use environment variables.
Example config/env/development.js:
module.exports = {
// ... other configs
email: {
smtp: {
host: process.env.SMTP_HOST || 'smtp.mailgun.org', // e.g., 'smtp.sendgrid.net'
port: process.env.SMTP_PORT || 587,
secure: process.env.SMTP_SECURE === 'true' || false, // true for port 465
auth: {
user: process.env.SMTP_USER || '[email protected]',
pass: process.env.SMTP_PASS || 'your-smtp-password'
}
},
from: process.env.EMAIL_FROM || 'Your App <[email protected]>'
}
};For local testing, use Ethereal.email (a fake SMTP service) to avoid sending real emails:
// In development.js (temporary)
email: {
smtp: {
host: 'smtp.ethereal.email',
port: 587,
secure: false,
auth: {
user: 'your-ethereal-user', // Generated on Ethereal
pass: 'your-ethereal-pass'
}
}
}Email Validity Check: Before Sending#
Sending emails to invalid addresses wastes resources and harms your sender reputation. Validate emails before hitting the SMTP server.
3.1 Basic Format Validation#
Use the email-validator package to check if an email has a valid format (e.g., [email protected]).
Create a helper function in api/helpers/validate-email.js:
const validator = require('email-validator');
module.exports = {
friendlyName: 'Validate email',
description: 'Check if an email address is valid.',
inputs: {
email: {
type: 'string',
required: true,
description: 'The email address to validate.'
}
},
exits: {
valid: {
description: 'Email is valid.'
},
invalid: {
description: 'Email is invalid.'
}
},
fn: async function ({ email }) {
const isValid = validator.validate(email);
if (!isValid) {
throw 'invalid';
}
return 'valid';
}
};3.2 Advanced Validation (Optional)#
For stricter checks (e.g., domain existence, disposable emails), use services like Mailboxlayer or Hunter.io.
Example with Mailboxlayer (add axios for API calls: npm install axios):
// In api/helpers/validate-email.js (extended)
const axios = require('axios');
fn: async function ({ email }) {
// First, check format
if (!validator.validate(email)) {
throw 'invalid';
}
// Then, check domain via Mailboxlayer API
const apiKey = process.env.MAILBOX_LAYER_API_KEY;
const response = await axios.get(`http://apilayer.net/api/check?access_key=${apiKey}&email=${email}&smtp=1&format=1`);
const { mx_found, disposable } = response.data;
// Reject if domain has no MX records or is disposable
if (!mx_found || disposable) {
throw 'invalid';
}
return 'valid';
}Sending Emails with Nodemailer: Immediate Success vs. Delivery#
Now, create an email service to send messages. Understand the critical difference between immediate SMTP acceptance and actual delivery.
4.1 Creating an Email Service#
Sails services (api/services/) are ideal for reusable logic like email sending. Create api/services/EmailService.js:
const nodemailer = require('nodemailer');
const { email } = sails.config; // Import SMTP config
// Create Nodemailer transporter
const transporter = nodemailer.createTransport(email.smtp);
module.exports = {
/**
* Send an email.
* @param {Object} options - Nodemailer mail options (to, subject, text/html, etc.)
* @returns {Promise<Object>} - Result of the send operation.
*/
send: async function (options) {
try {
// Validate recipient email first
await sails.helpers.validateEmail(options.to);
// Add default "from" address if not provided
const mailOptions = {
from: email.from,
...options
};
// Send email via Nodemailer
const info = await transporter.sendMail(mailOptions);
sails.log.info(`Email sent to ${options.to}. Message ID: ${info.messageId}`);
return { success: true, info };
} catch (error) {
sails.log.error(`Email failed to ${options.to}:`, error.message);
throw error; // Let the caller handle the error
}
}
};4.2 Interpreting "Immediate Success"#
When transporter.sendMail() resolves, Nodemailer returns an info object with messageId and response. A response like 250 2.0.0 OK means the SMTP server accepted the email—not that it was delivered to the recipient’s inbox.
Example info object:
{
messageId: '<[email protected]>',
response: '250 2.0.0 OK 1634567890 s1234567890abcdef',
envelope: { from: '[email protected]', to: ['[email protected]'] }
}Key Takeaway: Immediate success ≠ delivery. The email could still bounce later (e.g., invalid recipient, full inbox).
Verifying Actual Delivery Success#
To confirm delivery, you need to track what happens after the SMTP server accepts the email.
5.1 Understanding SMTP Acceptance vs. Delivery#
- Acceptance (250 OK): The SMTP server (yours or your provider’s) has received the email and will attempt to deliver it to the recipient’s server.
- Delivery: The recipient’s server accepts the email and delivers it to the inbox (or spam folder).
- Bounce: The recipient’s server rejects the email (permanent or temporary failure).
5.2 Using Delivery Status Notifications (DSN)#
SMTP supports DSN, which sends notifications when delivery succeeds/fails. Enable DSN in your Nodemailer transporter:
// In EmailService.js (transporter config)
const transporter = nodemailer.createTransport({
...email.smtp,
dsn: {
id: 'custom-message-id',
return: 'headers', // Return headers in DSN
notify: 'success,failure,delay', // Notify on success, failure, delay
recipient: '[email protected]' // Where to send DSNs
}
});However, DSNs are sent via email, requiring a separate inbox to parse—cumbersome for apps. Most developers use webhooks instead.
5.3 Webhooks for Bounce/Complaint Handling#
Email providers like SendGrid, Mailgun, and Postmark send webhooks (HTTP requests) when emails bounce or are marked as spam.
Step 1: Configure Webhooks in Your Email Provider#
In your provider’s dashboard (e.g., SendGrid), set a webhook URL (e.g., https://yourdomain.com/api/email/bounce) to receive bounce events.
Step 2: Create a Sails Route and Controller#
Add a route in config/routes.js:
'POST /api/email/bounce': 'EmailController.handleBounce'Create api/controllers/EmailController.js:
module.exports = {
handleBounce: async function (req, res) {
try {
// Verify webhook signature (critical for security!)
const signature = req.headers['x-sendgrid-signature']; // Provider-specific
const isValid = await sails.helpers.verifyWebhookSignature(req.body, signature);
if (!isValid) {
return res.status(403).send('Invalid signature');
}
// Parse bounce data (provider-specific format)
const events = req.body; // SendGrid sends an array of events
for (const event of events) {
if (event.event === 'bounce') {
const { email, reason, status } = event;
sails.log.warn(`Bounce detected for ${email}: ${reason} (Status: ${status})`);
// Update user record (e.g., mark email as invalid)
await User.updateOne({ email }).set({ isEmailValid: false });
}
}
return res.status(200).send('Bounce handled');
} catch (error) {
sails.log.error('Error handling bounce:', error);
return res.status(500).send('Error processing bounce');
}
}
};Handling Email Failures#
Failures occur at three stages: validation, SMTP transmission, and delivery. Handle each type explicitly.
6.1 Validation Failures#
Caught before sending (e.g., invalid email format). Use the validate-email helper in your controller before calling EmailService.send():
// In a controller action (e.g., UserController.sendWelcomeEmail)
try {
await sails.helpers.validateEmail({ email: user.email });
await EmailService.send({
to: user.email,
subject: 'Welcome!',
text: 'Thanks for signing up.'
});
} catch (error) {
if (error === 'invalid') {
sails.log.error(`Invalid email: ${user.email}`);
// Notify user or admin
return res.status(400).send('Invalid email address');
}
throw error; // Re-throw other errors
}6.2 SMTP Errors (Transient/Permanent)#
Errors during SMTP transmission (e.g., auth failed, server unavailable). Nodemailer throws errors with code and response properties:
// In EmailService.send() (updated error handling)
try {
// ... send email
} catch (error) {
sails.log.error(`SMTP Error: ${error.message} (Code: ${error.code})`);
// Retry on transient errors (e.g., 421 "Too many requests")
if (error.code === 'EENVELOPE' || error.response.includes('421')) {
sails.log.info('Retrying email...');
// Add retry logic (use a queue like Bull for reliability)
} else {
// Permanent error (e.g., 535 "Auth failed")
throw new Error(`Permanent SMTP error: ${error.message}`);
}
}6.3 Delivery Failures (Bounces)#
Handled via webhooks (Section 5.3). Classify bounces as:
- Hard Bounce: Permanent failure (e.g., invalid email). Never retry.
- Soft Bounce: Temporary failure (e.g., full inbox). Retry later (use a queue like Bull).
Logging & Monitoring#
Log everything to debug issues and track email health:
- Successes: Log
messageId, recipient, and timestamp. - Failures: Log error codes, bounce reasons, and recipient.
- Bounces/Complaints: Log event type, email, and severity.
Use Sails’ built-in logger or a tool like winston for structured logs. Store logs in a database (e.g., MongoDB) for analysis:
// In EmailService.send() (after success)
await EmailLog.create({
email: options.to,
messageId: info.messageId,
status: 'sent',
sentAt: new Date()
});For monitoring, set up alerts (e.g., Slack, PagerDuty) for:
- High bounce rates (>5%).
- SMTP server outages.
- Authentication failures.
Testing Your Setup#
- Validity Checks: Test with
invalid-emailor[email protected]to ensure they’re rejected. - SMTP Errors: Use wrong SMTP credentials to trigger auth failures.
- Bounces: Use your email provider’s test tools (e.g., SendGrid’s test emails) to simulate bounces.
Conclusion#
Verifying email delivery in Sails.js requires a multi-layered approach:
- Pre-send validation to catch invalid emails.
- SMTP error handling for transmission failures.
- Webhooks to track delivery status (bounces, complaints).
- Logging/monitoring to ensure reliability.
By combining Nodemailer’s sending capabilities with validation, webhooks, and robust error handling, you’ll build a system that ensures critical emails reach users—and alerts you when they don’t.