MailSignals

Open tracking

A pixel per message, and a count that means something.

The pixel is the only signal available without a Gmail scope, and it is a weak one: it is fetched by proxies, by prefetchers, by scanners, and by your own mail client. Open tracking in MailSignals is the work of turning that request into a stored fact with a reason attached.

  • ✓✓ Ada Lovelace Proposal: analytics engine, phase two — Attached the revised scope 10:42 Opened once · last 3 minutes ago
  • ✓✓ Kenji Sato Invoice 0142 — Payment terms as agreed Yesterday
  • Priya Raman Quick question about the API limits — Before I commit to the batch size Mon
One checkmark per row, from a local cache, so a list of fifty rows never waits on fifty round trips. A thread with several tracked sends shows the status of the most recent one.

What you see in Gmail

Sent, nothing yet
✓✓Delivered to Apple Mail; read unconfirmed
✓✓Opened by a person
A link was clicked
A reply arrived
Delivery failed

The tooltip on a checkmark is assembled from what is known: "Opened twice · last 14 minutes ago · through a mail proxy, so the count is a lower bound", or "Delivered to Apple Mail, which loads images before anyone reads them", or "Sent, not opened yet". A multi-recipient message adds "N recipients, so we cannot tell which one". The same summary appears in the thread sidebar as a timeline, with Mute and "That was me" beside it.

What the endpoint guarantees

Three rules from the project's constitution shape the tracking endpoint. Each is enforced in the code, not in a comment.

It answers first and records second

The GIF is flushed to the caller before any Firestore work starts, with no-store and the rest of the no-cache set, which Firebase Hosting repeats so they survive the CDN. Mail proxies give up quickly; a slow database must cost us data, never cost the recipient a broken image.

It reveals nothing

An unknown token gets byte-for-byte the same response as a real one, and so does an unparseable path. Anything else would turn the endpoint into an oracle for which tokens exist, and pixel tokens are printed into other people's inboxes.

It never stores a raw IP

Only the network truncated to a /24 (or /48 for IPv6) and a salted hash. The address exists as a local variable in the request handler and is never passed to a write.

What happens to a hit

  1. A cheap shape check on the token, then a per-instance negative cache (5 seconds, 5,000 entries) that absorbs repeats of tokens that missed. A flood of junk cannot turn into a Firestore bill.
  2. A hit for a token that is not registered yet is parked, at most ten per token, and the message is read once more in case the registration landed meanwhile. Parked hits are re-judged and replayed inside the transaction that eventually registers the message, so a proxy that beats the registration by a second loses nothing.
  3. Your settings, your recent view signals for this message and the IP fingerprint are gathered in parallel and handed to the classifier.
  4. The event and the counter update are committed in one transaction, which re-reads the message so concurrent hits stay consistent.
  5. Only a counted open moves "first opened" and "last opened". Every classification lands in a bucket (direct, proxied, unconfirmed, scanner, self), except an unrecognised client, which lands in none. A scanner's fetch must never claim the timestamp the UI renders as "opened 14 minutes ago".

What an open count means, and does not

What an open count means in different situations
SituationWhat MailSignals reports
A recipient reads it in GmailOpened (proxied): Gmail fetched it through its proxy while showing the message. Gmail hides the reader's device and location, and caches the image, so repeat reads may never reach you. The tooltip says "a lower bound".
A recipient reads it in Apple Mail with Privacy ProtectionDelivered, open unconfirmed. Apple downloads the image on delivery whether or not anyone reads. Amber tick, not counted at the default sensitivity.
A recipient reads it in Outlook, Yahoo, Thunderbird, Proton or a browserOpened, with the client named where it can be, at confidence 0.72 to 0.93 depending on how much the request gives away.
A security gateway fetches it before deliveryAutomated scan. Recorded, never counted, never alerted.
You open your own Sent folderYour own view. Withdrawn from the count; see self-open suppression.
Something we cannot identify fetches itUnrecognised. In the timeline, in no bucket, in no total.
Three people were on the To lineOpened, with the note that an open tells you someone read it, not which one. Per-contact statistics are not updated.

Sensitivity is yours to set: strict counts only direct fetches, normal (the default) adds proxied opens, lenient also counts Apple's delivery prefetch. The verdict never changes with the setting; only what is counted does.

Where it lives

functions/src/track.ts
The endpoint: response, routing, the answer-first rule.
functions/src/tracking/record.ts
Recording, fingerprinting, the transaction, the second look after parking.
functions/src/tracking/orphans.ts
Hits that arrived before their message was registered.
shared/src/pixel.ts
The 42-byte GIF, its headers and its markup.
scripts/e2e.mjs
Real HTTP against a deployment: headers, unknown-token indistinguishability, a proxy counted and a curl filtered.

Related: how a request is judged · how your own opens are withdrawn · why Gmail's proxy makes every count a lower bound.

Run it in your own project.

One Firebase project, two subdomains, one deploy script. About an hour the first time, and no subscription afterwards.