Email Auto-Replies: Detect Out-of-Office Messages Without Reply Loops
An out-of-office message is an automatic reply. A delivery status notification is a report. A read receipt is another kind of report. They can all arrive after your application sends email, but they should not enter the same reply workflow.
The safest first rule is simple: classify the structured headers and SMTP envelope before you look at the subject or body. If Auto-Submitted is present with any value other than no, do not automatically reply. If the message is a delivery or disposition report, route it as an event instead. A missing header is inconclusive, not proof that a person wrote the message.
Which headers identify an automatic reply?
RFC 3834 defines Auto-Submitted for this job. A personal out-of-office responder should emit auto-replied. Automatically generated mail that is not a direct reply uses auto-generated. Manually submitted mail can use no.
Auto-Submitted: auto-replied
Return-Path: <>
Content-Type: text/plain; charset=utf-8
The decisive line here is Auto-Submitted: auto-replied. The null return path is another suppression signal, but it does not tell you the exact message type by itself.
What about matching “Out of Office” in the subject? I would not make that a control. Subjects are localized, customizable, and easy to forge. RFC 3834 reserves the Auto: subject prefix as a hint for people, not as a machine-readable classification signal.
How do delivery reports and read receipts differ?
Check the MIME report type before treating a message as a generic automatic response. A delivery status notification, or DSN, uses multipart/report with report-type=delivery-status. It carries delivery outcomes such as delayed or failed recipient handling.
A message disposition notification, or MDN, uses report-type=disposition-notification. It can describe display, deletion, or another disposition. An MDN request is optional for the recipient to honor, so the absence of a receipt proves nothing about whether somebody read a message.
Return-Path: <>
Content-Type: multipart/report; report-type=delivery-status
Both reports belong in event handling, not a conversational auto-reply queue. Keep the visible From, reply destination, and envelope return path separate when you parse them. I cover those identities in the guide to From, Reply-To, and Return-Path.
What should the classifier do?
Use a mature MIME parser to unfold headers and give your application structured values. Do not run regular expressions over the raw message stream and assume folded headers, encoded parameters, and malformed input will behave.
Here is the dependency-free decision layer I tested. It expects already-parsed values. The order matters because a DSN or MDN is more specific than a general automatic-message signal.
function autoSubmittedToken(value) {
return String(value ?? '')
.split(';', 1)[0]
.trim()
.toLowerCase()
}
function reportType(value) {
const match = String(value ?? '').match(
/(?:^|;)\s*report-type\s*=\s*"?([^";\s]+)"?/i
)
return match?.[1]?.toLowerCase() ?? null
}
function hasNullReturnPath(value) {
if (value === null || value === undefined) return false
const normalized = String(value).trim()
return normalized === '' || normalized === '<>'
}
export function classifyInbound({
autoSubmitted,
contentType,
listId,
returnPath
}) {
const report = reportType(contentType)
if (report === 'delivery-status') {
return {
kind: 'delivery_status_notification',
replyEligible: false,
reason: 'delivery_status_report'
}
}
if (report === 'disposition-notification') {
return {
kind: 'message_disposition_notification',
replyEligible: false,
reason: 'disposition_notification_report'
}
}
const submitted = autoSubmittedToken(autoSubmitted)
if (submitted && submitted !== 'no') {
return {
kind: 'automatic_response',
replyEligible: false,
reason: `auto_submitted_${submitted}`
}
}
if (hasNullReturnPath(returnPath)) {
return {
kind: 'automatic_or_report',
replyEligible: false,
reason: 'null_return_path'
}
}
if (String(listId ?? '').trim()) {
return {
kind: 'list_message',
replyEligible: false,
reason: 'list_id_present'
}
}
return {
kind: 'ordinary_or_unmarked',
replyEligible: true,
reason: 'no_standard_suppression_signal'
}
}
I ran eight fixtures through this function: auto-replied, auto-generated, DSN, MDN, null return path, list mail, Auto-Submitted: no, and a message with none of these signals. All eight passed.
replyEligible: true deliberately means “continue to the next checks.” It does not mean “this is human mail.” Older software may omit the standard header, and a hostile sender can forge one. Email authentication answers a different question about domain authorization and integrity. It does not prove that a person composed the message.
How do you prevent the actual reply loop?
Inbound classification is only half of the design. Your outbound responder also needs a state boundary.
Before sending, require all of these conditions:
- The message passed the suppression classifier.
- The recipient address maps to the responder that is about to act.
- The same responder has not answered the same sender inside its cooldown window.
- The inbound event has not already been processed.
- The reply target is not your own sender or another internal automatic address.
For personal and group responders, RFC 3834 recommends no more than one automatic response to the same sender within several days and suggests seven days as a default. Store that decision durably. An in-memory flag disappears on restart and does not protect two workers processing the same message at once.
The deduplication key should come from the provider event ID or your stored message ID plus responder identity. Claim it atomically before enqueueing the response. That is the same boundary I use for retry-safe inbound events in the email webhook security guide.
Your outgoing automatic response should include Auto-Submitted: auto-replied. RFC 3834 also recommends a null reverse path for many automatic responses and NOTIFY=NEVER when the SMTP transport supports delivery-status notification options. Those choices reduce the chance that a failure report triggers another automated message. They do not replace the inbound checks.
Should you use Precedence or List-ID?
List-ID is a useful suppression signal for a personal responder because list traffic is rarely an appropriate target for an out-of-office reply. RFC 3834 explicitly notes that List-* fields can help decide whether a personal or group responder should act.
Precedence is different. It is not a standard Internet message header with one reliable meaning, and RFC 3834 does not prescribe behavior for it. You can log it as provider-specific evidence, but I would not let it override a standard report type, Auto-Submitted, or the envelope return path.
Where does email verification fit?
Email verification and automatic-message classification happen at different points. Verification can help you assess an address before you send. This classifier decides what to do with a message after it arrives. Neither process proves inbox ownership, human authorship, or guaranteed delivery.
The production sequence is therefore: parse the message, identify DSNs and MDNs, honor standard suppression signals, apply your durable cooldown and deduplication rules, then enqueue at most one response. Keep every suppressed reason in your logs. When a loop almost happens, that reason is what makes the incident explainable.