Verifying MX Record TTL for the list of domains

By Robert Dyjas on . Last edit:  • Edit this post

Microsoft recommends TTL for MX records to be not higher than 21600, ideally 3600. Let's check it with PowerShell.

In this article, we'll learn how to check the TTL of the MX records using PowerShell.

As an exercise, we'll write a script. The role of the script will be to check TTL for all accepted domains in our Exchange Online organization.

Background

Microsoft has posted a message in regard to MX Record TTL Support Policy. Its ID on Message Center is MC346908. It says that recommended TTL value should be 21,600 seconds (6 hours) or less. It also specifies that recommended TTL value is 3,600 seconds (1 hour).

We'll check whether our domains' records are in line with the recommendations.

Prerequisites

The script uses a connection to Exchange Online using PowerShell. There's, however, an alternative way to define the list of domains. It is described below.

Getting the domains

We first need to get the list of all the domains we're going to check. We want to check the accepted domains in our Exchange Online organization:

powershell
$acceptedDomains = Get-AcceptedDomain | Select-Object -ExpandProperty DomainName

What if we want to specify the list ourselves? We can define the array of domains inline. As long as the variable is an array of strings, it should be fine:

powershell
# We can define an array
$acceptedDomains = @(
'contoso.com',
'fabrikam.com'
)

# Or we can convert 
# a multi-line string to array
$acceptedDomains = @"
contoso.com
fabrikam.com
"@ -split "`n"

Getting DNS records

We're going to use Resolve-DnsName cmdlet to query the Google DNS server (8.8.8.8):

powershell
$checkResults = foreach ($acceptedDomain in $acceptedDomains) {
	<#
	$acceptedDomain = $acceptedDomains[0]
	#>
	# Prepare the object to store data
	$resObject = [PSCustomObject]@{
		'DomainName'      = $acceptedDomain
		'Result'          = 'TBD'
		'HasValidTTL'     = $false
		'HasSuggestedTTL' = $false
	}

	try {
		# Get the record and stop in case any errors
		$resolveDnsNameParams = @{
			Name        = $acceptedDomain
			Type        = 'MX'
			Server      = '8.8.8.8'
			ErrorAction = 'Stop'
		}
		# We're only interested in records pointing to
		# Exchange Online Protection
		$mxRecord = @(Resolve-DnsName @resolveDnsNameParams) |
			Where-Object {$_.NameExchange -like '*.mail.protection.outlook.com'}
		
		if (-not $mxRecord) {
			# No matching records, throw error
			throw 'NoExO'
		}
		# Note the success
		$resObject.Result = 'Success'
	} catch {
		$e = $_
		if ($e.Exception.Message -eq 'NoExO') {
			$resObject.Result = 'NoExO'
		} else {
			# Note the failure
			$resObject.Result = 'Failed'
		}
	} finally {
		# Return the object
		$resObject
	}

}

There are some things worth explaining in the script.

Returning data

First of all, we're saving the data returned by the loop to the $checkResults variable. We're going to use it later to display the gathered data.

Debugging tip

At the beginning of the loop I added the following code:

markup
	<#
	$acceptedDomain = $acceptedDomains[0]
	#>

What's the purpose of it, you may ask? I use it to simplify debugging. When I don't want to run the entire loop, I click line 2 from the code above. Then I click F8 to run a single line in PowerShell. Then I can run further lines step by step and do checks in the meantime.

Error handling

The code within the foreach statement is wrapped into the try catch finally block. The purpose of it is to allow error handling. There are two cases when we reach the catch block - if the DNS resolution fails or if we don't find the record pointing to Exchange Online.

To make sure that DNS resolution failure switches to the catch block, we added the -ErrorAction Stop parameter. Otherwise, we'd have a non-terminating error.

We also have a situation, when we throw an error intentionally. This happens when we don't have the MX record pointing to Exchange Online for a domain. By throwing the error, we prevent any checks from being run.

Splatting

For better readability, we use splatting for Resolve-DnsName. In other case, our line to execute the cmdlet would be very long.

Adding checks

Ok, we explained the basics, let's now add checks. That part consists of two if statements. First one checks for supported TTL value and the other for recommended value:

powershell
# Check if TTL is valid
if ($mxRecord.TTL -gt 0 -and $mxRecord.TTL -lt 21600) {
    $resObject.HasValidTTL = $true
}

# Check if TTL value is 3600
if ($mxRecord.TTL -eq 3600) {
    $resObject.HasSuggestedTTL = $true
}

Displaying the results

When we run the script we've written so far, we can process the results. We can display (or export) all the values and check manually.

We can also filter out the domains, which have everything correct. We'd then display (or export) only the one we need to verify:

powershell
# List the results
$checkResults

# List only domains with issues
$checkResults | Where-Object {
	$_.Result -ne 'Success' -or
	-not $_.HasValidTTL
}

Entire script

Below you can find everything we've written. It's compiled together to one script:

powershell
$acceptedDomains = Get-AcceptedDomain | Select-Object -ExpandProperty DomainName
$checkResults = foreach ($acceptedDomain in $acceptedDomains) {
	<#
	$acceptedDomain = $acceptedDomains[0]
	#>
	# Prepare the object to store data
	$resObject = [PSCustomObject]@{
		'DomainName'      = $acceptedDomain
		'Result'          = 'TBD'
		'HasValidTTL'     = $false
		'HasSuggestedTTL' = $false
	}

	try {
		# Get the record and stop in case any errors
		$resolveDnsNameParams = @{
			Name        = $acceptedDomain
			Type        = 'MX'
			Server      = '8.8.8.8'
			ErrorAction = 'Stop'
		}
		# We're only interested in records pointing to
		# Exchange Online Protection
		$mxRecord = @(Resolve-DnsName @resolveDnsNameParams) |
			Where-Object {$_.NameExchange -like '*.mail.protection.outlook.com'}
		
		if (-not $mxRecord) {
			# No matching records, throw error
			throw 'NoExO'
		}
		# Note the success
		$resObject.Result = 'Success'

		# Check if TTL is valid
		if ($mxRecord.TTL -gt 0 -and $mxRecord.TTL -lt 21600) {
			$resObject.HasValidTTL = $true
		}

		# Check if TTL value is 3600
		if ($mxRecord.TTL -eq 3600) {
			$resObject.HasSuggestedTTL = $true
		}
	} catch {
		$e = $_
		if ($e.Exception.Message -eq 'NoExO') {
			$resObject.Result = 'NoExO'
		} else {
			# Note the failure
			$resObject.Result = 'Failed'
		}
	} finally {
		# Return the object
		$resObject
	}

}

# List the results
$checkResults

# List only domains with issues
$checkResults | Where-Object {
	$_.Result -ne 'Success' -or
	-not $_.HasValidTTL
}

Limitations

The script doesn't cover some specific cases. It won't analyze more than one record. In theory, we might have more than one record pointing to Exchange Online.

If for some reason the domain your MX records point to is not under mail.protection.outlook.com you might need to update the filter in Where-Object.

Conclusion

We've written a script to validate whether our domains have their MX records TTL in line with Microsoft recommendations. We can now extend the script by generating the alert if there's an anomaly detected. If we run it periodically, we can forget about checking our MX records TTL - the script will do it for us.