Detecting auto-replies before your agent acts on them is one of the most important failure modes to handle in any agent email workflow — and one of the least documented. Miss it, and your agent can exchange thousands of messages with a vacation responder in under an hour, burning through send allowances, triggering abuse filters, and potentially getting your domain flagged.
This post covers the complete detection stack: RFC-standard headers, provider-specific headers, subject heuristics, and the architectural pattern that keeps loops from forming even when detection is imperfect.
Why agent email loops are dangerous
A loop forms when your agent receives an auto-reply, interprets it as a message requiring a response, and sends one — which triggers another auto-reply. Each round trip takes seconds. At a conservative estimate, a loop running for 10 minutes against a single OOO responder can generate hundreds of messages.
The consequences compound:
- Rate limit exhaustion: your sending allowance gets consumed by useless traffic.
- Reputation damage: high bounce-adjacent traffic from a single thread looks like spam behavior to receiving MTAs.
- Downstream breakage: if your agent is part of a larger workflow (ticket creation, CRM updates, invoicing), each looped message may spawn a duplicate record.
- Alert fatigue: if you have webhook-based monitoring, the event stream floods with noise, masking real issues.
Preventing this isn't optional. It's a correctness requirement.
The RFC-standard headers that identify auto-replies
The most reliable detection signal is the Auto-Submitted header, defined in RFC 3834. Any conforming mail system generating an automatic response must include it.
Auto-Submitted: auto-replied
Auto-Submitted: auto-generated
Auto-Submitted: auto-notified
The value no means it was manually sent. Anything else means automatic. Your first check should always be:
function isAutoSubmitted(headers: Record<string, string>): boolean {
const val = headers["auto-submitted"]?.toLowerCase();
return !!val && val !== "no";
}
Note the lowercase normalization — header names and values arrive inconsistently capitalized depending on the MTA.
Precedence header
Older systems (many corporate Exchange deployments still in production) use the Precedence header instead of or in addition to Auto-Submitted:
Precedence: bulk
Precedence: junk
Precedence: list
These signal that the message is automated or low-priority. A Precedence: bulk message should never receive an agent reply.
function isBulkPrecedence(headers: Record<string, string>): boolean {
const val = headers["precedence"]?.toLowerCase();
return val === "bulk" || val === "junk" || val === "list";
}
X-Auto-Response-Suppress
Microsoft Exchange and Office 365 use X-Auto-Response-Suppress to tell receiving systems not to send automated responses back:
X-Auto-Response-Suppress: OOF
X-Auto-Response-Suppress: AutoReply
X-Auto-Response-Suppress: All
X-Auto-Response-Suppress: DR, OOF, RN, NRN
If this header is present with any value other than None, treat the message as an auto-reply. Microsoft's own guidance specifies these values in the Exchange documentation.
function suppressesAutoResponse(headers: Record<string, string>): boolean {
const val = headers["x-auto-response-suppress"]?.toLowerCase();
return !!val && val !== "none";
}
Provider-specific headers
Beyond the RFC, major email providers add their own signals. Not standardized, but consistent enough to rely on.
| Provider | Header | Typical Value |
|---|---|---|
| Gmail | X-Google-Smtp-Source + Auto-Submitted |
auto-replied |
| Outlook / Exchange | X-MS-Exchange-Inbox-Rules-Loop |
1 |
| Outlook OOF | X-MS-Exchange-Generated-Message-Source |
OOF |
| Yahoo Mail | X-Yahoo-Newman-Property |
ymail-4 |
| Generic bulk | List-Id, List-Unsubscribe |
any value |
| Mailing lists | Mailing-List |
any value |
The List-Id and List-Unsubscribe headers are defined in RFC 2919 and RFC 2369. Any message bearing them is from a mailing list or marketing system — your agent should not reply.
function isListMessage(headers: Record<string, string>): boolean {
return !!headers["list-id"] || !!headers["list-unsubscribe"] || !!headers["mailing-list"];
}
Subject-line heuristics as a fallback
Header-based detection catches most auto-replies, but some legacy systems and misconfigured mailers omit Auto-Submitted entirely. Subject-line matching is an imperfect but necessary fallback.
Build a case-insensitive pattern set covering the most common prefixes:
const AUTO_REPLY_SUBJECT_PATTERNS = [
/^out of office/i,
/^automatic reply/i,
/^auto[- ]?reply/i,
/^autoreply/i,
/^vacation/i,
/^away from/i,
/^on leave/i,
/^\[?ooo\]?/i,
/^delivery status notification/i,
/^undeliverable/i,
/^mail delivery failed/i,
/^mailer-daemon/i,
/^returned mail/i,
];
function subjectLooksAutomatic(subject: string): boolean {
return AUTO_REPLY_SUBJECT_PATTERNS.some((pattern) => pattern.test(subject.trim()));
}
Keep this list conservative. False positives — blocking a legitimate message because it contains "vacation" — are worse than false negatives in most agent workflows. If uncertain, route to a human review queue rather than dropping silently.
From-address signals: Mailer-Daemon and MAILER-DAEMON
Bounce messages arrive with a null return path (Return-Path: <>) and often a From address of MAILER-DAEMON@ or postmaster@. These are delivery status notifications (DSNs), not human messages.
function isBounceOrDSN(headers: Record<string, string>, from: string): boolean {
const returnPath = headers["return-path"]?.trim();
const isNullReturnPath = returnPath === "<>" || returnPath === "";
const fromLower = from.toLowerCase();
const isMailerDaemon =
fromLower.startsWith("mailer-daemon") || fromLower.startsWith("postmaster");
return isNullReturnPath || isMailerDaemon;
}
Your agent should log DSNs — they indicate a delivery problem worth investigating — but never reply to them.
Composing the full detection function
Combine all signals into a single guard function that runs before any agent reply logic:
interface InboundMessage {
from: string;
subject: string;
headers: Record<string, string>;
}
function isAutoReply(msg: InboundMessage): boolean {
return (
isAutoSubmitted(msg.headers) ||
isBulkPrecedence(msg.headers) ||
suppressesAutoResponse(msg.headers) ||
isListMessage(msg.headers) ||
isBounceOrDSN(msg.headers, msg.from) ||
subjectLooksAutomatic(msg.subject)
);
}
This is the gate your webhook handler should call before passing any message.received event to your agent's LLM.
import { mails } from "@mailsai/sdk";
const hello = mails.agent("hello");
hello.onMessage(async (event) => {
const { data } = event;
if (isAutoReply({
from: data.from.address,
subject: data.subject,
headers: data.headers ?? {},
})) {
console.log(`Auto-reply detected from ${data.from.address}, skipping.`);
return; // Do not invoke the LLM or send any reply
}
// Safe to process as a genuine inbound message
});
Mails.ai's inbound email parsing delivers the raw headers alongside the parsed message body in the webhook payload, which is what makes header-based detection practical without a separate IMAP parsing layer.
The architectural pattern: loop-break budget
Detection can fail. A new OOO system might omit all standard headers and use a subject line your patterns don't cover. Defense-in-depth means adding a loop-break budget at the thread level.
Track how many times your agent has sent within a single thread_id. If it exceeds a threshold — two or three is reasonable — stop sending and flag the thread for review.
const THREAD_SEND_LIMIT = 3;
async function agentSendBudget(
threadId: string,
sendFn: () => Promise<void>
): Promise<void> {
const key = `thread_sends:${threadId}`;
const count = await redis.incr(key);
await redis.expire(key, 86400); // 24-hour window
if (count > THREAD_SEND_LIMIT) {
console.warn(`Thread ${threadId} exceeded send budget. Suppressing.`);
await flagThreadForReview(threadId);
return;
}
await sendFn();
}
This is a circuit breaker pattern applied to email threads. The loop-break budget is your last line of defense when detection fails.
sequenceDiagram participant Agent participant DetectFn as isAutoReply participant Budget as LoopBreakBudget participant LLM Agent->>DetectFn: message received DetectFn-->>Agent: true auto-reply detected Agent->>Agent: skip silently Agent->>DetectFn: genuine message DetectFn-->>Agent: false Agent->>Budget: check thread send count Budget-->>Agent: under limit Agent->>LLM: invoke reasoning LLM-->>Agent: reply content Agent->>Agent: send reply
Handling edge cases
Legitimate replies that look automated: some CRM systems send replies on behalf of users with Precedence: bulk set incorrectly. If your workflow involves replies from sales automation tools, you may need a whitelist by sender domain that bypasses the Precedence check.
Delivery status notifications vs. legitimate failures: a DSN telling you that a message hard-bounced is operationally important — you should update your suppression list. Log DSNs to a separate handler rather than routing them to the auto-reply discard path.
Auto-replies from your own domain: if your agent sends to addresses on the same domain (internal workflows), and those addresses have OOO rules, you'll see auto-replies from yourself. The isAutoReply function handles this correctly, but it's worth testing explicitly in staging.
Logging and observability
Never silently drop auto-replies without a log record. You need this data to:
- Debug false positives when a legitimate message gets suppressed
- Track which external senders generate the most auto-replies (useful for list hygiene)
- Monitor for sudden spikes that indicate a loop is forming despite detection
At minimum, log the thread_id, from address, which detection rule fired, and a timestamp. Store this in your existing observability stack — the same place you track your agent's send events. The agent email use cases that benefit most from this pattern are appointment scheduling, contract workflows, and anything with a human-in-the-loop approval step, where a missed detection could stall a real process.
Frequently Asked Questions
What is the most reliable single header for detecting auto-replies?
Auto-Submitted is the most reliable single signal, defined in RFC 3834. Any conforming mail system generating automatic responses must include it with a value other than no. Check it first. For Microsoft Exchange and Office 365 environments, X-Auto-Response-Suppress is equally reliable and often present even when Auto-Submitted is not.
Can I rely on subject-line matching alone?
No. Subject-line matching is a fallback for legacy or misconfigured senders. It should be the last check in your detection chain, not the primary one. Relying on subjects alone means you'll miss any auto-reply with an unusual prefix, and you risk false positives on legitimate messages that happen to contain common keywords.
Should I reply to bounce messages?
Never. Bounce messages (DSNs) have a null return path (Return-Path: <>) and originate from MAILER-DAEMON or postmaster addresses. Replying to them is a protocol violation and will have no effect — the address is not a real mailbox. Log them and use the bounce data to update your suppression list.
What is a safe loop-break budget for most agent workflows?
Two to three agent sends per thread within a 24-hour window is a reasonable default. Adjust based on your use case: a scheduling agent that expects multiple back-and-forth confirmations might allow five, while a notification agent sending one-way updates should allow only one. The key is that the limit exists and is enforced at the thread level, not just the message level.
How do mailing list headers fit into this?
List-Id (RFC 2919) and List-Unsubscribe (RFC 2369) indicate the message originated from a mailing list or marketing system. Your agent should never auto-reply to these — doing so can get you flagged as a source of reply-to-list spam. Treat any message bearing these headers the same as an auto-reply: log and skip.
Do I need to handle this differently for inbound webhooks versus IMAP polling?
The detection logic is identical — it operates on message headers and subject, which are present regardless of how you receive the email. The only difference is timing: a webhook fires within seconds of delivery, while IMAP polling introduces a delay proportional to your poll interval. For agent workflows, webhook delivery is strongly preferred, both for latency and to make detection-before-action easier to enforce in code.
Related guides: AI for Email: Integrating Agents Into Transactional Flows, AI for Email Automation: Triggers, Parsing & Actions, Inbound Email Webhooks for AI Agents: Parse & Act and AI Agent Transactional Email Patterns for Developers.
Explore the product: Inbound email parsing, Sender reputation, Email classification, All features and Pricing.
