Creating an Email Tracking System with Node.js and Deploying on Vercel

Tired of wondering if your emails are read or ignored? Let's build something cool. In this tutorial, we'll create a simple email tracking system using Node.js and Vercel. You'll learn to make an invisible image that reports when an email is opened. Ready? Let's dive in.

Understanding How the Code Works

The core of this system is a Node.js Express server that serves a 1x1 transparent PNG image. When an email containing this image is opened, the recipient's email client sends a request to our server. This triggers our tracking mechanism, allowing us to log the event and serve the image. Here's a breakdown of the main components:

  • Express server setup
  • Middleware for logging requests
  • Route for serving the transparent image
  • Integration with Vercel Blob for storing logs

// Import required modules
const app = require('express')(); // Create an Express application
const { put } = require('@vercel/blob'); // Import the 'put' function from Vercel Blob for storing logs
const Jimp = require('jimp'); // Import Jimp for image manipulation
const path = require('path'); // Import path module for file path operations

// Enable trust proxy to get the correct IP address when behind a reverse proxy
app.set('trust proxy', true);

// Set the port for the server to listen on
const PORT = process.env.PORT || 3000;

// Middleware to log requests and store them in Vercel Blob
app.use(async (req, res, next) => {
const now = new Date(); // Get the current date and time
const imageName = path.parse(req.path).name; // Extract the filename (without extension) from the request path

const ip = req.ip; // Get the IP address of the client
const log = `${now.toISOString()} - ${imageName} - IP: ${ip}`; // Create a log entry

try {
// Store the log entry in Vercel Blob under the 'gmail' directory
await put(`gmail/${imageName}.txt`, log, {
access: 'public', // Make the log file publicly accessible
addRandomSuffix: false, // Do not add a random suffix to the filename
});
} catch (error) {
console.error("Error writing log to Vercel Blob:", error); // Log any errors that occur while writing the log
}
next(); // Continue to the next middleware or route handler
});

// Route to respond with a simple "Hello" message
app.get('/', (req, res) => {
res.send('Hello');
});

// Route to serve a 1x1 transparent PNG image
app.get('/image/:filename.png', (req, res) => {
const width = 1;
const height = 1;

// Create a new 1x1 transparent image using Jimp
new Jimp(width, height, 0x00000000, (err, image) => {
if (err) {
console.error("Error creating image:", err);
return res.status(500).send("Error creating image");
}
// Convert the image to a buffer and send it as the response
image.getBuffer(Jimp.MIME_PNG, (err, buffer) => {
if (err) {
console.error("Error getting image buffer:", err);
return res.status(500).send("Error processing image");
}
res.set('Content-Type', 'image/png'); // Set the response content type to PNG
res.send(buffer); // Send the image buffer as the response
});
});
});

