โ 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. Rejecting or filtering mail to or from a major share of consumer addresses has real trade-offs for your users, your correspondents, and โ if you get the syntax wrong โ your entire mail flow.
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 lost or delayed mail, service outages, 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.
A true SMTP bounce means the message wasn't delivered โ you can't send a "bounce" and still deliver the same message, by definition. But several mail servers can do the practical equivalent: accept and deliver the message normally, and separately fire off a one-time courtesy notice back to the sender explaining the Microsoft blocking problem.
A plain callout โ connect, MAIL FROM/RCPT TO, then
abort without sending โ only tells you the recipient's server would probably
accept a message right now. It doesn't prove the actual notice would get all
the way through, since some servers accept the envelope and only reject or
defer once they see the full message at the DATA stage. A plain
"just send a courtesy notice and move on" recipe has the same weakness as a
plain callout: the notice itself is a message to a Microsoft-hosted address,
so it can silently vanish the same way any other message to these domains
can, leaving the sender no better informed than if you'd said nothing. So the
only recipe covered on this page skips probing and plain notice-sending
entirely: actually attempt delivery of the real courtesy notice,
synchronously, while the sender's connection to you is still open, and let
the outcome of that real attempt decide what happens next. If it's accepted,
the sender has genuinely been notified โ go ahead and accept and deliver
their original message too. If it's rejected or fails, a block is in force
right now โ reject the incoming message immediately instead, with a real
bounce, the same as the hard-reject
option. Because the notice delivery attempt is the test, each
tab below is a single, self-contained recipe โ there's no separate probe
step and no basic "just send a notice" version to also run alongside it.
Gate delivery on a live attempt to notify the sender
Postfix's built-in reject_unverified_sender only does an
envelope-level probe, and a plain Sieve/Dovecot vacation-style
auto-reply is no better โ it's just another message to a Microsoft-hosted
address, so it can vanish the same way any other message to these domains
can. To gate on an actual, complete delivery attempt of the notice instead,
hand the decision to a small external policy service (Postfix's standard
mechanism for exactly this โ see SMTPD_POLICY_README), scoped
to Microsoft senders via a restriction class. In main.cf:
smtpd_restriction_classes = microsoft_verify microsoft_verify = check_policy_service inet:127.0.0.1:10040, permit
Point a sender-domain map at the class. As a pcre table (no
postmap compile step needed, and one line covers every
country-specific domain instead of an ever-growing exact list) โ
/etc/postfix/microsoft_senders:
/^(?:(?:hotmail|outlook|passport|windowslive)\.[a-z.]+|live\.(?:com|co\.uk|fr|de|it|com\.au|com\.ar)|msn\.com)$/ microsoft_verify
Reference it in smtpd_sender_restrictions:
smtpd_sender_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
check_sender_access pcre:/etc/postfix/microsoft_senders,
... your existing restrictions ...
postfix reload
And the policy service itself. An earlier draft of this example was missing
its MX lookup, never created its own throttle directory, and couldn't handle
two requests at once โ fixed below (still needs pip install
dnspython, and real testing before you trust it):
#!/usr/bin/env python3 # microsoft-notice-policy.py - Postfix policy service on 127.0.0.1:10040 # Actually attempts delivery of the courtesy notice; the outcome of that # real attempt decides REJECT vs DUNNO (= "no opinion, keep going"). # Requires: pip install dnspython import smtplib, socketserver, time, hashlib, os, logging import dns.resolver logging.basicConfig(filename="/var/log/microsoft-notice-policy.log", level=logging.INFO) THROTTLE_DIR = "/var/lib/postfix/microsoft-notice-throttle" THROTTLE_SECONDS = 7 * 24 * 3600 NOTICE_FROM = "postmaster@yourdomain.example" REJECT_MSG = ("REJECT 550 5.7.1 We no longer attempt delivery to Microsoft-hosted " "addresses due to reliability issues right now. Please ask your " "correspondent for an alternative email address that doesn't rely " "on Microsoft services. See https://dumpmicrosoft.com/users.html") os.makedirs(THROTTLE_DIR, exist_ok=True) # was missing - first run used to crash here def throttle_path(sender): return os.path.join(THROTTLE_DIR, hashlib.sha256(sender.encode()).hexdigest()) def already_notified(sender): p = throttle_path(sender) return os.path.exists(p) and time.time() - os.path.getmtime(p) < THROTTLE_SECONDS def mark_notified(sender): # O_EXCL: two concurrent requests for the same sender can't both "win". try: fd = os.open(throttle_path(sender), os.O_CREAT | os.O_EXCL | os.O_WRONLY) os.close(fd) except FileExistsError: pass def mx_hosts_for(domain): try: answers = dns.resolver.resolve(domain, "MX") return [str(r.exchange).rstrip(".") for r in sorted(answers, key=lambda r: r.preference)] except Exception: return [domain] # last-resort fallback: try the domain itself def try_deliver_notice(sender): domain = sender.split("@")[-1] body = ("From: %s\r\nTo: %s\r\nSubject: A note about mail reliability\r\n\r\n" "This message was delivered normally. However, hotmail.com, " "outlook.com, live.com and msn.com have a documented history " "of blocking legitimate mail without warning.\r\n" "Details: https://dumpmicrosoft.com\r\n" % (NOTICE_FROM, sender)) for mx in mx_hosts_for(domain): # now actually iterates real MX hosts try: with smtplib.SMTP(mx, timeout=15) as smtp: smtp.sendmail(NOTICE_FROM, [sender], body) return True except Exception as e: logging.info("notice to %s via %s failed: %s", sender, mx, e) return False class Handler(socketserver.StreamRequestHandler): def handle(self): attrs = {} for line in self.rfile: line = line.decode(errors="replace").rstrip("\r\n") if not line: break if "=" in line: k, v = line.split("=", 1) attrs[k] = v sender = attrs.get("sender", "") if not sender: self.wfile.write(b"action=DUNNO\n\n") return if already_notified(sender) or try_deliver_notice(sender): mark_notified(sender) logging.info("accepted: %s", sender) self.wfile.write(b"action=DUNNO\n\n") else: logging.info("rejected: %s (notice delivery failed)", sender) self.wfile.write((f"action={REJECT_MSG}\n\n").encode()) # ThreadingMixIn so one slow probe doesn't stall every other check waiting behind it. class ThreadingPolicyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): daemon_threads = True if __name__ == "__main__": ThreadingPolicyServer(("127.0.0.1", 10040), Handler).serve_forever()
Run it under systemd (or similar) so it's always
listening before Postfix needs it โ and check what Postfix does if it isn't:
check_policy_service defers (temporary 4xx) rather than silently
permitting when it can't reach the socket, which is the safe default, but
confirm that in your own logs rather than assuming it. Important:
this daemon sends the real notice itself โ don't also add a separate
vacation-style auto-reply rule for these senders, or a sender
whose mail does get through ends up with two notices for one message.
Even with all of the above fixed, "delivery accepted" still only means the SMTP transaction succeeded, not that the notice reached an inbox โ see the home page's reporting on mail being accepted and then silently dropped. It's the strongest signal available over SMTP, not a guarantee.
Gate delivery on a live attempt to notify the sender
Exim's native verify = sender/callout only tests
MAIL FROM/RCPT TO acceptance, not a full send โ
and Exim's standard unseen router + autoreply
transport idiom for "vacation"-style copies is no better, since that notice
is just another message to a Microsoft-hosted address and can vanish the
same way any other message to these domains can. To gate on an actual
delivery attempt instead, use Exim's run expansion, which can
call out to a script and use its exit code as the condition, in
acl_check_mail:
acl_check_mail:
accept hosts = +relay_from_hosts
accept authenticated = *
deny
senders = ^.*@(?:(?:hotmail|outlook|passport|windowslive)\.[a-z0-9.-]+|live\.(?:com|co\.uk|fr|de|it|com\.au|com\.ar)|msn\.com)$
condition = ${run{/usr/local/bin/notify-and-test.py $sender_address}{no}{yes}}
message = "550 5.7.1 We no longer attempt delivery to Microsoft-hosted \
addresses due to reliability issues right now. Please ask your correspondent \
for an alternative email address that doesn't rely on Microsoft services. \
See https://dumpmicrosoft.com/users.html"
log_message = "Microsoft sender rejected - live notice delivery failed"
accept
And the script โ a sketch, add real MX lookup, logging and error handling before relying on it:
#!/usr/bin/env python3 # notify-and-test.py - called via Exim's ${run}. Actually attempts delivery # of the courtesy notice. Exit 0 = delivered (or already notified recently, # so skip re-sending); exit 1 = delivery failed just now. import sys, smtplib, time, hashlib, os THROTTLE_DIR = "/var/lib/exim4/microsoft-notice-throttle" THROTTLE_SECONDS = 7 * 24 * 3600 NOTICE_FROM = "postmaster@yourdomain.example" sender = sys.argv[1] if len(sys.argv) > 1 else "" if not sender: sys.exit(1) os.makedirs(THROTTLE_DIR, exist_ok=True) stamp = os.path.join(THROTTLE_DIR, hashlib.sha256(sender.encode()).hexdigest()) if os.path.exists(stamp) and time.time() - os.path.getmtime(stamp) < THROTTLE_SECONDS: sys.exit(0) # already notified recently - allow without re-sending domain = sender.split("@")[-1] body = ("From: %s\r\nTo: %s\r\nSubject: A note about mail reliability\r\n\r\n" "This message was delivered normally. However, hotmail.com, outlook.com, " "live.com and msn.com have a documented history of blocking legitimate " "mail without warning.\r\nDetails: https://dumpmicrosoft.com\r\n" % (NOTICE_FROM, sender)) try: # NB: resolve the real MX for `domain` in production. with smtplib.SMTP(domain, timeout=15) as smtp: smtp.sendmail(NOTICE_FROM, [sender], body) open(stamp, "w").close() sys.exit(0) except Exception: sys.exit(1)
Reload with systemctl reload exim4
after adding the ACL block. One catch worth knowing about: modern Exim (4.94+)
treats client-supplied data like $sender_address as "tainted" and
can refuse to pass it into ${run} unvalidated โ if you hit a
tainted-data error, check your version's documentation for the de-taint step
(typically an ${extract} or lookup-based sanitiser) before this
will run as written.
Gate delivery on a live attempt to notify the sender
Sendmail's access db can't do this on its own โ it only supports
accept/reject-style actions. A plain procmail "carbon copy" notice script
is no better as a signal either, since that notice is just another message
to a Microsoft-hosted address and can vanish the same way any other message
to these domains can. A milter hook can do the real thing: it
completes the SMTP conversation to the sender's MX (through
DATA, not just RCPT TO) sending the real notice,
and rejects the original message only if that genuinely fails. Using
MIMEDefang's filter_sender hook (real MX lookup via
Net::DNS, throttle storage, and error handling omitted for
brevity):
use Net::SMTP;
use Digest::SHA qw(sha256_hex);
my $THROTTLE_DIR = '/var/spool/MIMEDefang/microsoft-notice-throttle';
my $THROTTLE_SECS = 7 * 24 * 3600;
sub filter_sender {
my ($sender, $ip, $hostname, $helo) = @_;
return ACCEPT unless lc($sender) =~ /@(?:(?:hotmail|outlook|passport|windowslive)\.[a-z0-9.-]+|live\.(?:com|co\.uk|fr|de|it|com\.au|com\.ar)|msn\.com)>?$/;
my $stamp = "$THROTTLE_DIR/" . sha256_hex($sender);
if (-f $stamp && (time - (stat($stamp))[9]) < $THROTTLE_SECS) {
return ACCEPT; # already notified recently - don't re-send or re-test
}
my ($domain) = $sender =~ /\@(.+)$/;
my $delivered = 0;
for my $mx ( mx_hosts_for($domain) ) { # your own MX lookup helper
my $smtp = Net::SMTP->new($mx, Timeout => 15) or next;
$smtp->mail('postmaster@yourdomain.example');
if ($smtp->to($sender, { SkipBad => 1 })) {
$smtp->data;
$smtp->datasend("From: postmaster\@yourdomain.example\n");
$smtp->datasend("To: $sender\n");
$smtp->datasend("Subject: A note about mail reliability\n\n");
$smtp->datasend("This message was delivered normally. However, hotmail.com, ");
$smtp->datasend("outlook.com, live.com and msn.com have a documented history ");
$smtp->datasend("of blocking legitimate mail without warning.\n");
$smtp->datasend("Details: https://dumpmicrosoft.com\n");
$delivered = $smtp->dataend;
}
$smtp->quit;
last if $delivered;
}
if ($delivered) {
open(my $fh, '>', $stamp) and close $fh;
return ACCEPT;
}
return ('reject', "550 5.7.1 We no longer attempt delivery to " .
"Microsoft-hosted addresses due to reliability issues right " .
"now. Please ask your correspondent for an alternative email " .
"address. See https://dumpmicrosoft.com/users.html");
}
This is a starting point, not drop-in production code โ it needs a real MX lookup, timeout/retry handling, and testing against your actual mail flow before you rely on it.
Gate delivery on a live attempt to notify the sender
cPanel/WHM servers run Exim under the hood. In
WHM โ Service Configuration โ Exim Configuration Manager โ Advanced
Editor, add the same ${run}-based condition as the
Exim tab to the SMTP MAIL ACL section (acl_check_mail):
deny
senders = ^.*@(?:(?:hotmail|outlook|passport|windowslive)\.[a-z0-9.-]+|live\.(?:com|co\.uk|fr|de|it|com\.au|com\.ar)|msn\.com)$
condition = ${run{/usr/local/bin/notify-and-test.py $sender_address}{no}{yes}}
message = "550 5.7.1 We no longer attempt delivery to Microsoft-hosted \
addresses due to reliability issues right now. Please ask your correspondent \
for an alternative email address that doesn't rely on Microsoft services. \
See https://dumpmicrosoft.com/users.html"
Upload notify-and-test.py from the
Exim tab to the server and make it executable, then Restart Exim
after saving. If the script fails to deliver the real notice, the sender gets
an immediate bounce; if it succeeds, the message is accepted and the sender
has genuinely already been notified โ no separate step needed.
Prefer a hard reject instead? See Section A1. Want less effort and less reach? See Section A3 โ a header, nothing more.
Pointing an affected visitor here? Send them straight to the switching guide.
Open the user guide โ