Identifying Unresolved LegacyExchangeDNs via EWS and Powershell
I recently worked with a customer who had inadvertently deleted all their user accounts (and thus their Exchange mailboxes), and with no backup available, they had to recreate them. Talk about a nightmare! After they did so, they were able to get their email back, but they discovered that replying to email messages from before the problem resulted in a non-delivery report.
This is because of a fairly well-documented behavior. Email addresses in the From, To, and other fields are resolved to legacyExchangeDNs and stored in the message. When you reply to a message, we expect to be able to resolve that legacyExchangeDN. If we can’t, it causes an NDR. In various migration scenarios where the legacyExchangeDN of a user changes, we populate the user’s proxyAddresses with an X500 address that contains the old legacyExchangeDN. This allows the old value to resolve, preserving the ability to reply to old messages.
In this case, when the users were recreated, they got new legacyExchangeDNs, which broke the ability to reply to old email. We needed to somehow get the old leg DNs back, but we didn’t know what they all were, and having to manually poke around in mailboxes looking for them was not realistic.
To solve this problem, I wrote a script to scan a mailbox and output any unresolved legacyExchangeDNs. There are a few interesting parts to this script, so I figured I would post it in case others find it useful (and so I can easily refer back to it in the future).
The first interesting part is the way it gets the user’s password. I needed to get the user’s password without displaying it to the console. Unfortunately, EWS won’t seem to accept a SecureString, so I couldn’t use Get-Credential or similar approaches to ask the user for his password. To solve this, I had to adapt a routine I found in a couple of other blog posts so it would work in Powershell. You’ll find that near the top of the script. DJ found a much simpler way to accomplish this, so the 15ish lines of code that I had have been replaced with a single line that gets the password without showing it. Thanks DJ!
The second interesting part is that it demonstrates how to open another user’s mailbox folders. The script will ask you for one email address for authentication purposes, and then it will ask you for another user’s SMTP address, which is optional. If you enter the other user, the script will scan that user’s folders instead of the authenticating user’s folders.
The third interesting part is that EWS will not return the full recipient information as part of a FindItems() call – you only get back some basic information, which wasn’t enough to tell me if there was an unresolved legacyExchangeDN. To solve this, I had to actually bind to each individual email message. I did this using a specific property set of only a few properties that I was interested in, but it still made the script quite slow.
I wanted this script to work against both on-premises and Office 365 or BPOS environments. Unfortunately, Autodiscover to a cloud environment results in a redirect that can’t be handled in Powershell (or if it can, I haven’t figured out how). You have to create a callback to validate the redirection URL, and while I found that I could cast a script block to a delegate, I couldn’t get the script block to handle this properly. So if you’re connecting to the cloud, you have to manually specify your EWS URL. DJ also showed me how to make Autodiscover to the cloud work in Powershell, and I’ve updated the script below. Thanks again DJ! This script is really much better now thanks to him.
The script will scan the Inbox and any subfolders, as well as Sent Items and Deleted Items, looking for unresolved leg DNs. Any it finds will be written out to a CSV file. It caches what it finds so that duplicates are not written to the CSV.
Anyway, here’s the script!
# Scan-MailboxForLegDNs.ps1
Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"
$hostName = Read-Host "Hostname for EWS endpoint (leave blank to attempt Autodiscover)"
$outputFile = Read-Host "Output file name"
$emailAddress = Read-Host "Email address for authentication"
$password = $host.ui.PromptForCredential("Credentials", "Please enter your password to authenticate to EWS.", $emailAddress, "").GetNetworkCredential().Password
# If specified, we'll try to open this mailbox instead of the one that authenticated
$otherMailboxSmtp = Read-Host "SMTP address of other mailbox (optional)"
# Initialize the output file
Set-Content -Path $outputFile -Value "Display Name,LegacyExchangeDN"
# Make variables for the properties to make them easier to type,
# then stick them into a PropertySet
$toRecipientsProperty = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ToRecipients
$ccRecipientsProperty = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::CcRecipients
$fromProperty = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::From
$subjectProperty = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::Subject
$arrayOfPropertiesToLoad = @($toRecipientsProperty, $ccRecipientsProperty, $fromProperty, $subjectProperty)
$propertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet($arrayOfPropertiesToLoad)
# Here's where we'll store the ones we found to avoid duplicates in the CSV
$legDNsFound = new-object 'System.Collections.Generic.List[string]'
# This function checks an individual EmailAddress to see if it's an unresolved legacyExchangeDN
function CheckAddress($emailAddress)
{
if ($emailAddress.RoutingType -eq "EX")
{
(" Legacy DN: " + $emailAddress.Address)
(" Display Name: " + $emailAddress.Name)
if (!($legDNsFound.Contains($emailAddress.Address.ToLower())))
{
$legDNsFound.Add($emailAddress.Address.ToLower())
Add-Content -Path $outputFile -Value ($emailAddress.Name.Replace(",", ".") + "," + $emailAddress.Address)
}
}
}
# This function loops through the items in a folder
function ProcessFolder($folder)
{
("Scanning folder: " + $folder.DisplayName)
$itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView(100)
while (($folderItems = $folder.FindItems($itemView)).Items.Count -gt 0)
{
foreach ($item in $folderItems)
{
if ($item.GetType() -eq [Microsoft.Exchange.WebServices.Data.EmailMessage])
{
$message = [Microsoft.Exchange.WebServices.Data.EmailMessage]::Bind($exchService, $item.Id, $propertySet)
(" " + $message.Subject)
foreach ($emailAddress in $message.ToRecipients)
{
CheckAddress($emailAddress)
}
foreach ($emailAddress in $message.CcRecipients)
{
CheckAddress($emailAddress)
}
CheckAddress($message.From)
}
}
$offset += $folderItems.Items.Count
$itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView(100, $offset)
}
}
# This function recursively processes subfolders
function DoSubfoldersRecursive($folder)
{
if ($folder.ChildFolderCount -gt 0)
{
$folderView = new-object Microsoft.Exchange.WebServices.Data.FolderView($folder.ChildFolderCount)
$subfolders = $folder.FindFolders($folderView)
foreach ($subfolder in $subfolders)
{
ProcessFolder($subfolder)
DoSubfoldersRecursive($subfolder)
}
}
}
# Here's where we try to connect
# If a URL was specified we'll use that; otherwise we'll use Autodiscover
$exchService = new-object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)
$exchService.Credentials = new-object System.Net.NetworkCredential($emailAddress, $password, "")
if ($hostName -ne "")
{
("Using EWS URL:" + "https://" + $hostName + "/EWS/Exchange.asmx")
$exchService.Url = new-object System.Uri(("https://" + $hostName + "/EWS/Exchange.asmx"))
}
elseif ($otherMailboxSmtp -ne "")
{
("Autodiscovering " + $otherMailboxSmtp + "...")
$exchServce.AutoDiscoverUrl($otherMailboxSmtp, {$true})
}
else
{
("Autodiscovering " + $emailAddress + "...")
$exchService.AutodiscoverUrl($emailAddress, {$true})
}
if ($exchService.Url -eq $null)
{
return
}
$mailbox = new-object Microsoft.Exchange.WebServices.Data.Mailbox($emailAddress)
# If some other mailbox was specified, open that one instead.
if ($otherMailboxSmtp -ne "")
{
$mailbox = new-object Microsoft.Exchange.WebServices.Data.Mailbox($otherMailboxSmtp)
}
# Create some variables for the folder names so they're easier to type
$inboxFolder = [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox
$sentItemsFolder = [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::SentItems
$deletedItemsFolder = [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::DeletedItems
# We'll bind to each folder by instantiating a FolderId that points to the mailbox we want.
# First, scan the inbox
$inboxId = new-object Microsoft.Exchange.WebServices.Data.FolderId($inboxFolder, $mailbox)
$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService, $inboxId)
ProcessFolder($inbox)
# Now any subfolders
DoSubfoldersRecursive($inbox)
# Now Sent Items
$sentItemsId = new-object Microsoft.Exchange.WebServices.Data.FolderId($sentItemsFolder, $mailbox)
$sentItems = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService, $sentItemsId)
ProcessFolder($sentItems)
# Now Deleted Items
$deletedItemsId = new-object Microsoft.Exchange.WebServices.Data.FolderId($deletedItemsFolder, $mailbox)
$deletedItems = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService, $deletedItemsId)
ProcessFolder($deletedItems)
"Done!"
Comments
Anonymous
January 01, 2003
Hey, that actually works! Thanks DJ! I've updated the script.Anonymous
January 01, 2003
The comment has been removedAnonymous
January 01, 2003
I found another helper for your script. For the credential issue, you would just have to do the following: $Creds = Get-Credential $service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList $Creds.UserName, $Creds.GetNetworkCredential().PasswordAnonymous
September 16, 2011
Have you tried replacing: $exchService.AutoDiscoverUrl($emailAddress) with: $exchService.AutodiscoverUrl($emailAddress, {$true}) to handle the Autodiscover redirect?Anonymous
November 27, 2013
Is there a script to verify the current legacyExchangeDN and verify that is in fact the correct legacyExchangeDN?Anonymous
November 27, 2013
Jeff, not that I'm aware of.Anonymous
November 06, 2014
Hi Bill first I want to say you're a Genius and Thank you! also Thank you DJ for you contributions.
This is script is amazing but I was wondering and please pardon my super noobish Powershell scripting skills but how would I go about modifying it to add multiple other smtp users to it? We have Multi-tenent environment and we do a lot of company migrations between forests and I was wondering how would I go about modifying this to be able to add more smtp users and outputting it to either a single file with username and DN's below or even multiple files either one would work.