// Start the server and listen on the specified port
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`);
                });

Step 1: Setting Up the Environment

Before you start coding, ensure that you have Node.js installed on your system. If not, you can download it from Node.js.

Next, install the necessary packages by running the following commands in your terminal:

npm install express 

These packages include:

  • Express: A minimal and flexible Node.js web application framework.

Step 2: Understanding Jimp Usage

Jimp (JavaScript Image Manipulation Program) is used in this code to create a 1x1 transparent PNG image. Here's how it works:

  1. Jimp is imported at the beginning of the file:
  2. const Jimp = require('jimp');
  3. In the route handler for '/image/:filename.png', Jimp creates the image:
  4. // Create a new 1x1 transparent image using Jimp
    new Jimp(width, height, 0x00000000, (err, image) => {
    // Error handling if image creation fails
    if (err) {
    console.error("Error creating image:", err);
    return res.status(500).send("Error creating image");
    }
    
    // Convert the image to a PNG buffer
    image.getBuffer(Jimp.MIME_PNG, (err, buffer) => {
    // Error handling if buffer creation fails
    if (err) {
    console.error("Error getting image buffer:", err);
    return res.status(500).send("Error processing image");
    }
    
    // Set the content type of the response to PNG
    res.set('Content-Type', 'image/png');
    
    // Send the PNG buffer as the response
    res.send(buffer);
    });
    });
  5. The image is created with a width and height of 1 pixel, and a fully transparent color (0x00000000).
  6. The image is then converted to a buffer in PNG format and sent as the response.

This approach allows us to serve a tiny, invisible image that can be used for tracking email opens without affecting the email's appearance.

Step 3: Deploying the Project on Vercel

Deploying your Node.js application on Vercel is straightforward. Here's the process:

  1. Create a Vercel account if you don't have one: Vercel
  2. Install the Vercel CLI by running the following command in your terminal:
  3. npm install -g vercel
  4. Configure your project with a vercel.json file to specify build and routing configurations. Here is an example:
  5. {
        "version": 2, // Specifies the Vercel configuration schema version being used.
        "builds": [
        {
            "src": "./app.js", // Path to the source file that Vercel will build.
            "use": "@vercel/node" // Indicates that Vercel should use the Node.js runtime for this build.
        }
        ],
        "routes": [
        {
            "src": "/(.*)", // Pattern to match all incoming requests.
            "dest": "/app.js" // All requests are routed to the app.js file.
        }
        ]
                                    }
  6. Connect your local project with your Vercel account using the command:
  7. vercel
  8. Upload any changes to Vercel with the command:
  9. vercel --prod

Step 4: Setting Up Vercel Blob for Storing Logs

To store the logs of when emails are opened, I used Vercel's Blob storage. Here's how to set it up:

  1. Install the @vercel/blob package by running the following command in your terminal:
  2. npm install @vercel/blob
  3. Configure Vercel Blob in your project settings:
    • Go to your project in the Vercel dashboard.
    • Navigate to the "Storage" section.
    • Enable Blob storage for your project.
  4. Use the put function from the @vercel/blob package to store logs. Here’s a code snippet demonstrating this:
  5. // Import the 'put' function from Vercel Blob for storing logs
    const { put } = require('@vercel/blob');
    
    // Store the log entry in Vercel Blob
    await put(`logs/${imageName}.txt`, log, {
        access: 'public',  // Set the access level to public
        addRandomSuffix: false, // Do not add a random suffix to the filename
    });
  6. Implement error handling for any failed log writes to ensure that your application can handle issues gracefully.

Step 5: Testing Your Email Tracking Script

Moving on to testing, we'll use Gmail to verify your Vercel deployment. Follow along:

  1. Open Gmail and click on "Compose" to create a new email.
  2. Write your email content as usual.
  3. To insert the tracking image, click on the "Insert photo" icon in the formatting toolbar (it looks like a picture).
  4. In the "Insert photo" dialog, choose "Web address (URL)".
  5. In the URL field, paste your Vercel deployment URL followed by the endpoint `/image/` and then add a unique name for the image. For example:
    https://your-vercel-app.vercel.app/image/uniqueimagename.png

    Replace `your-vercel-app` with your actual Vercel app name, and `uniqueimagename` with a name of your choice.

  6. Click "Insert" to add the invisible tracking image to your email.
  7. Send the email to the intended recipient.
  8. When the recipient opens the email, most email applications automatically try to display all images in the message. This action triggers the email software to retrieve the invisible tracking pixel, which sends a GET request to your Vercel app.
  9. Your app will log this request, storing information about when the email was opened in Vercel Blob storage.
  10. After sending your email, you can monitor your Vercel Blob storage for real-time tracking data. This storage contains logs that record each instance when a recipient opens your email.

This tracking method uses an invisible 1x1 PNG image in your email. When opened, it silently contacts your server, providing engagement data without altering the email's appearance or alerting recipients. This allows accurate measurement of open rates and timing while preserving user experience.

Note: Remember that this method isn't 100% reliable as some email clients block images by default or give users the option to not load images. Additionally, be sure to comply with all relevant privacy laws and regulations when implementing email tracking.

Conclusion

With these steps, We successfully created and deployed a simple email tracking system using Node.js and Vercel. The key to this project was understanding how to serve and log image requests, as well as effectively deploying the server to Vercel. I hope this guide helps you in building your own email tracking systems or similar projects!

As an upcoming feature, We plan to add functionality "receive SMS notifications on your phone when someone opens your email". This will provide real-time alerts and further enhance the tracking capabilities of the system. Stay tuned for updates on how this SMS integration will be implemented!