• Fixing Mail Enabled Public Folders per KB 977921

    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
    }

  • Get-PublicFolderStatistics Can’t Find A Folder

    On Exchange 2010, when you use the Get-PublicFolderStatistics command to look for the statistics of a particular folder, you may see an error like this one:

    image

    The cmdlet throws an InvalidOperationException saying that it “couldn’t find public folder”. However, if you look at the replica list, it clearly indicates that there is a replica on that server. This is because Get-PublicFolderStatistics is doing something sneaky in the background without warning you.

    If you run Get-PublicFolderStatistics with no other parameters, you’ll see this message:

    image

    At the end of the list of results, it is warning you that it didn’t return the whole list of folders – it only returned the first 100. You have to use -ResultSize Unlimited to get the entire list.

    When you run Get-PublicFolderStatistics and specify a particular Identity, the same thing is happening in the background, but it doesn’t warn you. By default, it only returns the first 100 folders and looks through those for a matching Identity. If the folder you specified doesn’t happen to be one of the first 100, the command fails to find it.

    To make this work, you need to add -ResultSize Unlimited to the command:

    image

    With that command, it is able to search the entire list of folders to find the one the user specified.

    Hopefully this helps anyone out there who is confused by the behavior of this cmdlet. On a recent case, quite a bit of time was spent before we understood why the command was failing to find the folder!

  • Importing PFDAVAdmin or ExFolders Exports Without PFDAVAdmin or ExFolders

    It’s almost May and I haven’t posted anything yet this year, so it’s definitely time to post a new script.

    I recently worked with a customer that needed to export the public folder permissions from one Exchange organization and import them into another. The trick was that on the import side, some of the accounts were only mail-enabled – not mailbox-enabled. This creates a problem for ExFolders, because the API it uses does not consider mail-enabled accounts to be valid security principals. However, if you use the Add-PublicFolderClientPermission cmdlet to set the permissions, it has no problem with such an account.

    I looked at changing ExFolders to better support this, but the changes would be complex and I just didn’t have that kind of time. Instead, I decided to write a script to import the permissions using the cmdlet. There are a few things to be aware of about the script.

    First, the script will be much slower than using PFDAVAdmin or ExFolders to run the import. This is because the cmdlets only let you modify one user at a time, and unless you want to do a lot of complicated calculations, you need to remove the existing permissions for the user before adding the new ones. This means we end up running two cmdlets for every user permission. If you are importing permissions for 5 users on a folder, it will take 10 commands to assign those permissions.

    Second, the script currently only works with exports that are in legacyExchangeDN format. If you used NT Account format (domain\user), this script can’t handle them. This means the legacyExchangeDNs from the old environment have to be resolvable in the new one. That is usually the case, since they are brought over as X500 addresses in most migrations.

    And finally, this script works only with public folder permissions exports from PFDAVAdmin or ExFolders. If you exported replicas or properties or something, this script won’t help you.

    Anyway, here is the script. Enjoy!

    # Import-PFPermissions.ps1
    #
    # The purpose of this script is to import permissions from an export
    # file generated using PFDAVAdmin or ExFolders.
    #
    # Syntax example:
    #
    # .\Import-PFPermissions C:\someimportfile.txt MYPFSERVER

    param([string]$importFile, [string]$server)

    function ConvertRightsString([string]$pfdavRightsString)
    {
        $pfdavRights = $pfdavRightsString.Split(@(' '))
        $powershellRights = ""
        foreach ($right in $pfdavRights)
        {
            if ($right -eq "All")
            {
                $powershellRights += "Owner,"
            }
            elseif ($right -eq "Owner")
            {
                $powershellRights += "CreateItems,ReadItems,CreateSubfolders,FolderOwner,FolderVisible,EditOwnedItems,EditAllItems,DeleteOwnedItems,DeleteAllItems,"
            }
            elseif ($right -eq "Contact")
            {
                $powershellRights += "FolderContact,"
            }
            elseif ($right -eq "Create")
            {
                $powershellRights += "CreateItems,"
            }
            elseif ($right -eq "CreateSubfolder")
            {
                $powershellRights += "CreateSubfolders,"
            }
            elseif ($right -eq "Delete")
            {
                $powershellRights += "DeleteAllItems,"
            }
            elseif ($right -eq "DeleteOwn")
            {
                $powershellRights += "DeleteOwnedItems,"
            }
            elseif ($right -eq "Write")
            {
                $powershellRights += "EditAllItems,"
            }
            elseif ($right -eq "WriteOwn")
            {
                $powershellRights += "EditOwnedItems,"
            }
            elseif ($right -eq "o")
            {
                $powershellRights += "FolderOwner,"
            }
            elseif ($right -eq "Visible")
            {
                $powershellRights += "FolderVisible,"
            }
            elseif ($right -eq "Read")
            {
                $powershellRights += "ReadItems,"
            }
            else
            {
                $powershellRights += ($right + ",")
            }
        }
       
        $powershellRights = $powershellRights.TrimEnd(@(','))
        return $powershellRights
    }

    function RemoveFolderVisibleWithTrap([string]$folderPath, [string]$server, [string]$userString)
    {
        trap [Exception]
        {
            continue
        }
       
        $returnedValue = Remove-PublicFolderClientPermission -Identity $folderPath -Server $server -User $userString -AccessRights FolderVisible
    }

    $fileReader = new-object System.IO.StreamReader($importFile)
    while ($null -ne ($buffer = $fileReader.ReadLine()))
    {
        if ($buffer.StartsWith("SETACL`t"))
        {
            $columns = $buffer.Split(@("`t"))
           
            # Resolve the folder path, which should always be the second column.
            $folderPath = $columns[1]
            if ($folderPath.StartsWith("Public Folders\"))
            {
                $folderPath = $folderPath.Substring(14)
            }
            elseif ($folderPath.StartsWith("System Folders\"))
            {
                $folderPath = ("\NON_IPM_SUBTREE" + $folderPath.Substring(14))
            }
           
            ("Updating folder: " + $folderPath)
           
            for ($x = 2; $x -lt $columns.Length; $x+=2)
            {
                $userString = $columns[$x]
                if ($userString.StartsWith("/o="))
                {
                    $recipient = Get-Recipient $userString
                    if ($recipient -ne $null)
                    {
                        $userString = $recipient.Identity
                    }
                    else
                    {
                        ("     Could not resolve: " + $userString)
                        continue
                    }
                }
                elseif ($userString -ne "Default" -and $userString -ne "Anonymous")
                {
                    ("     Skipping unrecognized user format: " + $userString)
                    continue
                }
                ("     Processing user: " + $userString)
                ("          Checking for existing permissions...")
                $permsForUser = Get-PublicFolderClientPermission -Identity $folderPath -Server $server -User $userString
                if ($permsForUser -ne $null)
                {
                    ("          Removing existing permissions...")
                    $permsForUser | Remove-PublicFolderClientPermission -Identity $folderPath -Server $server -Confirm:$false
                }
               
                ("          Adding new permissions...")
                $rightsString = ConvertRightsString $columns[$x+1]
                ("          AccessRights: " + $rightsString)
                $rightsArray = $rightsString.Split(@(','))
                if ($permissionString -eq "None")
                {
                    # This requires special handling. In the cmdlet, there is no way to grant a user no permissions,
                    # denying the ability to even see the folder, because the None role includes the FolderVisible
                    # right. When a user has None without folder visible, it still shows up as None, but IsRole is
                    # false. We need to check for that situation here to make sure None is really None. In the
                    # export, None really means no rights at all.
                    #
                    # So, what we do here, is first we set rights of None. Then, we try to remove FolderVisible,
                    # and ignore any errors that happen. If it succeeds, we don't have FolderVisible anymore, and
                    # if it fails, we didn't have FolderVisible in the first place, so it doesn't matter.
                    $returnedValue = Add-PublicFolderClientPermission -Identity $folderPath -Server $server -User $userString -AccessRights $rightsArray
                    RemoveFolderVisibleWithTrap $folderPath $server $userString
                }
                else
                {
                    $returnedValue = Add-PublicFolderClientPermission -Identity $folderPath -Server $server -User $userString -AccessRights $rightsArray
                }
            }
        }
    }

  • Importing Public Folder Contacts From A CSV File

    I just had a customer who needed to migrate contacts from an external database into a public folder. They could export the contacts from the database to a CSV, but they needed a way to get the CSV into the public folder. Last night, I whipped up this quick script.

    This morning, I was looking at my RSS feeds, and found that Glen Scales had just solved the same problem in a new blog post. Figures! Here is my version, but I recommend taking a look at his work as well.

    That said, here’s what an example CSV might look like for use with my script:

    GivenName,Surname,EmailAddress1,EmailAddress2,HomePhone,MobilePhone,Business
    Contact,1,contact1@contoso.com,contact1@northwindtraders.com,888-555-1212,888-555-1212,One Microsoft Way%Redmond%WA%US%98052
    Contact,2,contact2@contoso.com,contact2@northwindtraders.com,888-555-1212,888-555-1212,One Microsoft Way%Redmond%WA%US%98052

    Notice the weird physical address syntax where each part of the address is separated with a % character. This was my way of dealing with the need to parse out the individual address fields (Street, City, State, CountryOrRegion, PostalCode). Everything else is pretty straightforward. There are a lot of properties you can set that are not shown in this example CSV. I’ve listed most of them in the comments at the top of the script. Here it is.

    # Import-PFContacts
    #
    # The purpose of this script is to import contacts into a public folder on
    # Exchange 2007 or 2010 from a CSV file. The script reads from a CSV and then
    # creates a contact with the appropriate fields using the Exchange Web
    # Services Managed API.
    #
    # Requirements:
    #
    # This script requires Powershell 2.0. If you get the following error then you do not
    # have Powershell 2.0 installed:
    #
    # The term 'Import-Module' is not recognized as a cmdlet, function, operable program,
    # or script file. Verify the term and try again.
    #
    # The script also requires the EWS managed API, which can be downloaded here:
    # http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=c3342fb3-fbcc-4127-becf-872c746840e1
    #
    # Make sure the Import-Module command below matches the DLL location of the API.
    #
    # Syntax:
    # .\Import-PFContacts admin@contoso.com C:\SomeFile.csv "Top Level Folder\Subfolder\Contacts Folder"
    #
    # The first parameter is the email address that we'll use to run autodiscovery
    # and logon. The second parameter is the import file, and the third is the folder
    # where we want to create the contacts.
    #
    # Below you will find details on the columns that are supported in the import
    # file. Note that column names are CASE SENSITIVE.
    #
    # The import file can contain the following email address columns:
    #
    # EmailAddress1, EmailAddress2, EmailAddress3
    #
    # The import file can contain the following phone number columns:
    #
    # AssistantPhone, BusinessFax, BusinessPhone, BusinessPhone2, Callback, CarPhone,
    # CompanyMainPhone, HomeFax, HomePhone, HomePhone2, Isdn, MobilePhone, OtherFax,
    # OtherTelephone, Pager, PrimaryPhone, RadioPhone, Telex, TtyTddPhone
    #
    # The import file can contain the following physical address columns. Note that
    # the syntax for these is a little odd at the moment. The address has five fields,
    # which must be in order and separated by % characters. They are street, city,
    # state, country or region, and postal code. Some of the fields can be left
    # blank if desired. An example address entry might look like this:
    # One Microsoft Way%Redmond%WA%US%98052
    # The physical address columns are:
    #
    # Home, Business, Other
    #
    # The imort file can contain the following other columns:
    #
    # AssistantName, Birthday, BusinessHomePage, CompanyName, CompleteName, Department,
    # DisplayName, FileAs, Generation, GivenName, Initials, JobTitle, Manager, MiddleName,
    # Mileage, NickName, OfficeLocation, Profession, SpouseName

    param([string]$autoDiscoverAddress, [string]$importFile, [string]$folderPath)

    ##########
    #
    # This path must match the install location of the EWS managed API. Change it if needed.
    #
    Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll"
    #
    ##########

    $emailAddressColumns = new-object System.Collections.Specialized.StringCollection
    $foo = $emailAddressColumns.AddRange(@("EmailAddress1", "EmailAddress2", "EmailAddress3"))
    $phoneNumberColumns = new-object System.Collections.Specialized.StringCollection
    $foo = $phoneNumberColumns.AddRange(@("AssistantPhone", "BusinessFax", "BusinessPhone", "BusinessPhone2", "Callback", "CarPhone"))
    $foo = $phoneNumberColumns.AddRange(@("CompanyMainPhone", "HomeFax", "HomePhone", "HomePhone2", "Isdn", "MobilePhone", "OtherFax"))
    $foo = $phoneNumberColumns.AddRange(@("OtherTelephone", "Pager", "PrimaryPhone", "RadioPhone", "Telex", "TtyTddPhone"))
    $physicalAddressColumns = new-object System.Collections.Specialized.StringCollection
    $foo = $physicalAddressColumns.AddRange(@("Business", "Home", "Other"))

    $exchService = new-object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)
    $exchService.UseDefaultCredentials = $true
    $exchService.AutodiscoverUrl($autoDiscoverAddress)
    $pfsRoot = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::PublicFoldersRoot)

    $tinyView = new-object Microsoft.Exchange.WebServices.Data.FolderView(2)
    $displayNameProperty = [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName
    $folderPathSplits = $folderPath.Split(@('\'))
    $folder = $pfsRoot
    for ($x = 0; $x -lt $folderPathSplits.Length;$x++)
    {
        $filter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($displayNameProperty, $folderPathSplits[$x])
        $results = $folder.FindFolders($filter, $tinyView)
        if ($results.TotalCount -gt 1)
        {
             ("Ambiguous name: " + $folderPathSplits[$x])
             return
        }
        elseif ($results.TotalCount -lt 1)
        {
             ("Folder not found: " + $folderPathSplits[$x])
             return
        }
        $folder = $results.Folders[0]
    }

    $importReader = new-object System.IO.StreamReader($importFile)
    $headers = $importReader.ReadLine().Split(@(','))
    $line = 1

    while ($null -ne ($buffer = $importReader.ReadLine()))
    {
        $line++
        if ($buffer.Length -gt 0)
        {
            $newContact = new-object Microsoft.Exchange.WebServices.Data.Contact($exchService)
            $columns = $buffer.Split(@(','))
            for ($x = 0; $x -lt $headers.Length; $x++)
            {
                if ($columns[$x].Length -lt 1)
                {
                    continue
                }
                if ($emailAddressColumns.Contains($headers[$x]))
                {
                    $emailAddressType = $headers[$x]
                    $emailAddressType = [Microsoft.Exchange.WebServices.Data.EmailAddressKey]::$emailAddressType
                    $newContact.EmailAddresses[$emailAddressType] = $columns[$x]
                }
                elseif ($phoneNumberColumns.Contains($headers[$x]))
                {
                    $phoneType = $headers[$x]
                    $phoneType = [Microsoft.Exchange.WebServices.Data.PhoneNumberKey]::$phoneType
                    $newContact.PhoneNumbers[$phoneType] = $columns[$x]
                }
                elseif ($physicalAddressColumns.Contains($headers[$x]))
                {
                    $addressFields = $columns[$x].Split(@('%'))
                    $addressType = $headers[$x]
                    $addressType = [Microsoft.Exchange.WebServices.Data.PhysicalAddressKey]::$addressType
                    $address = new-object Microsoft.Exchange.WebServices.Data.PhysicalAddressEntry
                    $address.Street = $addressFields[0]
                    $address.City = $addressFields[1]
                    $address.State = $addressFields[2]
                    $address.CountryOrRegion = $addressFields[3]
                    $address.PostalCode = $addressFields[4]
                    $newContact.PhysicalAddresses[$addressType] = $address
                }
                elseif ($headers[$x] -eq "Birthday")
                {
                    $newContact.Birthday = [System.DateTime]::Parse($columns[$x])
                }
                else
                {
                    $attribute = $headers[$x]
                    $newContact.$attribute = $columns[$x]
                }
            }
            $newContact.Save($folder.Id)
        }
    }

    "Done!"

  • 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!"