Bill Long's Exchange Blog

Exchange Server stuff, focusing on Public Folders, PFDAVAdmin, ExFolders, and Powershell scripting.

Fixing Mail Enabled Public Folders per KB 977921

Fixing Mail Enabled Public Folders per KB 977921

  • Comments 12
  • Likes

I admit it. I have gotten lazy about posting my new scripts, and haven't posted anything in forever. Today, someone emailed me about the KB 977921 problem, where a public folder has a directory object but is not flagged as mail-enabled. I wrote a script for this a year ago, and never posted it. So, just as I was about to fire off an email with the script attached, I figured I would just post it here so others can benefit as well.

Keep in mind that while this script runs, it will potentially be deleting and recreating directory objects for folders. While any individual folder should only be missing its directory object for a few seconds, it might be a good idea to pause incoming mail from the internet to avoid unwanted NDRs. The script requires an export file which will contain all the old folder email addresses, and it also generates ldif exports as it runs, so if something goes horribly wrong, you already have a backup of that data. It never hurts to make sure you have a good backup of your AD as well, just in case you need to do an authoritative restore of the Microsoft Exchange System Objects container.

# Fix-MailEnabled.ps1
#
# The purpose of this script is to read an ExFolders or PFDAVAdmin
# property export of public folders, and fix the folders where the
# mail-enabled state is not consistent.
#
# The export must include the following properties:
# PR_PF_PROXY, PR_PF_PROXY_REQUIRED, DS:proxyAddresses
#
# The export can optionally include the following
# properties, and if they are included, the script will preserve
# that setting:
# DS:msExchHideFromAddressLists
#
# This script must be run from Exchange Management Shell.

# File is required, ExchangeServer and DC are optional
# Example syntax:
# .\Fix-MailEnabled C:\ExFoldersExport.txt
# .\Fix-MailEnabled C:\ExFoldersExport.txt CONTOSO1 DC01
# .\Fix-MailEnabled -File C:\ExFoldersExport.txt -ExchangeServer CONTOSO1 -DC DC01

param([string]$File, [string]$ExchangeServer, [string]$DC, [string]$emailAddress, [string]$smtpServer)

# Log file stuff

$ScriptPath = Split-Path -Path $MyInvocation.MyCommand.Path -Parent
$Global:LogFileName = "FixMailEnabled"
$Global:LogFile  = $ScriptPath + "\" + $LogFileName + ".log"
$Global:ErrorLogFile = $ScriptPath + "\FixMailEnabled-Errors.log"

$sendEmailOnError = $false
if ($emailAddress -ne $null -and $emailAddress.Length -gt 0 -and $smtpServer -ne $null -and $smtpServer.Length -gt 0)
{
    $sendEmailOnError = $true
}

function Test-Transcribing
{

    $externalHost = $host.gettype().getproperty("ExternalHost", [reflection.bindingflags]"NonPublic,Instance").getvalue($host, @())
    try
    {
        $externalHost.gettype().getproperty("IsTranscribing", [reflection.bindingflags]"NonPublic,Instance").getvalue($externalHost, @())
    }
    catch
    {
        write-warning "This host does not support transcription."

    }

}

function writelog([string]$value = "")
{
    $Global:LogDate = Get-Date -uformat "%Y %m-%d %H:%M:%S"
    ("$LogDate $value")
}

