After migrating to a new email solution that routes messages into Microsoft 365, we discovered an unexpected side effect: messages from blocked senders and domains were no longer being filtered by Microsoft Defender for Office 365.
Why? Because our new mail gateway was explicitly trusted by Defender. This trust bypassed many of the built-in anti-spam mechanisms, including policies that normally block specific senders or domains.
As a result, messages from known bad actors — senders and domains we had already identified — were now being delivered to user inboxes.
Move Blocking Logic to Transport Rules
To enforce our existing blocks after the new platform hands off the message to Exchange Online, we needed to move the logic into Exchange transport rules.
Transport (mail flow) rules operate after the message has been accepted by Microsoft 365 and are not impacted by trust relationships like those in the anti-spam engine.
Challenge: ~2000 Senders and Domains
Our block lists had grown over time and contained roughly 2,000 entries — a mix of:
- Specific sender addresses (e.g. mailcious.user@blackhole.local)
- Entire domains (e.g., blackhole.local)
However, Exchange Online transport rules have size limits. A single rule can only hold a limited number of conditions, so we had to break the block list into smaller, manageable chunks.
Transport rule “states”
I need to point out a peculiar difference of the transport rule state, depending on where it’s created:
- Exchange Admin Center (EAC): Transport (mail flow) rules are created in a disabled state
- Powershell (Exchange Online or on-prem): Transport rules are enabled by default when created using New-TransportRule
This means as we are scripting transport rules I would highly recommend you create all your transport rules in the disabled state so when they’re all created, you can review them before you enable them:
-Enabled:$false
Step 1: Split the Sender/Domain List into Manageable Chunks
This requires a script to split the large list into multiple smaller files:
$inputFile = "C:\BlockList\domains.txt"
$outputFolder = "C:\BlockList\Chunks"
$splitCount = 4
$lines = Get-Content $inputFile | Where-Object { $_.Trim() -ne "" }
$perFile = [Math]::Ceiling($lines.Count / $splitCount)
for ($i = 0; $i -lt $splitCount; $i++) {
$start = $i * $perFile
$end = [Math]::Min($start + $perFile - 1, $lines.Count - 1)
$chunk = $lines[$start..$end]
$chunk | Set-Content "$outputFolder\blocklist_part$($i + 1).txt"
}
Step 2 : Cleanup the Text File
This will remove characters from the file that could cause parsing errors
# Path to the domain list file
$domainListPath = "senders_part1.txt"
# Clean and prepare the domain list
$domains = Get-Content $domainListPath | Where-Object { $_.Trim() -ne "" } | ForEach-Object { $_.Trim() }
$chunk = $lines[$start..$end]
Step 3: Create Transport Rules to Enforce Blocking
Depending on your organization's policy, you can either:
- Silently delete the messages (they disappear with no bounce or quarantine)
- Quarantine the messages (users or admins can review and release them)
Each option uses a separate action in the transport rule.
Option A: Delete Emails Silently
$senders = Get-Content "C:\BlockList\blocklist_part1.txt"
New-TransportRule -Name "Block Senders - Part 1" `
-From $senders `
-DeleteMessage $true
-Enabled:$false
Or for domains:
$domains = Get-Content "C:\BlockList\blocklist_domains1.txt"
New-TransportRule -Name "Block Domains - Part 1" `
-SenderDomainIs $domains `
-DeleteMessage $true
-Enabled:$false
Option B: Send Messages to Hosted Quarantine
$senders = Get-Content "C:\BlockList\blocklist_part1.txt"
New-TransportRule -Name "Quarantine Senders - Part 1" `
-From $senders `
-Quarantine $True
-Enabled:$false
For domains:
$domains = Get-Content "C:\BlockList\blocklist_domains1.txt"
New-TransportRule -Name "Quarantine Domains - Part 1" `
-SenderDomainIs $domains `
-Quarantine $True
-Enabled:$false
By using transport rules at the mail flow level, these blocks take effect regardless of trust status between your perimeter gateway and Microsoft 365. It gives you deterministic, policy-based control over unwanted messages — even when Defender for Office 365 doesn’t intervene.
Script : Command.ps1
If you need this as an example to create the transport rule with the error checking then you can do something like this:
Path to the domain list file
$domainListPath = "senders_part1.txt"
# Clean and prepare the domain list
$domains = Get-Content $domainListPath | Where-Object { $_.Trim() -ne "" } | ForEach-Object { $_.Trim() }
# Transport rule name
$ruleName = "Block Spam Senders #1"
# Check if rule already exists
$existingRule = Get-TransportRule -Identity $ruleName -ErrorAction SilentlyContinue
if ($existingRule) {
# Update existing rule
Set-TransportRule -Identity $ruleName -SenderDomainIs $senderDomains -Quarantine $True
Write-Host "Transport rule '$ruleName' updated."
} else {
# Create new rule
New-TransportRule -Name $ruleName -From $Domains -Quarantine $True -Enabled:$false
Write-Host "Transport rule '$ruleName' created."
}