For mail server administrators ยท Section A2

Deliver the mail, but also notify the sender

An in-between option for administrators who aren't ready for a hard reject: gate delivery on a live, synchronous attempt to notify the sender of the reliability problem โ€” if that notice genuinely can't get through right now, reject the incoming message instead.

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.

The flaw with just firing off a notice: that notice is itself an outbound email to a Microsoft-hosted address โ€” exactly the kind of message this whole site is about. It can get blocked by the same unpredictable filtering as anything else sent to hotmail.com/outlook.com/live.com/msn.com, in which case it disappears the same way a normal reply would, and the sender is no better informed than if you'd said nothing.

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.

Worth knowing before you enable this: it adds a real network round-trip โ€” now including a full message transfer, not just an envelope check โ€” to the SMTP transaction (typically a few seconds), so throttling matters more than ever: the examples below still track "already notified this sender recently" and skip straight to accept/reject without re-attempting delivery on every single message. A minority of receiving servers also treat frequent, short-lived connections from the same source with suspicion in their own right โ€” test on a low-traffic domain first.
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 โ†’