โ Read this before you change anything
Everything on this page is an illustrative example, not a tested, certified, or supported configuration for your mail server, OS, version, or environment. This technique rewrites live mail content on its way to every mailbox on the server โ a matching mistake doesn't affect one test account, it affects everyone at once.
You are solely responsible for reviewing, testing, and validating any change before it touches a production system, for confirming it's compatible with your existing configuration and policies, and for any legal, contractual, or regulatory obligations that apply to your organisation's handling of email.
By copying, adapting, or deploying any snippet, script, or configuration from this page, you accept full responsibility for the consequences โ including but not limited to misidentified messages, corrupted or lost mail content, and downstream impact on your users. DumpMicrosoft and its authors accept none. If you are not able or willing to take that responsibility, do not apply any of the changes described in this guide.
This is a different direction from the rest of Section A. Those pages cover mail arriving from a Microsoft-hosted sender. This one covers what happens after you (or one of your users) sends mail to a Microsoft-hosted address and it bounces: a delivery-failure notice โ formally a Delivery Status Notification, or DSN โ lands back as an ordinary email in whichever mailbox on your server sent the original message. Because it's just another inbound message, you can detect it and change what your users see when they open it. Worth getting straight before any of that, though: who actually wrote it. Usually, it isn't Microsoft.
RCPT TO or
DATA with a 4xx/5xx response โ they never accept it in the
first place. (This is the same behaviour Section
A2's live notice-test depends on: it can only tell you whether a notice
would get through by attempting real delivery while the sender's connection
is still open, which only works because the accept/reject decision happens
synchronously, in that same conversation.) When that's what happens,
your own mail server โ Postfix, Exim, Sendmail, whatever
you run โ is the one that builds the DSN and delivers it to your user, not
Microsoft. Microsoft's contribution is just the terse SMTP response text and
status code from that one rejected conversation; your MTA quotes it inside a
Diagnostic-Code field, and usually inside its own boilerplate
human-readable wording too. What your user sees is mostly your own
server's default bounce template, with a fragment of Microsoft's text
embedded in it โ not something Microsoft composed and sent. Everything
below targets that case. Microsoft's own infrastructure can generate and
send a DSN itself instead โ if it accepted the message and only failed to
deliver it afterwards, a full mailbox say โ but that's rare enough next to
the synchronous-rejection case that this guide doesn't build separate
matching for it; those few bounces will just pass through unrewritten.
How to recognise a Microsoft-triggered bounce
A DSN is a structured message (RFC 3464),
not free text โ that's what makes reliable detection possible. It's a
multipart/report; report-type=delivery-status message built from,
in order: a human-readable text/plain part (this is the wording
a person actually sees when they open it), a machine-readable
message/delivery-status part (structured fields like
Action, Status, Remote-MTA and
Diagnostic-Code), and usually a message/rfc822-headers
or full message/rfc822 part carrying the original message. Since
the DSN itself is usually your own server's, not Microsoft's, don't bother
looking for Microsoft in its headers โ Microsoft's fingerprint is inside that
structured part instead. Signals worth combining rather than trusting
individually:
- A
Remote-MTAfield, or a quoted "host ... said:" line inside the human-readable text, ending in.protection.outlook.com,.outlook.comor.hotmail.comโ the host your own server tried (and failed) to hand the message to. - A
Diagnostic-Code/Statususing enhanced codes and phrasing Microsoft commonly emits โ e.g.5.7.606,5.1.10,4.4.7, or text fragments like "Access denied, banned sender" or "Recipient address rejected". This is Microsoft's own text, quoted verbatim by your server from the rejection it received. - A
Subjectbeginning "Undeliverable:", "Delivery has failed to these recipients or groups", or "Message not delivered" โ though your own server may use its own default subject instead (Postfix's is "Undelivered Mail Returned to Sender"), so treat this as the weakest signal of the set.
Remote-MTA/quoted-host
match and a matching Diagnostic-Code) before rewriting
anything, and log near-misses somewhere you'll actually look at them, so you
can tune the pattern instead of silently missing โ or silently mangling โ
the wrong messages.
Where to intercept it: the human-readable part, not the whole message
Because RFC 3464 puts the human-readable explanation in its own
text/plain part, that's exactly what every mail client already
renders first when someone opens a DSN โ it's why a fragment of Microsoft's
wording, quoted by your own server, is what your users see today. The
reliable fix is narrow: replace
that part's content with your own explanation and a link to
dumpmicrosoft.com/users.html, and leave the
machine-readable message/delivery-status part and the original
message underneath untouched, in case you or a support agent need to dig
into the real diagnostic detail later. Don't try to rebuild the whole MIME
structure from scratch or rely on a custom header the way the
header-only page does for inbound mail โ
most clients don't surface custom headers, but every client already shows
this specific part, so target it directly instead.
A milter that rewrites the human-readable part in place
Postfix speaks the Milter protocol natively, and a milter is the right
tool here because it sits in front of the queue โ it sees and can modify
every message before any mailbox on the server receives it,
regardless of which local user the DSN is addressed to. Register it in
main.cf alongside any milters you already run (order
matters only if you have several โ keep content-modifying milters
after any that just accept/reject):
smtpd_milters = inet:127.0.0.1:9900 non_smtpd_milters = inet:127.0.0.1:9900 milter_default_action = accept
And the milter itself, using Python's pymilter
(pip install pymilter) โ a sketch showing the shape of the
match-then-rewrite logic; add real logging, the near-miss tracking
mentioned above, and testing against your own mail flow before trusting
it on live mailboxes:
#!/usr/bin/env python3 # microsoft-bounce-rewriter.py - Postfix milter on 127.0.0.1:9900 # Rewrites the human-readable part of a Microsoft-triggered DSN - whether # Postfix's own bounce daemon, reacting to Microsoft rejecting the original # message outright mid-SMTP-conversation - the common case this targets. # (Microsoft generating and sending the DSN itself is rare enough by # comparison that this doesn't build separate matching for it - those few # bounces just pass through unrewritten.) # Requires: pip install pymilter import re, email, Milter from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText MICROSOFT_HOST = re.compile(r"\b[\w.-]*\.(protection\.outlook|outlook|hotmail)\.com\b", re.I) MICROSOFT_DIAG = re.compile(r"\b(5\.7\.606|5\.1\.10|4\.4\.7)\b|access denied, banned sender|recipient address rejected", re.I) DSN_SUBJECT = re.compile(r"^(undeliverable:|message not delivered|delivery has failed|undelivered mail returned to sender)", re.I) REPLACEMENT_TEXT = ( "This is a delivery failure notice for a message you sent to a " "Microsoft-hosted address (hotmail.com, outlook.com, live.com or " "msn.com). Microsoft's mail servers block legitimate senders " "unpredictably, and their original wording below is often unhelpful. " "Details: https://dumpmicrosoft.com\n" "A more reliable option for the recipient: https://dumpmicrosoft.com/users.html\n" "\n" "-- Original notice follows --\n") class BounceRewriter(Milter.Base): def __init__(self): self.fp = None def envfrom(self, mailfrom, *str): # DSNs use a null return-path per RFC 3464 - a strong first signal. self.is_dsn_envelope = (mailfrom == "<>") self.fp = None return Milter.CONTINUE def header(self, name, value): if self.fp is None: self.fp = [] self.fp.append((name, value)) return Milter.CONTINUE def eom(self): headers = dict((k.lower(), v) for k, v in (self.fp or [])) subject = headers.get("subject", "") body = self.getbody() # see pymilter docs for streaming the body in a real filter msg = email.message_from_bytes(body) # Microsoft's fingerprint isn't in the headers above - this DSN is # Postfix's own, so its headers are Postfix's, not Microsoft's. # Microsoft's Remote-MTA and Diagnostic-Code live in the # delivery-status part instead. delivery_status = "" for part in msg.walk(): if part.get_content_type() == "message/delivery-status": delivery_status = part.get_payload(decode=True).decode(errors="replace") break signals = [ self.is_dsn_envelope, headers.get("content-type", "").lower().startswith("multipart/report"), bool(MICROSOFT_HOST.search(delivery_status)), bool(MICROSOFT_DIAG.search(delivery_status) or DSN_SUBJECT.search(subject)), ] if sum(bool(s) for s in signals) < 3: return Milter.ACCEPT # not confident enough - leave the message alone replaced = False for part in msg.walk(): if part.get_content_type() == "text/plain" and not replaced: original = part.get_payload(decode=True).decode(errors="replace") part.set_payload(REPLACEMENT_TEXT + original) replaced = True # only the FIRST text/plain part - that's the human-readable one if replaced: self.replacebody(msg.as_bytes()) return Milter.ACCEPT Milter.factory = BounceRewriter Milter.runmilter("microsoft-bounce-rewriter", "inet:9900@127.0.0.1")
Run it under systemd so it's always
listening before Postfix needs it, and set
milter_default_action = accept (as above) so a milter
outage fails open rather than blocking all mail server-wide โ this
filter should only ever add clarity, never become a new reason mail
doesn't get delivered. replacebody() replaces the whole
body Postfix queues for delivery, which is why matching conservatively
(the signal count above) matters so much more here than on the
accept/reject pages elsewhere in this guide.
A system filter piping matching mail through a rewrite script
Exim doesn't implement the Milter protocol, so the equivalent
mechanism is a system filter โ a filter file that runs
against every message Exim receives, before any per-user filter, which
is what makes it apply to all mailboxes on the server. Enable it in
exim.conf:
system_filter = /etc/exim4/microsoft-bounce.filter system_filter_user = mail
The filter file uses Exim's own filtering language to cheaply test
for any bounce before handing it to an external script โ it
can't usefully pre-filter on From or a Microsoft-specific
Subject the way you might expect, because the common case
is Exim generating this DSN itself (Microsoft rejected the original
message outright, mid-conversation), so those headers are Exim's own,
not Microsoft's. That deeper, Microsoft-specific match happens in the
script instead, against the delivery-status part:
# /etc/exim4/microsoft-bounce.filter
if error_message
then
pipe "/usr/local/bin/rewrite-microsoft-bounce.py"
seen finish
endif
And the script, which does the real MIME-level matching and either re-injects a rewritten copy or leaves the original to be delivered normally โ a sketch, add the same near-miss logging and testing called out above before relying on it:
#!/usr/bin/env python3 # rewrite-microsoft-bounce.py - reads the message from stdin (Exim's `pipe`). # Targets DSNs Exim generated itself, reacting to Microsoft rejecting the # original message outright mid-SMTP-conversation - the common case, where # Microsoft's fingerprint lives in the delivery-status part, not the # headers. (Microsoft generating and sending the DSN itself is rare enough # by comparison that this doesn't match for it separately - those few # bounces just get re-injected unchanged below, same as anything else that # doesn't match.) # Re-injects a rewritten copy via sendmail -t if it matches; otherwise # re-injects the message completely unchanged. Either way, the system # filter's `seen finish` stops Exim delivering the original a second time. import sys, re, subprocess, email MICROSOFT_HOST = re.compile(r"\b[\w.-]*\.(protection\.outlook|outlook|hotmail)\.com\b", re.I) REPLACEMENT_TEXT = ( "This is a delivery failure notice for a message you sent to a " "Microsoft-hosted address. Microsoft's mail servers block legitimate " "senders unpredictably, and their original wording below is often " "unhelpful. Details: https://dumpmicrosoft.com\n" "A more reliable option for the recipient: https://dumpmicrosoft.com/users.html\n" "\n-- Original notice follows --\n") raw = sys.stdin.buffer.read() msg = email.message_from_bytes(raw) content_type = msg.get_content_type() # Microsoft's fingerprint isn't in the headers above - this DSN is # Exim's own, so its headers are Exim's, not Microsoft's. Look in the # delivery-status part instead. delivery_status = "" for part in msg.walk(): if part.get_content_type() == "message/delivery-status": delivery_status = part.get_payload(decode=True).decode(errors="replace") break is_microsoft = bool(MICROSOFT_HOST.search(delivery_status)) if content_type == "multipart/report" and is_microsoft: for part in msg.walk(): if part.get_content_type() == "text/plain": original = part.get_payload(decode=True).decode(errors="replace") part.set_payload(REPLACEMENT_TEXT + original) break # only the first text/plain part - the human-readable one subprocess.run(["/usr/sbin/sendmail", "-t", "-oi"], input=msg.as_bytes())
Test the filter syntax with
exim -bf /etc/exim4/microsoft-bounce.filter before
reloading. The seen finish plus unconditional re-injection
in the script (matched or not) is what avoids either dropping mail the
pattern missed or delivering two copies of what it caught โ get that
pairing wrong in either direction and you'll lose messages or duplicate
them, so test with real captured DSNs before this runs against live
mailboxes. Because error_message is true for every bounce
Exim delivers, not just Microsoft-triggered ones, the script will see
(and correctly ignore) plenty of unrelated DSNs โ that's expected, not
a sign the filter needs tightening further.
A MIMEDefang milter with direct MIME::Entity access
As on the other Sendmail tabs in this guide, a milter is the natural
fit โ and MIMEDefang in particular gives you the message as a Perl
MIME::Entity object, which is exactly what's needed to
reach into a specific part and replace its content. In your
filter.pl:
use MIME::Entity;
sub filter_end {
my ($entity) = @_;
return unless $entity->head->mime_type eq 'multipart/report';
my $subject = $entity->head->get('Subject') || '';
# Microsoft's fingerprint isn't in the headers - if Microsoft rejected
# the original message outright, mid-SMTP-conversation (the common
# case this targets), THIS bounce was generated by your own Sendmail/
# MIMEDefang, not by Microsoft, so its headers are yours. Look inside
# the message/delivery-status part instead, where the Remote-MTA/
# Diagnostic-Code fields name Microsoft's server and quote its
# rejection text.
my $delivery_status = '';
for my $part ($entity->parts) {
if ($part->mime_type eq 'message/delivery-status') {
$delivery_status = $part->bodyhandle ? join('', @{ $part->bodyhandle->as_lines }) : '';
last;
}
}
my $is_microsoft = $delivery_status =~ /\.(protection\.outlook|outlook|hotmail)\.com/i;
my $looks_like_dsn = $subject =~ /^(undeliverable:|message not delivered|delivery has failed|undelivered mail returned to sender)/i;
return unless $is_microsoft && $looks_like_dsn;
for my $part ($entity->parts) {
next unless $part->mime_type eq 'text/plain';
my $original = join('', @{ $part->bodyhandle->as_lines });
my $io = $part->bodyhandle->open('w');
$io->print(
"This is a delivery failure notice for a message you sent to a " .
"Microsoft-hosted address. Microsoft's mail servers block " .
"legitimate senders unpredictably, and their original wording " .
"below is often unhelpful. Details: https://dumpmicrosoft.com\n" .
"A more reliable option: https://dumpmicrosoft.com/users.html\n\n" .
"-- Original notice follows --\n" .
$original);
$io->close;
action_change_body($entity);
last; # only the first text/plain part - the human-readable one
}
}
Adjust to match your existing MIMEDefang filter structure and version โ as with the milter examples on the deliver-and-notify page, this is illustrative of the hook and matching logic, not drop-in production code. Test against real captured DSNs, not assumptions about their format, before it runs against live mailboxes.
Same Exim system filter, added via WHM
cPanel/WHM servers run Exim under the hood, so the system filter from the Exim tab applies the same way. Upload the filter file and the rewrite script via File Manager or SSH, then in WHM โ Service Configuration โ Exim Configuration Manager โ Advanced Editor, add:
system_filter = /etc/exim4/microsoft-bounce.filter system_filter_user = mail
Make rewrite-microsoft-bounce.py
executable and confirm the path referenced by the filter's pipe
command matches where you uploaded it, then Restart Exim.
Because a system filter runs ahead of every account's own mail filters,
this applies across all mailboxes on the server without touching each
account individually.
This complements, rather than replaces, Section A2: that page gates your own outbound delivery on a live notice-test at send time, but only for mail that passes through the specific path you've configured it on. This page catches the bounce afterwards, for whatever actually made it back to a mailbox you run โ including mail sent through other paths on the same server.
Pointing an affected visitor here? Send them straight to the switching guide.
Open the user guide โ