function writeerror([string]$value = "")
{
    $Global:LogDate = Get-Date -uformat "%Y %m-%d %H:%M:%S"
    Add-Content -Path $Global:ErrorLogFile -Value ("$LogDate $value")
    if ($sendEmailOnError)
    {
        writelog("Sending email notification...")
        Send-MailMessage -From "Fix-MailEnabled@Fix-MailEnabled" -To $emailAddress -Subject "Fix-MailEnabled script error" `
         -Body $value -SmtpServer $smtpServer
    }
}

$isTranscribing = Test-Transcribing
if (!($isTranscribing))
{
    $transcript = Start-Transcript $Global:LogFile -Append
    writelog ($transcript)
}
else
{
    writelog ("Transcript already started. Logging to the current file will continue.")
}

writelog ("Fix-MailEnabled starting.")

# Directory objects will be exported prior to deletion. This could
# potentionally create a lot of export files. By default these are
# put in the same folder as the script. If you want to put them
# elsewhere, change this path and make sure the folder exists.
$ExportPath = $ScriptPath

if ($ExchangeServer -eq "")
{
    # Choose a PF server
    $pfdbs = Get-PublicFolderDatabase
    if ($pfdbs.Length -ne $null)
    {
        $ExchangeServer = $pfdbs[0].Server.Name
    }
    else
    {
        $ExchangeServer = $pfdbs.Server.Name
    }
   
    writelog ("ExchangeServer parameter was not supplied. Using server: " + $ExchangeServer)
}

if ($DC -eq "")
{
    # Choose a DC
    $rootDSE = [ADSI]("LDAP://RootDSE")
    $DC = $rootDSE.Properties.dnsHostName
    writelog ("DC parameter was not supplied. Using DC: " + $DC)
}

$reader = new-object System.IO.StreamReader($File)

# The first line in this file must be the header line, so we can
# figure out which column is which. Folder Path is always the
# first column in an ExFolders property export.

$headerLine = $reader.ReadLine()
if (!($headerLine.StartsWith("Folder Path")))
{
    writelog "The input file doesn't seem to have the headers on the first line."
    return
}

# Figure out which column is which

$folderPathIndex = -1
$proxyIndex = -1
$proxyRequiredIndex = -1
$proxyAddressesIndex = -1
$hideFromAddressListsIndex = -1
$headers = $headerLine.Split("`t")
for ($x = 0; $x -lt $headers.Length; $x++)
{
    if ($headers[$x] -eq "Folder Path")
    {
        $folderPathIndex = $x
    }
    elseif ($headers[$x] -eq "PR_PF_PROXY: 0x671D0102")
    {
        $proxyIndex = $x
    }
    elseif ($headers[$x] -eq "PR_PF_PROXY_REQUIRED: 0x671F000B")
    {
        $proxyRequiredIndex = $x
    }
    elseif ($headers[$x] -eq "DS:proxyAddresses")
    {
        $proxyAddressesIndex = $x
    }
    elseif ($headers[$x] -eq "DS:msExchHideFromAddressLists")
    {
        $hideFromAddressListsIndex = $x
    }
}

if ($folderPathIndex -lt 0 -or `
    $proxyIndex -lt 0 -or `
    $proxyRequiredIndex -lt 0 -or `
    $proxyAddressesIndex -lt 0)
{
    writelog "Required columns were not present in the input file."
    writelog "Headers found:"
    writelog $headers
    return
}

# Loop through the lines in the file
while ($null -ne ($buffer = $reader.ReadLine()))
{
    $columns = $buffer.Split("`t")
    if ($columns.Length -lt 4)
    {
        continue
    }
   
    # Folder paths from ExFolders always start with "Public Folders" or
    # "System Folders", so trim the first 14 characters.
    $folderPath = $columns[$folderPathIndex].Substring(14)
    $guidString = $columns[$proxyIndex]
    $proxyRequired = $columns[$proxyRequiredIndex]
    $proxyAddresses = $columns[$proxyAddressesIndex]
    $hideFromAddressLists = $false
    if ($hideFromAddressListsIndex -gt -1)
    {
        if ($columns[$hideFromAddressListsIndex] -eq "True")
        {
            $hideFromAddressLists = $true
        }
    }
   
    if ($proxyRequired -ne "True" -and $proxyRequired -ne "1" -and $guidString -ne "PropertyError: NotFound" -and $guidString -ne "" -and $guidString -ne $null)
    {
        # does this objectGUID actually exist?
        $proxyObject = [ADSI]("LDAP://" + $DC + "/<GUID=" + $guidString + ">")
        if ($proxyObject.Path -eq $null)
        {
            # It's possible the object is in a different domain than the one
            # held by the specified DC, so let's try again without a specific DC.
            $proxyObject = [ADSI]("LDAP://<GUID=" + $guidString + ">")
        }
       
        if ($proxyObject.Path -ne $null)
        {
            # PR_PF_PROXY_REQUIRED is false or not set, but we have a directory object.
            # This means we need to mail-enable the folder. Ideally it would link up to
            # the existing directory object, but often, that doesn't seem to happen, and
            # we get a duplicate. So, what we're going to do here is delete the existing
            # directory object, mail-enable the folder, and then set the proxy addresses
            # from the old directory object onto the new directory object.
           
            # First, check if it's already mail-enabled. The input file could be out of
            # sync with the actual properties.
            $folder = Get-PublicFolder $folderPath -Server $ExchangeServer
            if ($folder -ne $null)
            {
                if ($folder.MailEnabled)
                {
                    writelog ("Skipping folder because it is already mail-enabled: " + $folderPath)
                    continue
                }
            }
            else
            {
                writelog ("Skipping folder because it was not found: " + $folderPath)
                continue
            }
           
            # If we got to this point, we found the PublicFolder object and it is not
            # already mail-enabled.
            writelog ("Found problem folder: " + $folderPath)
           
            # Export the directory object before we delete it, just in case
            $fileName = $ExportPath + "\" + $guidString + ".ldif"
            $ldifoutput = ldifde -d $proxyObject.Properties.distinguishedName -f ($fileName)
            writelog ("    " + $ldifoutput)
            writelog ("    Exported directory object to file: " + $fileName)

            # Save any explicit permissions
            $explicitPerms = Get-MailPublicFolder $proxyObject.Properties.distinguishedName[0] | Get-ADPermission | `
                WHERE { $_.IsInherited -eq $false -and (!($_.User.ToString().StartsWith("NT AUTHORITY"))) }

            # Save group memberships
            # We need to do this from a GC to make sure we get them all
            $memberOf = ([ADSI]("GC://" + $proxyObject.Properties.distinguishedName[0])).Properties.memberOf
           
            # Delete the current directory object
            # For some reason Parent comes back as a string in Powershell, so
            # we have to go bind to the parent.
            $parent = [ADSI]($proxyObject.Parent.Replace("LDAP://", ("LDAP://" + $DC + "/")))
            if ($parent.Path -eq $null)
            {
                $parent = [ADSI]($proxyObject.Parent)
            }
           
            if ($parent.Path -eq $null)
            {
                $proxyObject.Parent
                writelog ("Skipping folder because bind to parent container failed: " + $folderPath)
                continue
            }
           
            $parent.Children.Remove($proxyObject)
            writelog ("    Deleted old directory object.")
           
            # Mail-enable the folder
            Enable-MailPublicFolder $folderPath -Server $ExchangeServer
            writelog ("    Mail-enabled the folder.")
           
            # Disable the email address policy and set the addresses.
            # Because we just deleted the directory object a few seconds ago, it's
            # possible that that change has not replicated everywhere yet. If the
            # Exchange server still sees the object, setting the email addresses will
            # fail. The purpose of the following loop is to retry until it succeeds,
            # pausing in between. If this is constantly failing on the first try, it
            # may be helpful to increase the initial pause.
            $initialSleep = 30 # This is the initial pause. Increase it if needed.
            writelog ("    Sleeping for " + $initialSleep.ToString() + " seconds.")
            Start-Sleep $initialSleep
           
            # Filter out any addresses that aren't SMTP, and put the smtp addresses
            # into a single comma-separated string.
            $proxyAddressArray = $proxyAddresses.Split(" ")
            $proxyAddresses = ""
            foreach ($proxy in $proxyAddressArray)
            {
                if ($proxy.StartsWith("smtp:"))
                {
                    if ($proxyAddresses.Length -gt 0)
                    {
                        $proxyAddresses += ","
                    }
                   
                    $proxyAddresses += $proxy.Substring(5)
                }
                elseif ($proxy.StartsWith("SMTP:"))
                {
                    if ($proxyAddresses.Length -gt 0)
                    {
                        $proxyAddresses = $proxy.Substring(5) + "," + $proxyAddresses
                    }
                    else
                    {
                        $proxyAddresses = $proxy.Substring(5)
                    }
                }
            }

            $proxyAddresses = $proxyAddresses.Split(",")
            $retryCount = 0
            $maxRetry = 3 # The maximum number of times we'll retry
            $succeeded = $false
            while (!($succeeded))
            {
                writelog ("    Setting proxy addresses...")
                # Retrieve the new proxy object
                $newMailPublicFolder = Get-MailPublicFolder $folderPath -Server $ExchangeServer

                # Now set the properties
                $Error.Clear()
                Set-MailPublicFolder $newMailPublicFolder.Identity -EmailAddressPolicyEnabled $false -EmailAddresses $proxyAddresses `
                    -HiddenFromAddressListsEnabled $hideFromAddressLists -Server $ExchangeServer
                if ($Error[0] -eq $null)
                {
                    $succeeded = $true
                }
                else
                {
                    writelog ("    Error encountered in Set-MailPublicFolder: " + $Error[0].ToString())
                    if ($retryCount -lt $maxRetry)
                    {
                        $retryCount++
                        writelog ("    Pausing before retry. This will be retry number " `
                            + $retryCount.ToString() + ". Max retry attempts is " + $maxRetry.ToString() + ".")
                        Start-Sleep 60 # This is how long we'll pause before trying again
                    }
                    else
                    {
                        writelog ("    Max retries reached. You must manually set the properties.")
                        writelog ("    See the error log for more details.")
                        writeerror ("Failed to set proxyAddresses on folder.`r`nFolder: " + $folderPath + `
                            "`r`nProxy Addresses:`r`n" + $proxyAddresses + `
                            "`r`nGroup membership:`r`n" + $memberOf + `
                            "`r`nExplicit Permissions:`r`n" + ($explicitPerms | Select-Object User,AccessRights | out-string) + "`r`n")
                       
                        break
                    }
                }
            }

            if ($succeeded -and $explicitPerms -ne $null)
            {
                $succeeded = $true
                writelog ("    Setting explicit permissions on new directory object...")
                $newMailPublicFolder = Get-MailPublicFolder $folderPath -Server $ExchangeServer
                foreach ($permission in $explicitPerms)
                {
                    $Error.Clear()
                    $temp = Add-ADPermission $newMailPublicFolder.Identity -User $permission.User -AccessRights $permission.AccessRights
                    if ($Error[0] -ne $null)
                    {
                        $succeeded = $false
                        writelog ("    Error setting explicit permissions. You must manually set the permissions:")
                        writelog ($explicitPerms)
                        writeerror ("Failed to set explicit permissions on folder.`r`nFolder: " + $folderPath + `
                        "`r`nExplicit Permissions:`r`n" + ($explicitPerms | Select-Object User,AccessRights | out-string) + "`r`n")

                        break
                    }
                }
            }

            if ($succeeded -and $memberOf -ne $null)
            {
                writelog ("    Setting group memberships...")
                $newMailPublicFolder = Get-MailPublicFolder $folderPath -Server $ExchangeServer
                $proxy = [ADSI]("LDAP://<GUID=" + $newMailPublicFolder.Guid + ">")
                $proxyDn = $proxy.Properties.distinguishedName[0]
                $succeeded = $true
                foreach ($group in $memberOf)
                {
                    $Error.Clear()
                    $groupObject = [ADSI]("LDAP://" + $group)
                    $temp = $groupObject.Properties.member.Add($proxyDn)
                    $groupObject.CommitChanges()
                    if ($Error[0] -ne $null)
                    {
                        writelog ("    Error setting group memberships. You must add the folder to these groups:")
                        writelog ($memberOf)
                        writeerror ("Failed to set group memberships on folder.`r`nFolder: " + $folderPath + `
                        "`r`nGroup memberships:`r`n" + $memberOf + "`r`n")
                        $succeeded = $false
                        break
                    }
                }
            }
           
            writelog ("    Set the properties on the new directory object.")
            writelog ("    Done with this folder.")  
        }
        else
        {
            # This means the input file said this was a bad folder, but when we tried
            # to bind to the objectGUID from PR_PF_PROXY, we failed. Either the file
            # is out of sync with the folder settings, or something else went wrong.
            # Do we want to generate any output here?
            writelog ("Skipping folder because the objectGUID was not found: " + $folderPath)
        }

    }
    else
    {
        # If we got here, it means that according to the input file, the folder is
        # not in a state where it has a directory object but is not mail-enabled.
        # Nothing we need to do in that case. The folder is good.
    }
   
}

$reader.Close()
writelog "Done!"
if (!($isTranscribing))
{
    Stop-Transcript
}

Comments
  • Thanks for this -- I'm the client that has caused your peer to email you in the first place.  :)  This validates the approach I was planning ... but I see you've left two important set of values out:  delivery restrictions, and saving the old legacyExchangeDn value as an X500 address on the new folder (to ensure it doesn't break any Outlook nicknames caches.)

  • Also: the altRecipient property

  • The nice thing about a script is you can always change it up for your particular needs. :-)  You can easily add the legacyExchangeDN and altRecipient to the ExFolders export by specifying those properties when you run it. Then, up in this section of the script:

    # Figure out which column is which

    $folderPathIndex = -1

    $proxyIndex = -1

    Add variables to store the indexes of those two columns, and add if statements to find them and set the values. In this section:

       # Folder paths from ExFolders always start with "Public Folders" or

       # "System Folders", so trim the first 14 characters.

       $folderPath = $columns[$folderPathIndex].Substring(14)

       $guidString = $columns[$proxyIndex]

       $proxyRequired = $columns[$proxyRequiredIndex]

    Add some lines to retrieve the values of those columns and store them in variables, such as $altRecipient and $legacyDN. Finally, just before these writelog statements:

               }

               writelog ("    Set the properties on the new directory object.")

               writelog ("    Done with this folder.")  

    We need to add code to set those props. Something like:

    $proxy = [ADSI]("LDAP://<GUID=" + $newMailPublicFolder.Guid + ">")

    $proxy.Properties.altRecipient.Add($altRecipient)

    $proxy.Properties.proxyAddresses.Add(("X500:" + $legacyDN))

    $proxy.CommitChanges()

  • Indeed!  We're adding close to half a dozen other important attributes to this.  In fact, since we're doing this as part of our migrations from 2003 --> 2010 we're skipping ExFolders entirely and instead making WebDAV calls on the fly to find these folders and correcting them using a combination of cmdlets and direct ADSI edits.  In our world, it's safer to do these on the fly over the course of months of replications to new servers, rather than do all 90k+ mail-enabled folders at once and risk breaking things on an epic level.  I just wanted to call it out that this wouldn't cover everything in a production situation, in case someone else decided to cut-and-paste and run with it.  :)  I appreciate you posting this so quickly!

    One last question:  When you had to do this in the past, were there impacts to rules anywhere?  i.e. what if a PF has a rule to forward content, or (even worse) what if a folder that gets corrected in this manner is the *target* of a rule in some other random PF or mailbox?  I think saving the old legDn as a new X500 would be enough (of course we intend to test), but am wondering if you had any thoughts.

    Thanks again!  You rock!

  • Not sure on that. I would expect that having the same legacy DN/X500 address would be enough, but yeah, testing it for yourself is the best approach.

  • I have opened a case with Microsoft two weeks ago for this exact problem. Currently, they try to reproduce the issue which has happened after the migration from 2007 SP3 rollup 10 to Exchange 2013 CU1. No way to just relink objects instead of recreating them ?

  • Hi Chris, I have seen rare instances where, if you simply mail-enable the folder, Exchange finds the existing object and decides not to create a new one. However, in the vast majority of cases, Exchange seems to create a brand new, duplicate object. That's why it's usually necessary to save the properties you're interested in, delete the directory objects, mail-enable to create new objects, and apply the old properties. So you can try just mail-enabling a folder or two and see what happens. You'll most likely get duplicates.

  • Thanks for the blog, Bill. It helped me figure out what was going on with our 2003/2010 public folders. In doing some additional testing and research, I've noticed, though, that if you have Exchange 2003 and are replicating between that and a 2010 server, you can use PFDAVAdmin on the 2003 servers to set the PR_PROXY_REQUIRED property and once it replicated to 2010 the Public Folders will be properly mail enabled with the necessary addresses attached. I don't know if this works in all environments, but it does work in ours. Do you know of a way to script that change against a 2003 server's public folders?

  • You know, that actually makes some sense. The reason we go through this big complicated process in Exchange 2010 is that when you mail-enable a folder in Exchange 2010, and there's already a directory object. 2010 usually seems to create a duplicate object. But back in Exchange 2003, Exchange would often recognize that a directory object already exists and would decide not to create a new one. Setting PR_PF_PROXY_REQUIRED to 1 (true) is equivalent to right-clicking on the folder in ESM and choosing to mail-enable it. Both set the property in store, which causes store to go check for a directory object and create one if it doesn't find it. Unfortunately, there's no way to script this on Exchange 2003, but you can use PFDAVAdmin to make a bulk property change against a tree of folders.

  • I found a way to get all the changes done automatically with PFDAVAdmin, and the replication got all of our Exchange 2010 public folders fixed. It's a little screwy, so I'll write my own blog on the subject and link here if that's ok.

  • Awesome! Yep that's fine.

  • I've finally finished writing my own blog explaining the process for fixing this problem if you still have Exchange 2003 Public Folder replicas around. Available here: http://acbrownit.wordpress.com/2014/02/28/public-folder-migration-issues-resolution-for-kb-977921-with-pfdavadmin/

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment