keepcalculator

06 Numbers and Powers

Email Suppression Lists in Node.js: A How-To

Card No. 06·54875 · Updated · Standard formula, shown below

This guide walks you through building and managing email suppression lists in Node.js. You’ll learn how to store suppressed addresses, check them before sending, and remove them when needed. By the end, you’ll have a working module that keeps your email campaigns clean and your deliverability high.

What You’ll Need

  • Node.js installed (version 12 or later)
  • An email-sending service (like SendGrid, SES, or a custom SMTP server) – you’ll integrate your suppression list with it
  • A database to store the suppression list – this guide uses SQLite for simplicity, but you can adapt it to PostgreSQL, MySQL, or even a simple JSON file for small lists
  • Basic familiarity with Express or another web framework if you plan to expose endpoints for managing the list

Step-by-Step

  1. Set up your project

    Create a new directory and initialize npm:

    mkdir suppression-list && cd suppression-list
    npm init -y

    Install the necessary packages. We’ll use better-sqlite3 for the database and nodemailer for sending emails (you can swap in your own email provider).

    npm install better-sqlite3 nodemailer
  2. Create the database schema

    Create a file db.js that sets up a SQLite database with a suppressions table. The table should store the email address, the reason for suppression (e.g., ‘bounce’, ‘complaint’, ‘unsubscribe’), and a timestamp.

    const Database = require('better-sqlite3');
    const db = new Database('suppressions.db');
    
    db.exec(`CREATE TABLE IF NOT EXISTS suppressions (
      email TEXT PRIMARY KEY,
      reason TEXT NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )`);
    
    module.exports = db;
  3. Implement the core suppression functions

    Create a file suppression.js that exports functions to add, check, and remove suppressions. Normalize emails to lowercase to avoid duplicates.

    const db = require('./db');
    
    function normalizeEmail(email) {
      return email.trim().toLowerCase();
    }
    
    function addSuppression(email, reason) {
      const normalized = normalizeEmail(email);
      const stmt = db.prepare('INSERT OR REPLACE INTO suppressions (email, reason) VALUES (?, ?)');
      stmt.run(normalized, reason);
    }
    
    function isSuppressed(email) {
      const normalized = normalizeEmail(email);
      const row = db.prepare('SELECT 1 FROM suppressions WHERE email = ?').get(normalized);
      return !!row;
    }
    
    function removeSuppression(email) {
      const normalized = normalizeEmail(email);
      db.prepare('DELETE FROM suppressions WHERE email = ?').run(normalized);
    }
    
    function listSuppressions() {
      return db.prepare('SELECT * FROM suppressions').all();
    }
    
    module.exports = { addSuppression, isSuppressed, removeSuppression, listSuppressions };
  4. Integrate suppression checks before sending

    In your email-sending code, check if the recipient is suppressed before you send. If they are, skip the send and log it.

    const { isSuppressed } = require('./suppression');
    const nodemailer = require('nodemailer');
    
    async function sendEmail(to, subject, body) {
      if (isSuppressed(to)) {
        console.log(`Skipping suppressed email: ${to}`);
        return { skipped: true };
      }
    
      // Configure your transporter (use your own SMTP or provider)
      const transporter = nodemailer.createTransport({ /* your config */ });
    
      const info = await transporter.sendMail({
        from: 'you@example.com',
        to,
        subject,
        text: body
      });
    
      return { skipped: false, info };
    }
  5. Handle bounce and complaint webhooks

    Most email providers send webhook notifications when an email bounces or a user complains. Set up an endpoint (e.g., in Express) that receives these events and adds the email to the suppression list.

    const express = require('express');
    const { addSuppression } = require('./suppression');
    
    const app = express();
    app.use(express.json());
    
    app.post('/webhook/bounce', (req, res) => {
      // Parse the payload according to your provider's format
      const { email } = req.body;
      addSuppression(email, 'bounce');
      res.sendStatus(200);
    });
    
    app.post('/webhook/complaint', (req, res) => {
      const { email } = req.body;
      addSuppression(email, 'complaint');
      res.sendStatus(200);
    });
    
    app.listen(3000);
  6. Provide an endpoint to manage suppressions (optional)

    You may want to allow users to unsubscribe or admins to remove a suppression. Create simple endpoints:

    app.post('/unsubscribe', (req, res) => {
      const { email } = req.body;
      addSuppression(email, 'unsubscribe');
      res.sendStatus(200);
    });
    
    app.delete('/suppression/:email', (req, res) => {
      removeSuppression(req.params.email);
      res.sendStatus(200);
    });
    
    app.get('/suppressions', (req, res) => {
      res.json(listSuppressions());
    });
  7. Test the flow

    Write a small test script to verify that the suppression functions work:

    const { addSuppression, isSuppressed, removeSuppression } = require('./suppression');
    
    addSuppression('test@example.com', 'bounce');
    console.log(isSuppressed('test@example.com')); // true
    console.log(isSuppressed('TEST@example.com')); // true (case-insensitive)
    removeSuppression('test@example.com');
    console.log(isSuppressed('test@example.com')); // false

Common Mistakes

  • Not normalizing emails: If you store ‘User@Example.com’ and later check ‘user@example.com’, they won’t match. Always lowercase and trim.
  • Ignoring webhooks: Failing to process bounce/complaint webhooks means your suppression list becomes stale and you keep sending to invalid addresses, hurting your sender reputation.
  • Using a single suppression list for all sending: You might have different domains or types of emails (transactional vs. marketing). A global suppression list can block legitimate transactional emails. Consider separate lists or a tag field.
  • Not cleaning up the list: Suppressions can be temporary (e.g., a transient bounce). Over-suppressing can reduce your audience unnecessarily. Implement a retention policy or allow re-subscription with confirmation.

Tips

  • Use a dedicated library: If you’re using a major email provider, they often have SDKs that handle suppression lists for you (e.g., SendGrid’s suppression management). But for custom setups, this DIY approach works.
  • Batch checks: If you’re sending to a large list, check emails in batches rather than one by one to reduce database load.
  • Cache the suppression list: For high-volume sending, load the entire list into memory (or use Redis) to avoid hitting the database for every email.
  • Log everything: Keep a log of when suppressions are added and removed for debugging and compliance.

Troubleshooting

  • Suppression not being detected: Check that you’re normalizing emails consistently. Also, verify that you’re using the same database instance (if you open multiple connections, you might have stale data).
  • Webhooks not updating the list: Test your webhook endpoint with a tool like Postman. Ensure the payload format matches what your provider sends. Log the incoming body to see what you’re actually receiving.
  • Database locked errors: SQLite can lock if multiple processes write simultaneously. Use WAL mode or switch to a client-server database for production.
  • Emails still being sent to suppressed addresses: Double-check that you’re calling isSuppressed before every send, including retries. If you have multiple sending paths, make sure they all go through the same check.

Frequently Asked Questions

What counts as a suppression?

Common reasons include hard bounces (permanent delivery failures), spam complaints, and explicit unsubscribes. Soft bounces (temporary issues) are often not suppressed permanently.

Should I suppress on soft bounces?

Generally no. Soft bounces (e.g., mailbox full) are temporary. Suppressing them permanently would lose valid contacts. Instead, track retry counts and suppress only after repeated failures.

How long should I keep a suppression?

It depends on your policy. Some keep them forever, others remove after a period (e.g., 6 months) if the user re-subscribes. For unsubscribes, you may be legally required to honor them indefinitely.

Can I use a JSON file instead of a database?

For very small lists (under a few thousand), a JSON file is fine. But for production, use a real database to handle concurrency and scaling.

How do I handle suppression lists across multiple email providers?

You can centralize your suppression list in your own database and check it before sending, regardless of provider. This is what this guide does.

What if a user re-subscribes after being suppressed?

You should remove them from the suppression list only if they explicitly opt back in (e.g., via a double opt-in). For unsubscribes, many jurisdictions require you to keep them suppressed unless they explicitly re-subscribe.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →