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#

  1. Prerequisites
  2. Setting Up Nodemailer in Sails.js
    • 2.1 Install Dependencies
    • 2.2 Configure SMTP Transport
  3. Email Validity Check: Before Sending
    • 3.1 Basic Format Validation
    • 3.2 Advanced Validation (Optional)
  4. Sending Emails with Nodemailer: Immediate Success vs. Delivery
    • 4.1 Creating an Email Service
    • 4.2 Interpreting "Immediate Success"
  5. 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
  6. Handling Email Failures
    • 6.1 Validation Failures
    • 6.2 SMTP Errors (Transient/Permanent)
    • 6.3 Delivery Failures (Bounces)
  7. Logging & Monitoring
  8. Testing Your Setup
  9. Conclusion
  10. 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 --save

If 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 emailnot 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-email or [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:

  1. Pre-send validation to catch invalid emails.
  2. SMTP error handling for transmission failures.
  3. Webhooks to track delivery status (bounces, complaints).
  4. 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.

References#