DEV Community

Rishi Kumar
Rishi Kumar

Posted on

Building a Simple Email-Open Tracking System for Your Gmail

In today’s email landscape, marketing platforms typically bundle “open” or “read” tracking by default. But what if you want a personal tracking pixel system—one where you can see open logs for certain one-off emails you send from Gmail?


GitHub Link : Mail Tracker

Roll your own Node.js + Express + EJS + SQLite app:

  1. Generates 1×1 pixels tied to unique IDs,
  2. Logs open events (date/time, IP, user agent),
  3. Serves a real .png file so Gmail/other clients see a standard image.

You’ll get a lightweight dashboard showing which pixels were created, plus the logs of open events. While this won't be 100% foolproof (due to image blocking or proxies like GoogleImageProxy), it’s a fun, educational way to see basic open-tracking in action for your personal emails.


1. Prerequisites

  • Node.js installed locally.
  • Some familiarity with Express (for routing).
  • A willingness to tinker with EJS (template engine) and SQLite for storing data.

Tip: If you want other people (external recipients) to open your pixel, your server must be accessible to the outside world. Hosting on a platform like Render, Heroku, or using a tunnel service (e.g. ngrok or localtunnel) can help.


2. Core Concepts

2.1. Tracking Pixel

A “tracking pixel” is just a hidden image in your email. If the recipient’s client loads images, your server sees an HTTP request for that 1×1 PNG (or GIF).

2.2. Logging Opens

When someone (or some proxy) fetches /tracker/<pixel_id>.png, you record:

  • Timestamp (when it was fetched),
  • IP address,
  • User agent (might show GoogleImageProxy for Gmail, indicating the real user’s IP is masked).

2.3. Gmail & Image Proxies

Modern Gmail proxies images (served from ggpht.com or similar). This means:

  • You won’t see the recipient’s real IP address.
  • Gmail often fetches the pixel once and caches it, so subsequent opens might not appear as a new log.

Despite these limitations, you can still see if/when the image was first fetched—often close to when the user opened your email for the first time.


3. Our Simple Node App

3.1. Project Setup

Create a folder (say mail-tracker), then:

cd mail-tracker
npm init -y
npm install express ejs sqlite3 uuid
Enter fullscreen mode Exit fullscreen mode

You’ll end up with a package.json that references these dependencies. Next:

  1. Create a public/images folder with a 1×1 transparent PNG named pixel.png.
  2. Create two EJS files in a views folder: index.ejs (dashboard) and logs.ejs (show logs).

3.2. app.js (Main Server File)

Below is a simplified excerpt. It:

  • Stores pixel data in SQLite (mail-tracker.db).
  • Serves static files (so pixel.png can be loaded if we want).
  • Has a /tracker/:id.png route that logs an open and sends back the real pixel.png.
const express = require('express');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const sqlite3 = require('sqlite3').verbose();

const app = express();
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: false }));

// Serve static files from /public
app.use(express.static(path.join(__dirname, 'public')));

// Initialize SQLite
const db = new sqlite3.Database(path.join(__dirname, 'mail-tracker.db'), (err) => {
  // Create tables if not existing
});

// Middleware to get baseUrl for EJS
app.use((req, res, next) => {
  const protocol = req.protocol;
  const host = req.get('host');
  res.locals.baseUrl = `${protocol}://${host}`;
  next();
});

// Dashboard: list all pixels
app.get('/', (req, res) => {
  // SELECT * FROM pixels ...
  res.render('index', { pixels });
});

// Create pixel
app.post('/create', (req, res) => {
  const pixelId = uuidv4();
  // Insert pixel into DB
  // redirect to '/'
});

// The tracker route
app.get('/tracker/:id.png', (req, res) => {
  // Check pixel ID, log open in DB
  // Serve the real 'pixel.png'
  res.sendFile(path.join(__dirname, 'public', 'images', 'pixel.png'));
});

