Test SMTP relay to Microsoft 365 with PowerShell
You've set up an M365 relay connector, but will mail actually get through? This script checks the endpoint and sends one test message, helping you see where the process stops.
Before you start
Use Windows PowerShell 5.1 on the Windows machine whose relay path you want to test. You need an existing inbound connector, its exact Microsoft 365 MX endpoint, a sender address in an accepted domain, and a test mailbox you control. Copy the endpoint from the tenant configuration rather than guessing it from the domain name.
For IP-based relay, the machine's outbound public IP must match the connector. Run from the same network path as the application you're diagnosing. Microsoft 365 connector relay uses TCP port 25, with STARTTLS enabled in this script. Port 587 with mailbox authentication is a separate submission method. See Microsoft's SMTP relay setup guide.
Each successful invocation submits one message to the address you supply. It doesn't create or change a connector. SMTP acceptance is one checkpoint; you still need to confirm delivery.
Get the script
Read through the script before running it on your PC.
<#
.SYNOPSIS
Tests an existing Microsoft 365 inbound SMTP relay connector.
.DESCRIPTION
Resolves the endpoint, checks TCP port 25 and sends one test message with
STARTTLS and normal server certificate validation. Uses no mailbox login.
Designed for Windows PowerShell 5.1. Does not configure the connector.
.PARAMETER SmtpServer
The exact Microsoft 365 MX endpoint from your tenant configuration.
.PARAMETER CertificateThumbprint
Optional client certificate in CurrentUser\My or LocalMachine\My.
Its private key must be accessible to the account running this script.
.EXAMPLE
.\test-m365-relay.ps1 -SmtpServer 'contoso-com.mail.protection.outlook.com' -From '[email protected]' -To '[email protected]'
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$SmtpServer,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$From,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$To,
[string]$CertificateThumbprint,
[string]$Subject = "M365 Relay Test - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')",
[string]$Body = "This is an SMTP relay test sent at $(Get-Date)."
)
$smtpClient = $null
$mailMessage = $null
try {
# Check addresses before making any network connection.
$senderAddress = [System.Net.Mail.MailAddress]::new($From)
$recipientAddress = [System.Net.Mail.MailAddress]::new($To)
Write-Host "Resolving $SmtpServer..."
Resolve-DnsName -Name $SmtpServer -DnsOnly -ErrorAction Stop |
Where-Object { $_.IPAddress } |
ForEach-Object { Write-Host " $($_.IPAddress)" }
Write-Host 'Checking TCP port 25...'
$tcpTest = Test-NetConnection -ComputerName $SmtpServer -Port 25 -WarningAction SilentlyContinue -ErrorAction Stop
if (-not $tcpTest.TcpTestSucceeded) {
throw 'TCP port 25 is unreachable. Check the endpoint, firewall, routing and ISP restrictions. Connector authentication is checked during SMTP, after TCP connects.'
}
$smtpClient = [System.Net.Mail.SmtpClient]::new($SmtpServer, 25)
$smtpClient.DeliveryMethod = [System.Net.Mail.SmtpDeliveryMethod]::Network
$smtpClient.UseDefaultCredentials = $false
$smtpClient.Credentials = $null
$smtpClient.EnableSsl = $true
$smtpClient.Timeout = 30000
if ($CertificateThumbprint) {
$thumbprint = ($CertificateThumbprint -replace '\s', '').ToUpperInvariant()
if ($thumbprint -notmatch '^[0-9A-F]{40}$') {
throw 'Enter the complete 40-character certificate thumbprint.'
}
$certificates = @(Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction Stop |
Where-Object { $_.Thumbprint -eq $thumbprint -and $_.HasPrivateKey -and $_.NotBefore -le (Get-Date) -and $_.NotAfter -gt (Get-Date) })
if ($certificates.Count -eq 0) {
throw 'No currently valid matching certificate with a private key was found in CurrentUser\My or LocalMachine\My.'
}
$null = $smtpClient.ClientCertificates.Add($certificates[0])
Write-Host "Using client certificate $thumbprint."
}
$mailMessage = [System.Net.Mail.MailMessage]::new()
$mailMessage.From = $senderAddress
$mailMessage.To.Add($recipientAddress)
$mailMessage.Subject = $Subject
$mailMessage.Body = $Body
Write-Host "Sending one test message from $From to $To with STARTTLS..."
$smtpClient.Send($mailMessage)
Write-Host 'SMTP submission accepted. Check the recipient mailbox and Exchange Online message trace to confirm delivery.' -ForegroundColor Green
} catch {
throw "Relay test failed: $($_.Exception.Message)"
} finally {
if ($mailMessage) { $mailMessage.Dispose() }
if ($smtpClient) { $smtpClient.Dispose() }
}
Write-Host 'For tracing, connect separately to Exchange Online with an authorised account and use Get-MessageTraceV2.'
Run an IP-based relay test
Download the file, or copy the code into Notepad and save it as test-m365-relay.ps1 with All files selected. Open Windows PowerShell in that folder.
Replace every example value with your endpoint and addresses:
$relayTest = @{
SmtpServer = 'contoso-com.mail.protection.outlook.com'
}
.\test-m365-relay.ps1 @relayTest
No mailbox password is requested. This tests connector relay, not authenticated SMTP submission. If execution policy prevents running the file, use your organisation's approved script process.
Use a certificate-based connector
Supply the full certificate thumbprint as well:
$relayTest.CertificateThumbprint = 'REPLACE_WITH_YOUR_40_CHARACTER_THUMBPRINT'
.\test-m365-relay.ps1 @relayTest
The script searches CurrentUser\My and LocalMachine\My for a currently valid certificate with a private key. The running account also needs access to that private key. Its Subject or SAN and connector configuration must meet the tenant's requirements; a local certificate match alone doesn't establish connector acceptance. See Microsoft's certificate-based connector guidance.
Understand the result
| Stage | What to check if it fails |
|---|---|
| DNS | Confirm the exact endpoint and resolver access. |
| TCP port 25 | Check routing, firewall and ISP restrictions. A failed TCP connection doesn't establish a connector allow-list problem. |
| TLS or certificate | Check server trust, endpoint spelling, TLS support and client certificate/private-key access. |
| SMTP submission | Capture the server error and check the connector identity, public IP, sender domain and mail-flow restrictions. |
| Accepted, but no message | Inspect message trace, filtering and the recipient mailbox. |
Keep the full error and the test time. Those details make the next check much easier.
Confirm delivery
Check the recipient inbox and junk folder. In the Exchange admin center, use message trace to find the sender, recipient and test time.
If you already have an authorised Exchange Online PowerShell session, you can query separately with:
-StartDate (Get-Date).AddMinutes(-30) -EndDate (Get-Date)
Use your real addresses and allow for trace processing time. This query isn't run by the script. See Microsoft's Get-MessageTraceV2 reference.
Delivering to an internal mailbox alone doesn't prove external relay authorisation. If external relay is your requirement, perform a separate test to an external mailbox you control and confirm that delivery too.
About this version
This is an updated version of the supplied relay test script. It keeps server certificate validation enabled, requires STARTTLS on port 25, disposes SMTP resources, checks client certificate validity, and points to Get-MessageTraceV2. It doesn't include the original certificate-validation bypass or plaintext switch.
It uses .NET SmtpClient for this Windows PowerShell diagnostic. Microsoft doesn't recommend SmtpClient for new application development; this script isn't an OAuth mail client or a production mail-sending service. It has not been tested against a live tenant connector.