Replies and bounces
The two outcomes a sender can actually observe.
People ask whether the recipient deleted, archived or reported their email. None of that is observable: nothing leaves the recipient's mailbox when they do it, and this product does not pretend otherwise. What is observable is a reply, and Gmail reporting that it could not deliver.
- ↩ Grace Hopper Re: Compiler talk on Thursday — Yes, 3pm works. I will bring the slides 09:15 Replied 2 hours ago · Opened once · last 3 hours ago
- ✕ ops@oldvendor Renewal — Following up on the quote from March Mon Sent, not opened yet · Bounced: ops@oldvendor.example could not be reached
Replies
A reply is the signal that actually matters; an open is only a proxy for it. The Gmail API would make it trivial. Without one it has to come from the page.
The insight
If a thread containing one of your tracked sends gains a newer message from somebody else, that is the reply. The extension is already in the thread view and the Inbox list for other reasons, so this costs nothing extra.
Three detectors
- The open thread. Every message view is walked; a reply is one whose sender is not you and not Gmail's mailer-daemon, dated at or after your newest tracked send. Detection is sender-based, not positional, because a collapsed message has no synchronous id and the positional version gave up on any thread longer than one screen.
- A message view loading. When a message from someone else loads, detection re-runs for up to 60 seconds, because a reply's view is often what Gmail expands after the thread handler has already looked.
- The Inbox list. A row showing more messages than tracked sends we know of in that thread is a reply (or a bounce, if the daemon is among the contacts), reported once per thread per session.
On the server
A reply is applied to the newest tracked message in the thread. A reply answers the last thing you sent; attributing it to an older message would mark that one replied while the real question stays open, and follow-up reminders would then skip the message actually waiting on an answer. The call is idempotent, the earliest reply time is kept, and a real event is written so the timeline and alerts work exactly as they do for opens.
Bounces
Gmail files a delivery-failure notice from mailer-daemon into the same thread. The extension recognises it by its sender and its wording, and reads the visible text to find the address that failed and Gmail's one-line reason. That is the only time it reads any message text, and it sends only those two things.
| Recognised by | Examples |
|---|---|
| Sender | mailer-daemon, postmaster, or a display name matching "Mail Delivery Subsystem" or "delivery status" |
| Subject or body | "Delivery Status Notification (Failure)", "Address not found", "Message not delivered", "Undeliverable", "your message wasn't delivered" |
| Soft rather than hard | "Delivery Status Notification (Delay)", "will keep trying", "has been delayed", "delivery incomplete" |
| The reason | Gmail's one line, tidied to 140 characters: "Address not found", "Mailbox full", a 5.x.x code |
A daemon message that says nothing about delivery is not a bounce. A notice for a thread with no tracked send is simply not matched. Duplicate notices, seen from two Gmail tabs, are counted once.
Engagement outranks a bounce
The status becomes bounced only when nothing has been read, clicked or replied to and every recipient hard-bounced. One bad address among several is shown next to whatever the others did. A soft bounce never changes the status.
A bounce raises its alert once per message, even on a closed thread, because the address is bad and you will want to resend elsewhere; follow-up reminders stop; the contact remembers the failed address; and the timeline gets a "Delivery failed" entry whose Who line reads "Gmail, reporting that it could not deliver."
Closed by you
When you archive or trash a thread, Gmail shows a toast: "Conversation archived", "moved to Trash". The extension watches for it, resolves the affected threads (the selected rows, the open thread, or the thread whose view was just destroyed), and marks their tracked messages closed. Gmail's own Undo within 15 seconds reopens them, and so does a button in the dashboard.
The effect is deliberately small: follow-up reminders stop, and opens and clicks on the thread no longer alert. A reply still does. It is the one thing that should pull a closed conversation back. Every recorded event stays, and nothing is ever inferred from the recipient's side.
The toast patterns are English only. In any other Gmail language closed detection never fires, which is the safe failure; bounce detection still works if the daemon's address and Gmail's English notice text are present.
What cannot be seen, said plainly
- Whether the recipient deleted your email.
- Whether the recipient archived it, or moved it to a folder.
- Whether the recipient reported it as spam.
- Whether the recipient forwarded it (a forward's open looks like any other open).
- A reply on a thread Gmail never renders in front of you.
Where it lives
extension/src/gmail/threads.ts- Open-thread reply and bounce detection.
extension/src/gmail/threadRows.ts- Inbox-row detection.
extension/src/gmail/closed.ts- Archive and trash detection from Gmail's toast.
shared/src/bounce.ts- Recognising and parsing a delivery-failure notice; pure, and unit-tested.
functions/src/api/messages.ts- Recording a reply or bounce, and the closed and reopened marks.
Related: the replied and bounced alerts · the whole flow.
Run it in your own project.
One Firebase project, two subdomains, one deploy script. About an hour the first time, and no subscription afterwards.