// View logs
app.get('/logs/:id', (req, res) => {
  // SELECT logs from DB for that pixel
  res.render('logs', { pixel, logs });
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Listening on ${PORT}`));
Enter fullscreen mode Exit fullscreen mode

3.3. index.ejs (Dashboard)

Shows a form to create a new pixel plus a table listing existing ones:

<!DOCTYPE html>
<html>
  <head>
    <title>Mail Tracker - Dashboard</title>
    <meta charset="UTF-8" />
  </head>
  <body>
    <h1>Mail Tracker - Dashboard</h1>

    <h2>Create a New Tracking Pixel</h2>
    <form action="/create" method="POST">
      <label>Pixel Name (optional):</label>
      <input type="text" name="name" />
      <button type="submit">Create Pixel</button>
    </form>

    <hr />

    <h2>Existing Pixels</h2>
    <!-- For each pixel, show tracker URL like: baseUrl/tracker/PIXEL_ID.png -->
    <!-- Link to logs page to see open events -->
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

3.4. logs.ejs (Show Logs)

Lists each open event (time, IP, user agent). We can format the time, group rapid logs with color, etc.:

<!DOCTYPE html>
<html>
  <head>
    <title>Mail Tracker - Logs</title>
    <meta charset="UTF-8" />
  </head>
  <body>
    <h1>Logs for <%= pixel.name %></h1>
    <p>Created At: <%= pixel.createdAt %></p>

    <h2>Open Events</h2>
    <p style="font-style: italic;">
      You may see extra logs if bots (for example, GoogleImageProxy via ggpht.com) or the email client repeatedly load the image.
      Check each log’s timestamp to distinguish real user opens from proxy fetches.
    </p>

    <table>
      <thead>
        <tr><th>Time</th><th>IP</th><th>User-Agent</th></tr>
      </thead>
      <tbody>
        <% // For each log, show the local-time date, IP, userAgent, etc. %>
      </tbody>
    </table>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

4. Embedding into Gmail

4.1. Copy the Tracker URL

Once your server is running publicly, your pixel has a URL like:

https://myapp.example.com/tracker/1234abcd-....png
Enter fullscreen mode Exit fullscreen mode

Or, if you’re using a local tunnel:

https://random.loca.lt/tracker/1234abcd-....png
Enter fullscreen mode Exit fullscreen mode

4.2. Insert Photo by URL

  1. Compose a new email in Gmail.
  2. Click Insert photoWeb Address (URL).
  3. Paste the Tracker URL.
  4. If Gmail says “We can’t find or access that image,” you can still insert and send.
  5. When the recipient opens (and images are enabled), Gmail loads (or caches) that image, and your logs record an event.

Note: Because Gmail proxies images, the request will likely come from Google servers (e.g., GoogleImageProxy/ggpht.com). You won’t get the real recipient’s IP. But you do see when the pixel was fetched—often correlating with a user open.


5. Understanding “Extra Logs” & Proxy Behavior

  • Multiple Logs: If Gmail or another client refreshes or re-fetches the image, or if the user reopens the message, you’ll see multiple entries. Some might be within seconds of each other, triggered by background processes or spam filters.
  • User Agent: You might see GoogleImageProxy instead of a real browser’s user agent. This is normal for Gmail. Other clients, like Apple Mail or Outlook, might show more direct info.
  • IP: Because of proxies, you’ll usually see a Google IP range, not the actual recipient’s IP. That’s a privacy measure.

6. Limitations & Future Enhancements

  1. Image-Blocking: If the recipient’s mail client blocks images by default, your pixel is never loaded. So you won’t see an open event.
  2. Caching: Gmail especially caches images. Subsequent opens may not trigger a new request.
  3. No IP / Location: With proxies, you don’t see the real user’s IP or location.
  4. Unique Query Params: If you want to track individual recipients, create a separate pixel for each person or append query strings like ?user=john@example.com.

7. Why Build a Personal Tracker?

  • Educational: Learn how open tracking works under the hood.
  • Privacy: You control your own data, rather than trusting a third-party marketing provider.
  • Debugging: If you want to see if a friend/client is reading your email, or simply confirm some one-time outreach was opened.

(Always be mindful of legal or ethical constraints in your region and among your contacts.)


8. Conclusion

Running your own open-tracking service can be a fun side project—especially to see how Gmail or other providers handle images. You’ll quickly discover that GoogleImageProxy can mask real IP addresses, and not every open is captured if images are off. Nonetheless, for personal use, it’s a neat way to see open events in real time.

Key Steps Recap:

  1. Build a Node.js + Express server.
  2. Generate unique IDs/pixels.
  3. Log requests to /tracker/:id.png.
  4. Serve a legitimate .png file.
  5. Embed the pixel in your Gmail messages.
  6. Check your logs for open events (and interpret carefully).

That’s it—your own personal email tracking system. Once you see it in action, you’ll have a deeper appreciation for how major mailing platforms do open tracking at scale and why they run into the same limitations of caching and image-blocking.


GitHub Link : Mail Tracker

Top comments (0)