D(one) IT

IT Tips, Tricks & Such

Fixing Public Folder replication issues from Exchange 2003 to 2007 or 2010

During a transition of Exchange 2003 to 2010, we ran into replication issues in some of the Public Folders. The issue was tracked down to Categories that contain a bad character. Instead of going through all the entries manually, I found a replication fix script: http://blogs.technet.com/b/bill_long/archive/2010/04/22/fixing-public-folder-replication-errors-from-exchange-2003-to-exchange-2007-or-2010.aspx

Notes:

  • You might need to modify $allPFs. Change the “All Public Folders” text to match however it looks in Outlook.
  • By default the script will not change any data unless you set $commitChanges to true.
  • Power shell’s default script execution mode is Restricted, which might prevent the use of Fix-PFItems.ps1. Quick workaround is to run the following command in power shell (remember to change back to restricted when done):
    Set-ExecutionPolicy Unrestricted

Fix-PFItems.ps1 (click “show source” below)


# Fix-PFItems.ps1
#
# This script should be run on a workstation where Outlook is installed
# as well as Powershell. You do not need the Exchange Management Console
# installed in order to use this. The Outlook profile you are using should be
# one that will access public folders on the Exchange 2003 server, because
# the whole point is to fix the items on the 2003 side so they will replicate
# to Exchange 2007/2010. If you're not sure which replica of a folder your
# profile is accessing, launch MFCMapi, logon to the same profile, navigate
# to the public folder, and look at the PR_REPLICA_SERVER property. This will
# tell you which replica your client is looking at.
#
# Syntax info:
#
# Examples:
# .\Fix-PFItems -folderPath "Departments\HR" -splitOnBadchar $true -resetEmptyCategories $false -doAppointments $false -doInstanceKey $false -doSubfolders $false
# .\Fix-PFItems "Departments\HR" $true $false $false $false $false
#
# folderPath should be in the form: "TopLevelFolder\Subfolder\Subfolder 2"
# a folderPath of "" will run against all public folders
# splitOnBadChar determines whether we split the category into two names, or just replace badChar
# resetEmptyCategories will clear all categories on items that already appear to have no
#   categories. This ensures that the categories value is REALLY empty, and should fix
#   items that have an empty array in the categories. Be aware that since this changes ALL
#   items that appear to have no categories, it could cause a lot of replication.
# doAppointments determines whether appointment items are processed. Note that if this is $true
#    and $commitChanges is set to $true, ALL appointment items in the specified folders
#    will be modified by the script. This will change the last modified time and cause
#    replication.
# doInstanceKey determines whether we clear PR_INSTANCE_KEY on items. Note that if this is $true
#    and $commitChanges is set to $true, ALL items in ALL folders that you specify will be
#    be modified.
# doSubfolders determines whether we automatically traverse subfolders of the specified folder
#
# This script will identify 5 types of problems:
#
# 1. Categories that contain a bad character, typically a comma. This error is identified in the
#    content conversion tracing by output that states:
#
#    PropertyValidationException: Property validation failed. Property = [{GUID}:'Keywords']
#    Categories Error = Element 0 in the multivalue property is invalid..
#
#    This problem will always be fixed if $commitChanges is $true (see below). The
#    -splitOnBadChar parameter lets you choose whether to split the category into two separate
#    categories (A category such as "catone,cattwo" would become two separate categories
#    "catone" and "cattwo") or to simply get rid of the bad character ("catone,cattwo" becomes
#    "catonecattwo").
#
# 2. Categories that contain an emtpy array. This problem is identified by the same tracing
#    output as that shown for problem #1. This problem can be fixed if -resetEmptyCategories
#     is $true and $commitChanges is changed to $true (see below). Note that this will modify
#    all items that appear to have no categories, as the script can't tell the difference
#    between an empty array and an item that truly has no categories.
#
# 3. Appointment items with invalid start/end dates. This problem can be fixed if $doAppointments
#     is $true and $commitChanges is changed to $true. See below. Note that this does not fix
#    every possible problem with appointments.
#
# 4. Address type properties that are longer than 9 characters. This problem can be identified
#    by content conversion tracing that states:
#
#    Property validation failed. Property = [{GUID}:0x8nnn] Email2AddrType Error =
#    Email2AddrType is too long: maximum length is 9, actual length is nn..
#
#    This problem cannot be fixed by the script. You can try to fix it manually using
#    mfcmapi, but sometimes these properties cannot be changed and the item
#    must be deleted.
#
# 5. A bad PR_INSTANCE_KEY. This problem can be identified by an error in the
#    content conversion tracing that says MapiExceptionPropsDontMatch. This problem can
#    be fixed by the script if -doInstanceKey is $true and $commitChanges is $true (see
#    below).
#

param([string]$folderPath, [bool]$splitOnBadchar, [bool]$resetEmptyCategories, [bool]$doAppointments, [bool]$doInstanceKey, [bool]$doSubfolders)

#
# By default, the script does NOT change anything. You must
# change $commitChanges to $true in order for the script to actually
# do anything.
#
# This has the potential to change a lot of items in your public folders!
# Run it in READ ONLY mode first to see what will get changed, and have a
# good backup just in case!
#
$commitChanges = $false

# $badChar is the character we want to get rid of in the category names.
# Normally it's a comma, but it can be changed to anything you want.
$badChar = ","

# $reportAll will make the script report all categories and address types
# that were found. This is just for reporting purposes so you can see what values
# exist.
$reportAll = $false

#
##########################################################################
#
# Be careful changing anything below this point.
#
##########################################################################
#

$allCategories = new-object System.Collections.Specialized.StringCollection
$allAddressTypes = new-object System.Collections.Specialized.StringCollection

function GetNamedFromCollection($name, $collection)
{
 foreach ($item in $collection)
 {
 if ($item.Name -eq $name -or $item.DisplayName -eq $name)
 {
 return $item
 }
 }
 return $null
}

function RecordAddrType($type)
{
 if ($reportAll)
 {
 if (!($allAddressTypes.Contains($type)))
 {
 $temp = $allAddressTypes.Add($type)
 }
 }
}

function DoCategories($item)
{
 $categoryArray = $item.PropertyAccessor.GetProperty("urn:schemas-microsoft-com:office:office#Keywords")
 if ($categoryArray.Length -eq 0 -and $resetEmptyCategories -eq $true)
 {
 if ($commitChanges)
 {
 "    Resetting categories..."
 $item.PropertyAccessor.SetProperty("urn:schemas-microsoft-com:office:office#Keywords", $null)
 $item.Save()
 }
 else
 {
 "    Would have reset the categories, but we're in Read-Only mode."
 }
 }
 else
 {
 $fixedCategories = $false
 [string[]]$newCategoryArray = @()
 foreach ($cat in $categoryArray)
 {
 if ($reportAll)
 {
 if (!($allCategories.Contains($cat)))
 {
 $temp = $allCategories.Add($cat)
 }
 }
 if ($cat.Contains($badChar))
 {
 if (!($splitOnBadchar))
 {
 # In this case, we just replace the badChar with nothing
 $newCategoryArray += $cat.Replace($badChar, "")
 }
 else
 {
 # In this case, we split it into multiple separate categories
 $categorySplit = $cat.Split($badChar)
 foreach ($newCat in $categorySplit)
 {
 $newCat = $newCat.Trim()
 if ($newCat.Length -gt 0)
 {
 $newCategoryArray += $newCat
 }
 }
 }
 $fixedCategories = $true
 }
 elseif ($cat.Trim() -eq "")
 {
 # this category should be deleted from the item
 $fixedCategories = $true
 }
 else
 {
 $newCategoryArray += $cat
 }
 }

if ($fixedCategories)
 {
 "    Old category list for this item:"
 foreach ($cat in $categoryArray)
 {
 ("        " + $cat)
 }
 "    New category list for this item:"
 foreach ($cat in $newCategoryArray)
 {
 ("        " + $cat)
 }
 if ($commitChanges)
 {
 $item.PropertyAccessor.SetProperty("urn:schemas-microsoft-com:office:office#Keywords", $newCategoryArray)
 $item.Save()
 ("    Changes saved.")
 }
 else
 {
 ("    Changes not saved (READ ONLY mode).")
 }
 }
 }
}

function DoAppointmentProps($item)
{
 if ($item.Class -eq 26) # olAppointment in the olObjectClass enumeration is 26
 {
 if ($commitChanges)
 {
 ("    Updating appointment props: " + $item.Subject)
 if ($item.IsRecurring)
 {
 $recurPattern = $item.GetRecurrencePattern()
 $recurPattern.StartTime = $recurPattern.StartTime
 $recurPattern.EndTime = $recurPattern.EndTime
 $item.Save()
 }
 else
 {
 $item.Start = $item.Start
 $item.End = $item.End
 $item.Save()
 }
 }
 else
 {
 ("    Appointment props not updated (READ ONLY mode).")
 }
 }
}

function DoInstanceKey($item)
{
 if ($commitChanges)
 {
 ("    Clearing PR_INSTANCE_KEY: " + $item.Subject)
 $item.PropertyAccessor.DeleteProperty("<a href="http://schemas.microsoft.com/mapi/proptag/0x0FF60102%22)">http://schemas.microsoft.com/mapi/proptag/0x0FF60102")</a>
 $item.Save()
 ("    Changes saved.")
 }
 else
 {
 ("    PR_INSTANCE_KEY not cleared (READ ONLY mode).")
 }
}

function DoAddrType($item)
{
 $senderAddrType = $item.PropertyAccessor.GetProperty("<a href="http://schemas.microsoft.com/mapi/proptag/0x0C1E001E%22)">http://schemas.microsoft.com/mapi/proptag/0x0C1E001E")</a>
 RecordAddrType $senderAddrType
 if ($senderAddrType.Length -gt 9)
 {
 ("    WARNING! PR_SENDER_ADDRTYPE is too long: " + $senderAddrType)
 }

$sentRepresentingAddrType = $item.PropertyAccessor.GetProperty("<a href="http://schemas.microsoft.com/mapi/proptag/0x0064001E%22)">http://schemas.microsoft.com/mapi/proptag/0x0064001E")</a>
 RecordAddrType $sentRepresentingAddrType
 if ($sentRepresentingAddrType.Length -gt 9)
 {
 ("    WARNING! PR_SENT_REPRESENTING_ADDRTYPE is too long: " + $sentRepresentingAddrType)
 }

$recipients = $item.Recipients
 if ($recipients -ne $null)
 {
 foreach ($recipient in $recipients)
 {
 $addrType = $recipient.PropertyAccessor.GetProperty("<a href="http://schemas.microsoft.com/mapi/proptag/0x3002001E%22)">http://schemas.microsoft.com/mapi/proptag/0x3002001E")</a>
 if ($addrType.Length -gt 9)
 {
 ("    WARNING! A recipient PR_ADDRTYPE is too long: " + $addrType)
 }
 }
 }

if ($item.Email1AddressType.Length -gt 0)
 {
 RecordAddrType $item.Email1AddressType
 }
 if ($item.Email1AddressType.Length -gt 9)
 {
 ("    WARNING! Email1AddressType is too long: " + $item.Email1AddressType)
 }

if ($item.Email2AddressType.Length -gt 0)
 {
 RecordAddrType $item.Email2AddressType
 }
 if ($item.Email2AddressType.Length -gt 9)
 {
 ("    WARNING! Email2AddressType is too long: " + $item.Email2AddressType)
 }

if ($item.Email3AddressType.Length -gt 0)
 {
 RecordAddrType $item.Email3AddressType
 }
 if ($item.Email3AddressType.Length -gt 9)
 {
 ("    WARNING! Email3AddressType is too long: " + $item.Email3AddressType)
 }
}

function DoFolder($folder)
{
 $replicaServer = $folder.PropertyAccessor.GetProperty("<a href="http://schemas.microsoft.com/mapi/proptag/0x6644001E%22)">http://schemas.microsoft.com/mapi/proptag/0x6644001E")</a>
 $items = $folder.Items
 ($items.Count.ToString() + " items found in folder " + $folder.FolderPath)
 ("Accessing this folder on server: " + $replicaServer)
 foreach ($item in $items)
 {
 ("Checking item: " + $item.Subject)
 if ($item.PropertyAccessor -ne $null)
 {
 DoCategories($item)
 DoAddrType($item)
 if ($doAppointments)
 {
 DoAppointmentProps($item)
 }
 if ($doInstanceKey)
 {
 DoInstanceKey($item)
 }
 }
 else
 {
 "    No PropertyAccessor on this item. It may be in a conflict state."
 }
 }
 if ($doSubfolders)
 {
 foreach ($subfolder in $folder.Folders)
 {
 DoFolder($subfolder)
 }
 }
}

"Starting..."

if ($commitChanges)
{
 "commitChanges has been set to TRUE. The script WILL save changes."
}
else
{
 "commitChanges is set to FALSE. Running in READ ONLY mode. Changes will NOT be saved."
}

$outlook = new-object -com Outlook.Application
if (!($outlook.Version -like "12.*" -or $outlook.Version -like "14.*"))
{
 ("This script requires Outlook 2007 or 2010. Your version: " + $outlook.Version)
 return
}
$mapi = $outlook.GetNamespace("MAPI")

#
# First, check the categories list and fix if necessary
#

"Checking Outlook categories list..."
$categories = $mapi.Categories
foreach ($category in $categories)
{
 if ($category.Name -ne $null)
 {
 if ($category.Name.Contains($badChar))
 {
 [string[]]$newNames = @()
 ("    Fixing category in Outlook category list: " + $category.Name)
 if (!($splitOnBadchar))
 {
 # In this case, we just replace the badChar with nothing
 $newName += $category.Name.Replace($badChar, "")
 }
 else
 {
 # In this case, we split it into multiple separate categories
 $categorySplit = $category.Name.Split($badChar)
 foreach ($newCat in $categorySplit)
 {
 if ($newCat.Length -gt 0)
 {
 $newNames += $newCat
 }
 }
 }
 if ($commitChanges)
 {
 $foo = $categories.Remove($category.Name)
 foreach ($newName in $newNames)
 {
 $foo = $categories.Add($newName)
 }
 }
 }
 }
}

#
# Categories list should be in good shape now.
# Now find the specified folder.
#

"Finding the specified folder..."
$session = $mapi.Session
$pfStore = $null
foreach ($store in $mapi.Stores)
{
 if ($store.ExchangeStoreType -eq 2)
 {
 $pfStore = $store
 }
}
$pfRoot = $pfStore.GetRootFolder()
$allPFs = GetNamedFromCollection "All Public Folders" $pfRoot.Folders
if ($allPFs -eq $null)
{
 "Couldn't find All Public Folders folder."
 return
}

if ($pfStore -eq $null)
{
 "Couldn't find public folder store."
 return
}

$pfRoot = $pfStore.GetRootFolder()

$folderPath = $folderPath.Trim("\")
$folderPathSplit = $folderPath.Split("\")
$folder = $allPFs

if ($folderPath.Length -gt 0)
{
 "Traversing folder path..."
 for ($x = 0; $x -lt $folderPathSplit.Length; $x++)
 {
 $folder = GetNamedFromCollection $folderPathSplit[$x] $folder.Folders
 }
 if ($folder -eq $null)
 {
 ("Could not find folder: " + $folderPath)
 return
 }
 ("Found folder: " + $folder.FolderPath)
}

#
# Got the folder.
# Start processing the folder and subfolders.
#

DoFolder $folder

"Done!"

if ($reportAll)
{
 ""
 "Categories found:"
 $allCategories
 ""
 "Address types found:"
 $allAddressTypes
}

Advertisements

2 responses to “Fixing Public Folder replication issues from Exchange 2003 to 2007 or 2010

  1. Samir October 20, 2011 at 7:22 am

    Hi, it’s said in your script that FolderPath “” will run the script against all public folders but it doesn’t seem to be recursive… is-it normal?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: