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
- Set up your project
Create a new directory and initialize npm:
mkdir suppression-list && cd suppression-list npm init -yInstall the necessary packages. We’ll use
better-sqlite3for the database andnodemailerfor sending emails (you can swap in your own email provider).npm install better-sqlite3 nodemailer - Create the database schema
Create a file
db.jsthat sets up a SQLite database with asuppressionstable. 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; - Implement the core suppression functions
Create a file
suppression.jsthat 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 }; - 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 }; } - 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); - 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()); }); - 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
isSuppressedbefore 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
- Campus Tech Innovation Hubs: What They Are
- Poco C51 Airtel Prepaid Locked Variant India?
- Jenkins vs GitHub Actions: Which CI/CD Fits Your Team?
- Turn Off Backup Encryption in iTunes?
- Shared VPS Hosting: Is It Better?
- Disk Defrag Pro: What It Does and Whether You Need It