Tracking email replies
Integrating reply tracking into your application can turn an incoming response into a meaningful event—for example, converting a cold lead into a hot opportunity the moment they reply. In this guide, we'll walk through how to:
- Send outbound messages with a reusable
Message-ID
for tracking. - Receive and inspect webhooks for new messages.
- Detect replies by matching
In-Reply-To
headers. - Filter out bounces and auto-responses.
PrerequisitesA running EmailEngine instance (see the Getting Started guide).A valid license key (free 14‑day trial enabled; purchase at PostalSys for continued access).An account configured with an IMAP or a Gmail/MS Graph integration.
1. Sending messages with a tracked Message-ID
When sending outbound mail, include a unique Message-ID
header. Store this ID in your database so that replies referencing it can be matched later. Suppress some automated replies by adding X-Auto-Response-Suppress
.
// send.js
async function sendTrackedEmail(accountId, to, subject, html) {
const messageId = `<${Date.now()}-${accountId}@yourdomain.com>`;
const payload = {
messageId,
headers: {
'X-Auto-Response-Suppress': 'OOF'
},
from: 'no-reply@yourdomain.com',
to,
subject,
html
};
// Store messageId in your DB for future matching
await saveMessageRecord({ accountId, messageId, to, subject });
const res = await fetch(`https://your-emailengine.com/v1/account/${accountId}/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
return res.json();
}
Tip: Always persist the exact messageId
string (including angle brackets) in your database.
2. Handling inbound webhook notifications
EmailEngine emits a webhook on every new message. The payload differs slightly between generic IMAP servers and Gmail-based accounts.
- Standard IMAP:
path
contains the folder name (e.g.,INBOX
). - Gmail/GSuite:
path
is only set for All Mail, Junk, Trash. Use thelabels
array to find special-use tags like\\Inbox
,\\Sent
.
Example notification for Gmail accounts:
{
"path": "[Google Mail]/All Mail",
"specialUse": "\\All",
"event": "messageNew",
"data": {
"id": "AAAAAQAAMqo",
"inReplyTo": "<1234567890-yourdomain.com>",
"labels": ["\\Inbox", "Leads"]
}
}
3. Detecting replies
- Check mailbox placement: Ensure the message landed in the inbox:
- For IMAP:
path === 'INBOX'
. - For Gmail:
data.labels.includes('\\Inbox')
.
- For IMAP:
- Match
In-Reply-To
: Look fordata.inReplyTo
matching any storedmessageId
.
function isReply(notification, storedMessageIds) {
const inReplyTo = notification.data.inReplyTo;
const inInbox = (
notification.path === 'INBOX' ||
(notification.data.labels || []).includes('\\Inbox')
);
return inInbox && storedMessageIds.includes(inReplyTo);
}
4. Filtering out bounces and auto-responses
Even with OOF suppression, some automated messages still slip through. Fetch full headers for deeper inspection:
async function fetchHeaders(accountId, messageId) {
const res = await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}`
);
const msg = await res.json();
return msg.headers; // object: lowercase header name → [values]
}
Check these headers:
- Return-Path: If
return-path[0] === '<>'
, skip (bounce). - Auto-Submitted: If present and not
'no'
, skip. - List-ID / List-Unsubscribe: Likely a mailing-list reply; skip if you only care about one-to-one replies.
Also inspect the Subject
for prefixes like Out of Office:
or Auto:
.
function isAutomated(headers) {
const auto = headers['auto-submitted']?.[0];
const subj = headers.subject?.[0] || '';
return (
auto && auto.toLowerCase() !== 'no' ||
/^\s*(auto:|out of office:)/i.test(subj) ||
headers['list-id'] || headers['list-unsubscribe']
);
}
5. Finalizing reply detection
If a message passes all checks, classify it as a genuine reply. You can then:
- Signal lead status: Mark the user interaction as a hot lead.
- Archive content: Fetch full body and attachments via
/message/:id
for further processing.
async function processNotification(accountId, notification) {
const storedIds = await getStoredMessageIds(accountId);
if (!isReply(notification, storedIds)) return;
const headers = await fetchHeaders(accountId, notification.data.id);
if (isAutomated(headers)) return;
// Genuine reply!
await markLeadAsReplied(notification.data.inReplyTo);
// Optionally, fetch full content:
const fullMsg = await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${notification.data.id}`
).then(r => r.json());
// Process fullMsg.text, attachments, etc.
}
And that’s it! With these steps, you’ll reliably detect replies and trigger workflows based on genuine user responses.