Submitted By: Gregory J. DeCecco
Verifies public folder migration between Exchange 2000 and Exchange 2007.
#--------------------==--------------------# set-psdebug –strict # Require declaration of powershell variables # Set-PSDebug -Off # Do not require declaration of variables #------------ Exchange Servers ------------# # # Populate $NewServer and $OldServer with your # Exchange 2007 and Exchange 2000 server names. # Don't use <server name>.<domain>.com notation. $NewServer = "<Exchange 2007 server name>"; $OldServer = "<Exchange 2000 server name>"; #---------- Public Folder Access ----------# # # Please use a fresh account for this script. It does not need # any administrative rights but I did make it a member of the new # Exchange Public Folder Administrators security group. I # suggest a fresh account because access permissions to Public # Folders will be added AND removed for this account. You don't # want to screw up a legitimate production account, do you? This # account will also need a mailbox. $ConnectAs = "<domain>\<userID>"; $Password = "<password>"; #-------- Security Permission Mode --------# # # This section needs a little explanation before you're ready to use it. It it very likely # that $ConnectAs will be unable # to view every Public Folder hosted on $OldServer. At first this script would check permissions, add 'FolderVisible' if # necessary, wait for the permissions to replicate to $OldServer, then compare Public Folder size and item count between # $OldServer and $NewServer. This was it was taking upwards of 20 minutes for the security permission update to replicate # to $OldServer each and every time an Access Denied error was encountered. This is why I broke out the functionality into # four modes. Each mode has a description after it. I recommend that you run this script under $Security_Permission_Mode_Add # by setting $Current_SPM = $Security_Permission_Mode_Add; wait a couple hours or a day for Public Folder Replication to # work its magic; re-run this script using $Security_Permission_Mode_Add_Only_As_Necessary; then when you have all your results, # run it once more using $Security_Permission_Mode_Remove to clean up the security changes. $Security_Permission_Mode_Add = 1; # Don't perform any mailbox compares. Just add 'FolderVisible' permissions for $ConnectAs where necessary. $Security_Permission_Mode_Remove = 2; # Don't perform any mailbox compares. Just remove any 'FolderVisible' permissions for $ConnectAs. $Security_Permission_Mode_No_Changes = 3; # Compare Public Folders using the security permissions available at the time of the run. $Security_Permission_Mode_Add_Only_As_Necessary = 4; # Add 'FolderVisible' permissions for $ConnectAs where necessary, # wait for Public Folder Replication, then compare folders. $Security_Permission_Mode_Add_Remove_As_Necessary = 5; # Add 'FolderVisible' permissions for $ConnectAs where necessary, # wait for Public Folder Replication, compare folders, then remove added permissions. $Current_SPM = $Security_Permission_Mode_Add_Only_As_Necessary; $TimeConstant = [int32]1800; # How many seconds to wait for security permissions to replicate from $OldServer to $NewServer per folder. # This value is neede only during $Security_Permission_Mode_Add_Only_As_Necessary #-------------- Quit On Error -------------# # # If an error is encountered while attempting to # access the Public Folders via the web site hosted by # $OldServer, the script will halt or not based on the config below. # This flag is active during modes $Security_Permission_Mode_No_Changes, # $Security_Permission_Mode_Add_Only_As_Necessary, and # $Security_Permission_Mode_Add_Remove_As_Necessary. $Quit_On_Error = $True; function Check_Security([string]$PublicFolderHTTPFormat, [string]$UserID, [string]$Pass) { $strRequest = "<?xml version=""1.0"" ?><D:propfind xmlns:D=""DAV:"" xmlns:E=""http://schemas.microsoft.com/mapi/proptag/"">` <D:prop><E:x0e080014 /><E:x36020003 /></D:prop></D:propfind>"; $xmlHTTP = new-object -com msxml2.xmlhttp; #Write-Host `t Accessing: $PublicFolderHTTPFormat; #Write-Host `t UserID: $UserID; #Write-Host `t Password: $Pass; if ($PublicFolderHTTPFormat.EndsWith(".")) { $tempString = $PublicFolderHTTPFormat + "%20"; } else { $tempString = $PublicFolderHTTPFormat } $xmlHTTP.Open("PROPFIND", $tempString, $False, $UserID, $Pass); $xmlHTTP.setRequestHeader("Content-type:", "text/xml"); $xmlHTTP.setRequestHeader("Depth","0"); $xmlHTTP.send($strRequest); #Write-Host `t xmlHTTP.Status: $xmlHTTP.Status; return $xmlHTTP.Status; } [xml] $xmlLog = '<?xml version="1.0" encoding="ISO-8859-1"?> <Public_Folders><x /></Public_Folders>'; # Generate Run Details $xmlRunDetails = $xmlLog.CreateElement("Run_Details"); $xmlStart = $xmlLog.createelement("Start"); $xmlFinish = $xmlLog.createelement("Finish"); $xmlTimeStamp = $xmlLog.CreateElement("TimeStamp"); ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Year"))))).psBase.InnerText = Get-Date -UFormat %Y; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Month"))))).psBase.InnerText = Get-Date -Format %M; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Day"))))).psBase.InnerText = Get-Date -Format %d; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Time"))))).psBase.InnerText = Get-Date -UFormat %T; $xmlParameters = $xmlLog.CreateElement("Parameters"); ($xmlParameters.AppendChild(($($xmlLog.CreateElement("Connect_As_Identiy"))))).psBase.InnerText = $ConnectAs; ($xmlParameters.AppendChild(($($xmlLog.CreateElement("New_Server"))))).psBase.InnerText = $NewServer; ($xmlParameters.AppendChild(($($xmlLog.CreateElement("Old_Server"))))).psBase.InnerText = $OldServer; switch ($Current_SPM) { $Security_Permission_Mode_Add {($xmlParameters.AppendChild(($($xmlLog.CreateElement("Security_Permission_Mode"))))).psBase.InnerText = "Add";} $Security_Permission_Mode_Remove {($xmlParameters.AppendChild(($($xmlLog.CreateElement` ("Security_Permission_Mode"))))).psBase.InnerText = "Remove";} $Security_Permission_Mode_No_Changes {($xmlParameters.AppendChild` (($($xmlLog.CreateElement("Security_Permission_Mode"))))).psBase.InnerText = "No Changes";} $Security_Permission_Mode_Add_Only_As_Necessary {($xmlParameters.AppendChild` (($($xmlLog.CreateElement("Security_Permission_Mode"))))).psBase.InnerText = "Add Only As Necessary";} $Security_Permission_Mode_Add_Remove_As_Necessary {($xmlParameters.AppendChild` (($($xmlLog.CreateElement("Security_Permission_Mode"))))).psBase.InnerText = "Add Remove As Necessary";} } $Catch_Output = $xmlStart.AppendChild($xmlTimeStamp); $Catch_Output = $xmlRunDetails.AppendChild($xmlStart); $Catch_Output = $xmlRunDetails.AppendChild($xmlFinish); $Catch_Output = $xmlRunDetails.AppendChild($xmlParameters); $Catch_Output = $xmlLog.Public_Folders.AppendChild($xmlRunDetails); $intCounter = 0; Get-PublicFolder -recurse | foreach { if ($_.ParentPath -ne $null) { if ([int]$($_.ParentPath.get_length()) -gt 1) { $PublicFolderPath = $_.ParentPath + "\" + $_.name } else { $PublicFolderPath = "\" + $_.name } } else { $PublicFolderPath = "\" + $_.name } # NOTE: Microsoft allows the character "/" to be used in Public Folder Names. # Try to reference a Public Folder named "Pizza/Toppings" via HTTP. You can't. The web server # will come back with a HTTP 409 error. So we have to do a replace with "_xF8FF_". $PublicFolderWebPath = "HTTP://" + $OldServer + "/public" + $PublicFolderPath.replace("/","_xF8FF_").replace("\","/"); $PublicFolderWebPathProper = $PublicFolderWebPath.replace(" ","%20"); $ReplicaSet = $_.Replicas; $intCounter += 1; $xmlFolder = $xmlLog.CreateElement("Public_Folder"); $xmlFolder.SetAttribute("ID",[string]$intCounter); ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Name"))))).psBase.InnerText = $_.name; ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Path"))))).psBase.InnerText = $PublicFolderPath; ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Friendly_Web_Address"))))).psBase.InnerText = $PublicFolderWebPath; ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Proper_Web_Address"))))).psBase.InnerText = $PublicFolderWebPathProper; Write-Host $_.name; $Error_Flag = $False; forEach ( $Replica in $ReplicaSet ) { if ( ($([string]$Replica).ToLower().Contains($OldServer.ToLower()) -eq $true)) { # The code that deals with XML came from this location: # [link=http://www.infinitec.de/post/2004/12/Retrieve-the-size-of-a-Microsoft-Exchange-20002003-public-folder.aspx]` http://www.infinitec.de/post/2004/12/Retrieve-the-size-of-a-Microsoft-Exchange-20002003-public-folder.aspx[/link] # It was written by Henning Krause. Many thanks for the excelent code. # Visit Henning's Blog at [link=http://www.infinitec.de/author/hkrause.aspx]http://www.infinitec.de/author/hkrause.aspx[/link] # I translated it into PowerShell # A list of additional (and sometimes available) MAPI properties are available at: # [link=http://svn.opengroupware.org/OGoProjects/evolution-groupdav/trunk/utils/mapi-properties]` http://svn.opengroupware.org/OGoProjects/evolution-groupdav/trunk/utils/mapi-properties[/link] $strRequest = "<?xml version=""1.0"" ?><D:propfind xmlns:D=""DAV:"" xmlns:E=""http://schemas.microsoft.com/mapi/proptag/""><D:prop><E:x0e080014 /><E:x36020003 /></D:prop></D:propfind>"; $xmlHTTP = new-object -com msxml2.xmlhttp; $xmlDOC = new-object -com msxml2.DOMDocument; $return = $xmlDOC.loadXML($strRequest); Write-Host Inspecting: $PublicFolderWebPath; $xmlReplica = $xmlLog.CreateElement("Replica"); ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Server_Name"))))).psBase.InnerText = $OldServer; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Web_Address_Header"))))).psBase.InnerText = 'http://' + $OldServer; # I was getting a lot of HTTP 401 Access Denied errors while attempting to access # some public folders. In the end, I discovered that this is because the account # I'm using to troll through the Public Folders does not have 'FolderVisible' # rights for that particular Public Folder after ALL the folder permissions have # been compiled and compared against the user $ConnectAs. # So I got the bright idea to check for a denied condition ("401" error) and # grant 'FolderVisible' access rights using the "Add-PublicFolderClientPermission" # cmdlet that you'll see a couple lines below. Initially I didn't use the "-Server" # parameter. This caused some heartache. The security changes were being made on # $NewServer and would eventually take effect on $OldServer through the Public # Folder replication. This was not fast enough. I tried to grant rights on # $OldServer manually using the Exchange System Manager (remember: $OldServer is # running Exchange 2000) and after a few seconds, I got this email in my inbox: # 'A folder design conflict has occurred in "Public Folder Store (<$OldServer>)". # The design of this folder has been simultaneously modified on two or more # folder replicas. Only the set of changes made last have been saved.' # Having read this, I decided to attempt to use the "-Server $OldServer" parameter # of the cmdlet "Add-PublicFolderClientPermission". # This failed miserably since $OldServer is running Exchange 2000. My only option # left was to grant permissions on $NewServer then wait for the predefined replication # period before continuing. Through repeated testing, I found that I had to keep # keep extending the wait time stored in $Time for the security replication to take place. # Test to see if $ConnectAs can access $PublicFolderPath. # If not, add an Access Control Entry (ACE) into the Access Control Llist (ACL) # for $PublicFolderPath. $Time = $TimeConstant; $TimeCounter = $Time; If (( $(Check_Security $PublicFolderWebPathProper $ConnectAs $Password) -eq 401 ) -and ` (($Current_SPM -eq $Security_Permission_Mode_Add) -or ` ($Current_SPM -eq $Security_Permission_Mode_Add_Only_As_Necessary) -or ` ($Current_SPM -eq $Security_Permission_Mode_Add_Remove_As_Necessary)) ) { Write-Host `t Adding FolderVisible Access Permissions to $NewServer; ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Security_Action"))))).psBase.InnerText = ` "Adding FolderVisible Access Permissions to " + $NewServer + " for " + $ConnectAs; $return = Add-PublicFolderClientPermission -Identity $PublicFolderPath -User $ConnectAs -AccessRights "FolderVisible" -Confirm:$False; If (($Current_SPM -eq $Security_Permission_Mode_Add) -or ` ($Current_SPM -eq $Security_Permission_Mode_Add_Only_As_Necessary)) { $strActivity = 'Waiting up to ' + $Time.tostring() + ' Seconds for Public Folder replication to push permissions to ' + $OldServer; $Show = $True; While (($Time -gt 0) -and ($(Check_Security $PublicFolderWebPathProper $ConnectAs $Password) -eq 401)){ Write-Progress -Activity $strActivity -Status "Polling in 10 Second Intervals" -SecondsRemaining $TimeCounter -Completed $Show; Start-Sleep -Seconds 10; $TimeCounter = $TimeCounter - 10; if ($Show -eq $True) {$Show = $False} else {$Show = $True} } Write-Progress -Activity $strActivity -Status "Polling in 10 Second Intervals" ` -SecondsRemaining $Time; #-PercentComplete (0) -Completed:$True; Write-Progress -Activity $strActivity -Status "Polling in 10 Second Intervals" -Completed:$True; } } # We've passed through the section of code that makes security changes on the fly (if necessary). # Hopefully by this point everything is in order to allow a successful read attempt when we access # the HTTP directory that represents the Public Folder located on $OldServer. if ( ($Current_SPM -eq $Security_Permission_Mode_Add_Remove_As_Necessary) -or ` ($Current_SPM -eq $Security_Permission_Mode_Add_Only_As_Necessary) -or ` ($Current_SPM -eq $Security_Permission_Mode_No_Changes) ) { # Some of the folders in my organization ended with a period (".") and a space. # I tried removing just the space but that frequently didn't work; the space would come back. # If I removed both the space and the period, the change stuck. So when I encounter # a public folder that ends with a period, I append a space to the end of the URL. # Then IIS can access the URL. This is explained again in greater detail several # lines below. Search for "404_Error" and look for the big blob of text above it. if ($PublicFolderWebPathProper.EndsWith(".")) { $tempString = $PublicFolderWebPathProper + "%20"; } else { $tempString = $PublicFolderWebPathProper } $xmlHTTP.Open("PROPFIND", $tempString, $False, $ConnectAs, $Password); $xmlHTTP.setRequestHeader("Content-type:", "text/xml"); $xmlHTTP.setRequestHeader("Depth","0"); $xmlHTTP.send($strRequest); $xmlDOC = $xmlHTTP.ResponseXML; if ( $xmlHTTP.Status -eq 207 ) { $xmlDOC.SetProperty("SelectionNamespaces", "xmlns:ex='http://schemas.microsoft.com/mapi/proptag/'"); # To see the properties & methods for a single XML Node (Msxml2.DOMDocument), uncomment this next line # forEach ( $property in $($xmlDOC.selectSingleNode("//ex:x36020003") | get-member)) { Write-Host $property } Write-Host `t Items: $xmlDOC.selectSingleNode("//ex:x36020003").text vs ` $(Get-PublicFolderStatistics -identity $PublicFolderPath).ItemCount.ToString(); Write-Host `t Bytes: $xmlDOC.selectSingleNode("//ex:x0e080014").text vs ` $(Get-PublicFolderStatistics -identity $PublicFolderPath).TotalItemSize.ToString().Replace("B",""); ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Item_Count")))))` .psBase.InnerText = $xmlDOC.selectSingleNode("//ex:x36020003").text; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Byte_Count")))))` .psBase.InnerText = $xmlDOC.selectSingleNode("//ex:x0e080014").text; } else { Write-Host `t Failed: $xmlHTTP.Status `n`t Actual HTTP path attempted: $PublicFolderWebPathProper if ($TimeCounter -eq 0) { Write-Host '`tTime expired while waiting for security permissions to replicate to $oldServer'; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Time_Error"))))).psBase.InnerText = ` 'Time expired while waiting for security permissions to replicate to $oldServer'; } if ($xmlHTTP.Status.ToString() -eq "404") { $string = "`t A 'Not Found' error was encountered. Please check the public folder ` `n`t " + $PublicFolderPath + "`n and make sure there are no trailing spaces. ` `n`t The easiest way to do this is to open Exchange System Manager ` `n`t on the Exchange 2000 server an navigate to 'Administrative ` `n`t Groups' -> 'First Administrative Group' -> 'Folders' -> ` `n`t 'Public Folders' -> " + $PublicFolderPath + ` "`n`n`t Before you continue running this script, I recommend that ` `n`t you visit [link=http://visualbasicscript.com/m_61934/tm.htm]http://visualbasicscript.com/m_61934/tm.htm[/link] and run ` `n`t this script to discover which Public Folders have trailing spaces. ` `n`n`t If the above does not work, try this method:` `n`t Using PFDavAdmin.exe, grant yourself 'Owner' Rights to the Public Folder ` `n`t then go to that folder within Outlook. Change the Name of the troublesome ` `n`t folder. Verify the name change in both Exchange 2000's Exchange System ` `n`t Manager and Exchange 2007 SP1's Public Folder Management Console. When ` `n`t both GUI's show the new name, then rerun this script. ` `n`t NOTE: I encountered one particular folder that had a period and a space ` `n`t at the end of its name ('. '). Outlook displayed the period but not the ` `n`t trailing space. Removing the period also removed the space in Exchange ` `n`t 2000's Exchange System Manager. Using Outlook to add the period back ` `n`t to the end of the Public Folder name also added the trailing space ` `n`t automatically. In the end, I decided to remove the trailing space ` `n`t and period to make sure it worked."; Write-Host $string; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("404_Error"))))).psBase.InnerText = $string; } $Error_Flag = $True; } $Catch_Output = $xmlFolder.AppendChild($xmlReplica); } # Look for the Access Control Entry for the user $ConnectAs and remove it. if ( ($Current_SPM -eq $Security_Permission_Mode_Add_Remove_As_Necessary) -or ` ($Current_SPM -eq $Security_Permission_Mode_Remove) ) { foreach ($ACE in $(get-publicfolderclientpermission -Identity $PublicFolderPath)) { #Write-Host `t ACE:. $([string]$ACE.User) #Write-Host `t User: $([string]$(get-user $ConnectAs).Identity) #Write-Host `t $([string]$ACE.User).CompareTo( $([string]$(get-user $ConnectAs).Identity) ) if ($([string]$ACE.User).CompareTo( $([string]$(get-user $ConnectAs).Identity) ) -eq 0) { ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Security_Action"))))).psBase.InnerText = ` "Removing all Access Permissions to " + $NewServer + " for " + $ConnectAs; Remove-PublicFolderClientPermission -Identity $PublicFolderPath -User $ACE.User -AccessRights $ACE.AccessRights -Confirm:$False; Write-Host `t Removing ACE for $ConnectAs; } } } Write-host ------------------------------- Write-host ------------------------------- $Catch_Output = $xmlFolder.AppendChild($xmlReplica); } # This code only centers upon $NewServer and $OldServer; which must be explicitly # defined at the beginning of the script. A good project for someone else would # be to modify this script to handle 3+ Exchange servers and NOT require $OldServer # be explicitly defined. if ( ($([string]$Replica).ToLower().Contains($NewServer.ToLower()) -eq $true)) { $xmlReplica = $xmlLog.CreateElement("Replica"); ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Server_Name"))))).psBase.InnerText = $NewServer; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Web_Address_Header"))))).psBase.InnerText = 'https://' + $NewServer; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Item_Count"))))).psBase.InnerText = ` $(Get-PublicFolderStatistics -identity $PublicFolderPath).ItemCount.ToString(); ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Byte_Count"))))).psBase.InnerText = ` $(Get-PublicFolderStatistics -identity $PublicFolderPath).TotalItemSize.ToString().Replace("B",""); $Catch_Output = $xmlFolder.AppendChild($xmlReplica); } $Catch_Output = $xmlLog.Public_Folders.AppendChild($xmlFolder); if (($Quit_On_Error -eq $True) -and ($Error_Flag -eq $True)) {exit;} } } Write-Host "Done Processing Folders.`nWriting Log to file."; $xmlTimeStamp = $xmlLog.CreateElement("TimeStamp"); ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Year"))))).psBase.InnerText = Get-Date -UFormat %Y; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Month"))))).psBase.InnerText = Get-Date -Format %M; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Day"))))).psBase.InnerText = Get-Date -Format %d; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Time"))))).psBase.InnerText = Get-Date -UFormat %T; $Catch_Output = $xmlFinish.AppendChild($xmlTimeStamp); $xmlLog.Save($(join-path -path $(get-location).tostring() ((get-date -UFormat ` "%Y %m %d %A %r Ex2000 vs Ex2007 Public Folders.xml").replace(" ","_").replace(",","").replace(":","_")))); Example of the XML log file: <?xml version="1.0" encoding="ISO-8859-1"?> <Public_Folders> <x /> <Run_Details> <Start> <TimeStamp> <Year>2008</Year> <Month>7</Month> <Day>14</Day> <Time>15:03:08</Time> </TimeStamp> </Start> <Finish> <TimeStamp> <Year>2008</Year> <Month>7</Month> <Day>14</Day> <Time>20:35:07</Time> </TimeStamp> </Finish> <Parameters> <Connect_As_Identity>UserID</Connect_As_Identity> <New_Server>Exchange2007</New_Server> <Old_Server>Exchange2000</Old_Server> <Security_Permission_Mode>No Changes</Security_Permission_Mode> </Parameters> </Run_Details> <Public_Folder ID="2"> <Name>Access</Name> <Path>\Access</Path> <Friendly_Web_Address>HTTP://OldServer/public/Access</Friendly_Web_Address> <Proper_Web_Address>HTTP://OldServer/public/Access</Proper_Web_Address> <Replica> <Server_Name>NewServer</Server_Name> <Web_Address_Header>https://NewServer</Web_Address_Header> <Item_Count>0</Item_Count> <Byte_Count>0</Byte_Count> </Replica> </Public_Folder> <Public_Folder ID="3"> <Name>Access Issues</Name> <Path>\Access\Access Issues</Path> <Friendly_Web_Address>HTTP://OldServer/public/Access/Access Issues</Friendly_Web_Address> <Proper_Web_Address>HTTP://OldServer/public/Access/Access%20Issues</Proper_Web_Address> <Replica> <Server_Name>OldServer</Server_Name> <Web_Address_Header>http://OldServer</Web_Address_Header> <Item_Count>26</Item_Count> <Byte_Count>529033</Byte_Count> </Replica> <Replica> <Server_Name>NewServer</Server_Name> <Web_Address_Header>https://NewServer</Web_Address_Header> <Item_Count>26</Item_Count> <Byte_Count>540999</Byte_Count> </Replica> </Public_Folder> ... To parse the output file and display Public Folders with mismatched counts: #--------------------==--------------------# #-------------- Description --------------# # # This script takes an input file (example shown below) and looks # for any Public Folders where the items counts for all the # replicas are not identical. #--------------- Input File ---------------# # # The input file is generated from the output of my PowerShell script # Compare_Public_Folders_Between_Ex2000_and_Ex2007.ps1. An excerpt of # this file is shown below. # <?xml version="1.0" encoding="ISO-8859-1"?> # <Public_Folders> # <x /> # <Run_Details> # <Start> # <TimeStamp> # <Year>2008</Year> # <Month>7</Month> # <Day>15</Day> # <Time>13:28:23</Time> # </TimeStamp> # </Start> # <Finish> # <TimeStamp> # <Year>2008</Year> # <Month>7</Month> # <Day>15</Day> # <Time>15:23:26</Time> # </TimeStamp> # </Finish> # <Parameters> # <Connect_As_Identiy>UserID</Connect_As_Identiy> # <New_Server>NewMailServer</New_Server> # <Old_Server>OldMailServer</Old_Server> # <Security_Permission_Mode>Add</Security_Permission_Mode> # </Parameters> # </Run_Details> # <Public_Folder ID="3"> # <Name>Access Issues</Name> # <Path>\Access\Access Issues</Path> # <Friendly_Web_Address>HTTP://OldMailServer/public/Access/Access Issues</Friendly_Web_Address> # <Proper_Web_Address>HTTP://OldMailServer/public/Access/Access%20Issues</Proper_Web_Address> # <Replica> # <Server_Name>cocsxchng01</Server_Name> # <Web_Address_Header>http://OldMailServer</Web_Address_Header> # </Replica> # <Replica> # <Server_Name>NewMailServer</Server_Name> # <Web_Address_Header>https://NewMailServer</Web_Address_Header> # <Item_Count>26</Item_Count> # <Byte_Count>540999</Byte_Count> # </Replica> # </Public_Folder> #----------------- Output -----------------# # # This is an example of the output that is displayed on screen. # # Item Count mismatch for folder # \Finance\Conference Call Calendar # OldMailServer : 71 # NewMailServer : 55 # Item Count mismatch for folder # \Marketing\Administration\Team A # OldMailServer : 2655 # NewMailServer : 2653 # Done. set-psdebug –strict; # Require declaration of powershell variables # Set-PSDebug -Off; # Do not require declaration of variables $strPathToFile = "U:\Development"; $strFileName = "XMLDoc.old.txt"; $xmlLog = [xml]( Get-Content (join-path -path $strPathToFile $strFileName)); $colPublicFolders = $xmlLog.Public_Folders.Public_Folder; foreach ($objPublicFolder in $colPublicFolders) { # If there was only one replica for a folder, then # ($objPublicFolder.Replica -is [array]) would return false if ($objPublicFolder.Replica -is [array]) { $Item_Count_for_First_Replica = $objPublicFolder.Replica[0].Item_Count; $colReplicas = $objPublicFolder.Replica; $mismatch = $False; # Compare the item count for each replica against # the first replica item count. foreach ($objReplica in $colReplicas) { If ($objReplica.Item_Count -ne $Item_Count_for_First_Replica) { $mismatch = $True; } } if ($mismatch -eq $True) { Write-Host "Item Count mismatch for folder`n`t" $objPublicFolder.Path; foreach ($objReplica in $colReplicas) { Write-Host "`t" $objReplica.Server_Name ": " $objReplica.Item_Count;} } } $colReplicas = $Null; $Item_Count_for_First_Replica = $Null; $objReplica = $Null; } $strPathToFile = $Null $strFileName = $Null $objPublicFolder = $Null; $colPublicFolders = $Null; $xmlLog = $Null; Write-Host "Done.";
#--------------------==--------------------# set-psdebug –strict # Require declaration of powershell variables # Set-PSDebug -Off # Do not require declaration of variables #------------ Exchange Servers ------------# # # Populate $NewServer and $OldServer with your # Exchange 2007 and Exchange 2000 server names. # Don't use <server name>.<domain>.com notation. $NewServer = "<Exchange 2007 server name>"; $OldServer = "<Exchange 2000 server name>"; #---------- Public Folder Access ----------# # # Please use a fresh account for this script. It does not need # any administrative rights but I did make it a member of the new # Exchange Public Folder Administrators security group. I # suggest a fresh account because access permissions to Public # Folders will be added AND removed for this account. You don't # want to screw up a legitimate production account, do you? This # account will also need a mailbox. $ConnectAs = "<domain>\<userID>"; $Password = "<password>"; #-------- Security Permission Mode --------# # # This section needs a little explanation before you're ready to use it. It it very likely # that $ConnectAs will be unable # to view every Public Folder hosted on $OldServer. At first this script would check permissions, add 'FolderVisible' if # necessary, wait for the permissions to replicate to $OldServer, then compare Public Folder size and item count between # $OldServer and $NewServer. This was it was taking upwards of 20 minutes for the security permission update to replicate # to $OldServer each and every time an Access Denied error was encountered. This is why I broke out the functionality into # four modes. Each mode has a description after it. I recommend that you run this script under $Security_Permission_Mode_Add # by setting $Current_SPM = $Security_Permission_Mode_Add; wait a couple hours or a day for Public Folder Replication to # work its magic; re-run this script using $Security_Permission_Mode_Add_Only_As_Necessary; then when you have all your results, # run it once more using $Security_Permission_Mode_Remove to clean up the security changes. $Security_Permission_Mode_Add = 1; # Don't perform any mailbox compares. Just add 'FolderVisible' permissions for $ConnectAs where necessary. $Security_Permission_Mode_Remove = 2; # Don't perform any mailbox compares. Just remove any 'FolderVisible' permissions for $ConnectAs. $Security_Permission_Mode_No_Changes = 3; # Compare Public Folders using the security permissions available at the time of the run. $Security_Permission_Mode_Add_Only_As_Necessary = 4; # Add 'FolderVisible' permissions for $ConnectAs where necessary, # wait for Public Folder Replication, then compare folders. $Security_Permission_Mode_Add_Remove_As_Necessary = 5; # Add 'FolderVisible' permissions for $ConnectAs where necessary, # wait for Public Folder Replication, compare folders, then remove added permissions. $Current_SPM = $Security_Permission_Mode_Add_Only_As_Necessary; $TimeConstant = [int32]1800; # How many seconds to wait for security permissions to replicate from $OldServer to $NewServer per folder. # This value is neede only during $Security_Permission_Mode_Add_Only_As_Necessary #-------------- Quit On Error -------------# # # If an error is encountered while attempting to # access the Public Folders via the web site hosted by # $OldServer, the script will halt or not based on the config below. # This flag is active during modes $Security_Permission_Mode_No_Changes, # $Security_Permission_Mode_Add_Only_As_Necessary, and # $Security_Permission_Mode_Add_Remove_As_Necessary. $Quit_On_Error = $True; function Check_Security([string]$PublicFolderHTTPFormat, [string]$UserID, [string]$Pass) { $strRequest = "<?xml version=""1.0"" ?><D:propfind xmlns:D=""DAV:"" xmlns:E=""http://schemas.microsoft.com/mapi/proptag/"">` <D:prop><E:x0e080014 /><E:x36020003 /></D:prop></D:propfind>"; $xmlHTTP = new-object -com msxml2.xmlhttp; #Write-Host `t Accessing: $PublicFolderHTTPFormat; #Write-Host `t UserID: $UserID; #Write-Host `t Password: $Pass; if ($PublicFolderHTTPFormat.EndsWith(".")) { $tempString = $PublicFolderHTTPFormat + "%20"; } else { $tempString = $PublicFolderHTTPFormat } $xmlHTTP.Open("PROPFIND", $tempString, $False, $UserID, $Pass); $xmlHTTP.setRequestHeader("Content-type:", "text/xml"); $xmlHTTP.setRequestHeader("Depth","0"); $xmlHTTP.send($strRequest); #Write-Host `t xmlHTTP.Status: $xmlHTTP.Status; return $xmlHTTP.Status; } [xml] $xmlLog = '<?xml version="1.0" encoding="ISO-8859-1"?> <Public_Folders><x /></Public_Folders>'; # Generate Run Details $xmlRunDetails = $xmlLog.CreateElement("Run_Details"); $xmlStart = $xmlLog.createelement("Start"); $xmlFinish = $xmlLog.createelement("Finish"); $xmlTimeStamp = $xmlLog.CreateElement("TimeStamp"); ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Year"))))).psBase.InnerText = Get-Date -UFormat %Y; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Month"))))).psBase.InnerText = Get-Date -Format %M; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Day"))))).psBase.InnerText = Get-Date -Format %d; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Time"))))).psBase.InnerText = Get-Date -UFormat %T; $xmlParameters = $xmlLog.CreateElement("Parameters"); ($xmlParameters.AppendChild(($($xmlLog.CreateElement("Connect_As_Identiy"))))).psBase.InnerText = $ConnectAs; ($xmlParameters.AppendChild(($($xmlLog.CreateElement("New_Server"))))).psBase.InnerText = $NewServer; ($xmlParameters.AppendChild(($($xmlLog.CreateElement("Old_Server"))))).psBase.InnerText = $OldServer; switch ($Current_SPM) { $Security_Permission_Mode_Add {($xmlParameters.AppendChild(($($xmlLog.CreateElement("Security_Permission_Mode"))))).psBase.InnerText = "Add";} $Security_Permission_Mode_Remove {($xmlParameters.AppendChild(($($xmlLog.CreateElement` ("Security_Permission_Mode"))))).psBase.InnerText = "Remove";} $Security_Permission_Mode_No_Changes {($xmlParameters.AppendChild` (($($xmlLog.CreateElement("Security_Permission_Mode"))))).psBase.InnerText = "No Changes";} $Security_Permission_Mode_Add_Only_As_Necessary {($xmlParameters.AppendChild` (($($xmlLog.CreateElement("Security_Permission_Mode"))))).psBase.InnerText = "Add Only As Necessary";} $Security_Permission_Mode_Add_Remove_As_Necessary {($xmlParameters.AppendChild` (($($xmlLog.CreateElement("Security_Permission_Mode"))))).psBase.InnerText = "Add Remove As Necessary";} } $Catch_Output = $xmlStart.AppendChild($xmlTimeStamp); $Catch_Output = $xmlRunDetails.AppendChild($xmlStart); $Catch_Output = $xmlRunDetails.AppendChild($xmlFinish); $Catch_Output = $xmlRunDetails.AppendChild($xmlParameters); $Catch_Output = $xmlLog.Public_Folders.AppendChild($xmlRunDetails); $intCounter = 0; Get-PublicFolder -recurse | foreach { if ($_.ParentPath -ne $null) { if ([int]$($_.ParentPath.get_length()) -gt 1) { $PublicFolderPath = $_.ParentPath + "\" + $_.name } else { $PublicFolderPath = "\" + $_.name } } else { $PublicFolderPath = "\" + $_.name } # NOTE: Microsoft allows the character "/" to be used in Public Folder Names. # Try to reference a Public Folder named "Pizza/Toppings" via HTTP. You can't. The web server # will come back with a HTTP 409 error. So we have to do a replace with "_xF8FF_". $PublicFolderWebPath = "HTTP://" + $OldServer + "/public" + $PublicFolderPath.replace("/","_xF8FF_").replace("\","/"); $PublicFolderWebPathProper = $PublicFolderWebPath.replace(" ","%20"); $ReplicaSet = $_.Replicas; $intCounter += 1; $xmlFolder = $xmlLog.CreateElement("Public_Folder"); $xmlFolder.SetAttribute("ID",[string]$intCounter); ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Name"))))).psBase.InnerText = $_.name; ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Path"))))).psBase.InnerText = $PublicFolderPath; ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Friendly_Web_Address"))))).psBase.InnerText = $PublicFolderWebPath; ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Proper_Web_Address"))))).psBase.InnerText = $PublicFolderWebPathProper; Write-Host $_.name; $Error_Flag = $False; forEach ( $Replica in $ReplicaSet ) { if ( ($([string]$Replica).ToLower().Contains($OldServer.ToLower()) -eq $true)) { # The code that deals with XML came from this location: # [link=http://www.infinitec.de/post/2004/12/Retrieve-the-size-of-a-Microsoft-Exchange-20002003-public-folder.aspx]` http://www.infinitec.de/post/2004/12/Retrieve-the-size-of-a-Microsoft-Exchange-20002003-public-folder.aspx[/link] # It was written by Henning Krause. Many thanks for the excelent code. # Visit Henning's Blog at [link=http://www.infinitec.de/author/hkrause.aspx]http://www.infinitec.de/author/hkrause.aspx[/link] # I translated it into PowerShell # A list of additional (and sometimes available) MAPI properties are available at: # [link=http://svn.opengroupware.org/OGoProjects/evolution-groupdav/trunk/utils/mapi-properties]` http://svn.opengroupware.org/OGoProjects/evolution-groupdav/trunk/utils/mapi-properties[/link] $strRequest = "<?xml version=""1.0"" ?><D:propfind xmlns:D=""DAV:"" xmlns:E=""http://schemas.microsoft.com/mapi/proptag/""><D:prop><E:x0e080014 /><E:x36020003 /></D:prop></D:propfind>"; $xmlHTTP = new-object -com msxml2.xmlhttp; $xmlDOC = new-object -com msxml2.DOMDocument; $return = $xmlDOC.loadXML($strRequest); Write-Host Inspecting: $PublicFolderWebPath; $xmlReplica = $xmlLog.CreateElement("Replica"); ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Server_Name"))))).psBase.InnerText = $OldServer; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Web_Address_Header"))))).psBase.InnerText = 'http://' + $OldServer; # I was getting a lot of HTTP 401 Access Denied errors while attempting to access # some public folders. In the end, I discovered that this is because the account # I'm using to troll through the Public Folders does not have 'FolderVisible' # rights for that particular Public Folder after ALL the folder permissions have # been compiled and compared against the user $ConnectAs. # So I got the bright idea to check for a denied condition ("401" error) and # grant 'FolderVisible' access rights using the "Add-PublicFolderClientPermission" # cmdlet that you'll see a couple lines below. Initially I didn't use the "-Server" # parameter. This caused some heartache. The security changes were being made on # $NewServer and would eventually take effect on $OldServer through the Public # Folder replication. This was not fast enough. I tried to grant rights on # $OldServer manually using the Exchange System Manager (remember: $OldServer is # running Exchange 2000) and after a few seconds, I got this email in my inbox: # 'A folder design conflict has occurred in "Public Folder Store (<$OldServer>)". # The design of this folder has been simultaneously modified on two or more # folder replicas. Only the set of changes made last have been saved.' # Having read this, I decided to attempt to use the "-Server $OldServer" parameter # of the cmdlet "Add-PublicFolderClientPermission". # This failed miserably since $OldServer is running Exchange 2000. My only option # left was to grant permissions on $NewServer then wait for the predefined replication # period before continuing. Through repeated testing, I found that I had to keep # keep extending the wait time stored in $Time for the security replication to take place. # Test to see if $ConnectAs can access $PublicFolderPath. # If not, add an Access Control Entry (ACE) into the Access Control Llist (ACL) # for $PublicFolderPath. $Time = $TimeConstant; $TimeCounter = $Time; If (( $(Check_Security $PublicFolderWebPathProper $ConnectAs $Password) -eq 401 ) -and ` (($Current_SPM -eq $Security_Permission_Mode_Add) -or ` ($Current_SPM -eq $Security_Permission_Mode_Add_Only_As_Necessary) -or ` ($Current_SPM -eq $Security_Permission_Mode_Add_Remove_As_Necessary)) ) { Write-Host `t Adding FolderVisible Access Permissions to $NewServer; ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Security_Action"))))).psBase.InnerText = ` "Adding FolderVisible Access Permissions to " + $NewServer + " for " + $ConnectAs; $return = Add-PublicFolderClientPermission -Identity $PublicFolderPath -User $ConnectAs -AccessRights "FolderVisible" -Confirm:$False; If (($Current_SPM -eq $Security_Permission_Mode_Add) -or ` ($Current_SPM -eq $Security_Permission_Mode_Add_Only_As_Necessary)) { $strActivity = 'Waiting up to ' + $Time.tostring() + ' Seconds for Public Folder replication to push permissions to ' + $OldServer; $Show = $True; While (($Time -gt 0) -and ($(Check_Security $PublicFolderWebPathProper $ConnectAs $Password) -eq 401)){ Write-Progress -Activity $strActivity -Status "Polling in 10 Second Intervals" -SecondsRemaining $TimeCounter -Completed $Show; Start-Sleep -Seconds 10; $TimeCounter = $TimeCounter - 10; if ($Show -eq $True) {$Show = $False} else {$Show = $True} } Write-Progress -Activity $strActivity -Status "Polling in 10 Second Intervals" ` -SecondsRemaining $Time; #-PercentComplete (0) -Completed:$True; Write-Progress -Activity $strActivity -Status "Polling in 10 Second Intervals" -Completed:$True; } } # We've passed through the section of code that makes security changes on the fly (if necessary). # Hopefully by this point everything is in order to allow a successful read attempt when we access # the HTTP directory that represents the Public Folder located on $OldServer. if ( ($Current_SPM -eq $Security_Permission_Mode_Add_Remove_As_Necessary) -or ` ($Current_SPM -eq $Security_Permission_Mode_Add_Only_As_Necessary) -or ` ($Current_SPM -eq $Security_Permission_Mode_No_Changes) ) { # Some of the folders in my organization ended with a period (".") and a space. # I tried removing just the space but that frequently didn't work; the space would come back. # If I removed both the space and the period, the change stuck. So when I encounter # a public folder that ends with a period, I append a space to the end of the URL. # Then IIS can access the URL. This is explained again in greater detail several # lines below. Search for "404_Error" and look for the big blob of text above it. if ($PublicFolderWebPathProper.EndsWith(".")) { $tempString = $PublicFolderWebPathProper + "%20"; } else { $tempString = $PublicFolderWebPathProper } $xmlHTTP.Open("PROPFIND", $tempString, $False, $ConnectAs, $Password); $xmlHTTP.setRequestHeader("Content-type:", "text/xml"); $xmlHTTP.setRequestHeader("Depth","0"); $xmlHTTP.send($strRequest); $xmlDOC = $xmlHTTP.ResponseXML; if ( $xmlHTTP.Status -eq 207 ) { $xmlDOC.SetProperty("SelectionNamespaces", "xmlns:ex='http://schemas.microsoft.com/mapi/proptag/'"); # To see the properties & methods for a single XML Node (Msxml2.DOMDocument), uncomment this next line # forEach ( $property in $($xmlDOC.selectSingleNode("//ex:x36020003") | get-member)) { Write-Host $property } Write-Host `t Items: $xmlDOC.selectSingleNode("//ex:x36020003").text vs ` $(Get-PublicFolderStatistics -identity $PublicFolderPath).ItemCount.ToString(); Write-Host `t Bytes: $xmlDOC.selectSingleNode("//ex:x0e080014").text vs ` $(Get-PublicFolderStatistics -identity $PublicFolderPath).TotalItemSize.ToString().Replace("B",""); ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Item_Count")))))` .psBase.InnerText = $xmlDOC.selectSingleNode("//ex:x36020003").text; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Byte_Count")))))` .psBase.InnerText = $xmlDOC.selectSingleNode("//ex:x0e080014").text; } else { Write-Host `t Failed: $xmlHTTP.Status `n`t Actual HTTP path attempted: $PublicFolderWebPathProper if ($TimeCounter -eq 0) { Write-Host '`tTime expired while waiting for security permissions to replicate to $oldServer'; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Time_Error"))))).psBase.InnerText = ` 'Time expired while waiting for security permissions to replicate to $oldServer'; } if ($xmlHTTP.Status.ToString() -eq "404") { $string = "`t A 'Not Found' error was encountered. Please check the public folder ` `n`t " + $PublicFolderPath + "`n and make sure there are no trailing spaces. ` `n`t The easiest way to do this is to open Exchange System Manager ` `n`t on the Exchange 2000 server an navigate to 'Administrative ` `n`t Groups' -> 'First Administrative Group' -> 'Folders' -> ` `n`t 'Public Folders' -> " + $PublicFolderPath + ` "`n`n`t Before you continue running this script, I recommend that ` `n`t you visit [link=http://visualbasicscript.com/m_61934/tm.htm]http://visualbasicscript.com/m_61934/tm.htm[/link] and run ` `n`t this script to discover which Public Folders have trailing spaces. ` `n`n`t If the above does not work, try this method:` `n`t Using PFDavAdmin.exe, grant yourself 'Owner' Rights to the Public Folder ` `n`t then go to that folder within Outlook. Change the Name of the troublesome ` `n`t folder. Verify the name change in both Exchange 2000's Exchange System ` `n`t Manager and Exchange 2007 SP1's Public Folder Management Console. When ` `n`t both GUI's show the new name, then rerun this script. ` `n`t NOTE: I encountered one particular folder that had a period and a space ` `n`t at the end of its name ('. '). Outlook displayed the period but not the ` `n`t trailing space. Removing the period also removed the space in Exchange ` `n`t 2000's Exchange System Manager. Using Outlook to add the period back ` `n`t to the end of the Public Folder name also added the trailing space ` `n`t automatically. In the end, I decided to remove the trailing space ` `n`t and period to make sure it worked."; Write-Host $string; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("404_Error"))))).psBase.InnerText = $string; } $Error_Flag = $True; } $Catch_Output = $xmlFolder.AppendChild($xmlReplica); } # Look for the Access Control Entry for the user $ConnectAs and remove it. if ( ($Current_SPM -eq $Security_Permission_Mode_Add_Remove_As_Necessary) -or ` ($Current_SPM -eq $Security_Permission_Mode_Remove) ) { foreach ($ACE in $(get-publicfolderclientpermission -Identity $PublicFolderPath)) { #Write-Host `t ACE:. $([string]$ACE.User) #Write-Host `t User: $([string]$(get-user $ConnectAs).Identity) #Write-Host `t $([string]$ACE.User).CompareTo( $([string]$(get-user $ConnectAs).Identity) ) if ($([string]$ACE.User).CompareTo( $([string]$(get-user $ConnectAs).Identity) ) -eq 0) { ($xmlFolder.AppendChild(($($xmlLog.CreateElement("Security_Action"))))).psBase.InnerText = ` "Removing all Access Permissions to " + $NewServer + " for " + $ConnectAs; Remove-PublicFolderClientPermission -Identity $PublicFolderPath -User $ACE.User -AccessRights $ACE.AccessRights -Confirm:$False; Write-Host `t Removing ACE for $ConnectAs; } } } Write-host ------------------------------- Write-host ------------------------------- $Catch_Output = $xmlFolder.AppendChild($xmlReplica); } # This code only centers upon $NewServer and $OldServer; which must be explicitly # defined at the beginning of the script. A good project for someone else would # be to modify this script to handle 3+ Exchange servers and NOT require $OldServer # be explicitly defined. if ( ($([string]$Replica).ToLower().Contains($NewServer.ToLower()) -eq $true)) { $xmlReplica = $xmlLog.CreateElement("Replica"); ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Server_Name"))))).psBase.InnerText = $NewServer; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Web_Address_Header"))))).psBase.InnerText = 'https://' + $NewServer; ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Item_Count"))))).psBase.InnerText = ` $(Get-PublicFolderStatistics -identity $PublicFolderPath).ItemCount.ToString(); ($xmlReplica.AppendChild(($($xmlLog.CreateElement("Byte_Count"))))).psBase.InnerText = ` $(Get-PublicFolderStatistics -identity $PublicFolderPath).TotalItemSize.ToString().Replace("B",""); $Catch_Output = $xmlFolder.AppendChild($xmlReplica); } $Catch_Output = $xmlLog.Public_Folders.AppendChild($xmlFolder); if (($Quit_On_Error -eq $True) -and ($Error_Flag -eq $True)) {exit;} } } Write-Host "Done Processing Folders.`nWriting Log to file."; $xmlTimeStamp = $xmlLog.CreateElement("TimeStamp"); ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Year"))))).psBase.InnerText = Get-Date -UFormat %Y; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Month"))))).psBase.InnerText = Get-Date -Format %M; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Day"))))).psBase.InnerText = Get-Date -Format %d; ($xmlTimeStamp.AppendChild(($($xmlLog.CreateElement("Time"))))).psBase.InnerText = Get-Date -UFormat %T; $Catch_Output = $xmlFinish.AppendChild($xmlTimeStamp); $xmlLog.Save($(join-path -path $(get-location).tostring() ((get-date -UFormat ` "%Y %m %d %A %r Ex2000 vs Ex2007 Public Folders.xml").replace(" ","_").replace(",","").replace(":","_")))); Example of the XML log file: <?xml version="1.0" encoding="ISO-8859-1"?> <Public_Folders> <x /> <Run_Details> <Start> <TimeStamp> <Year>2008</Year> <Month>7</Month> <Day>14</Day> <Time>15:03:08</Time> </TimeStamp> </Start> <Finish> <TimeStamp> <Year>2008</Year> <Month>7</Month> <Day>14</Day> <Time>20:35:07</Time> </TimeStamp> </Finish> <Parameters> <Connect_As_Identity>UserID</Connect_As_Identity> <New_Server>Exchange2007</New_Server> <Old_Server>Exchange2000</Old_Server> <Security_Permission_Mode>No Changes</Security_Permission_Mode> </Parameters> </Run_Details> <Public_Folder ID="2"> <Name>Access</Name> <Path>\Access</Path> <Friendly_Web_Address>HTTP://OldServer/public/Access</Friendly_Web_Address> <Proper_Web_Address>HTTP://OldServer/public/Access</Proper_Web_Address> <Replica> <Server_Name>NewServer</Server_Name> <Web_Address_Header>https://NewServer</Web_Address_Header> <Item_Count>0</Item_Count> <Byte_Count>0</Byte_Count> </Replica> </Public_Folder> <Public_Folder ID="3"> <Name>Access Issues</Name> <Path>\Access\Access Issues</Path> <Friendly_Web_Address>HTTP://OldServer/public/Access/Access Issues</Friendly_Web_Address> <Proper_Web_Address>HTTP://OldServer/public/Access/Access%20Issues</Proper_Web_Address> <Replica> <Server_Name>OldServer</Server_Name> <Web_Address_Header>http://OldServer</Web_Address_Header> <Item_Count>26</Item_Count> <Byte_Count>529033</Byte_Count> </Replica> <Replica> <Server_Name>NewServer</Server_Name> <Web_Address_Header>https://NewServer</Web_Address_Header> <Item_Count>26</Item_Count> <Byte_Count>540999</Byte_Count> </Replica> </Public_Folder> ... To parse the output file and display Public Folders with mismatched counts: #--------------------==--------------------# #-------------- Description --------------# # # This script takes an input file (example shown below) and looks # for any Public Folders where the items counts for all the # replicas are not identical. #--------------- Input File ---------------# # # The input file is generated from the output of my PowerShell script # Compare_Public_Folders_Between_Ex2000_and_Ex2007.ps1. An excerpt of # this file is shown below. # <?xml version="1.0" encoding="ISO-8859-1"?> # <Public_Folders> # <x /> # <Run_Details> # <Start> # <TimeStamp> # <Year>2008</Year> # <Month>7</Month> # <Day>15</Day> # <Time>13:28:23</Time> # </TimeStamp> # </Start> # <Finish> # <TimeStamp> # <Year>2008</Year> # <Month>7</Month> # <Day>15</Day> # <Time>15:23:26</Time> # </TimeStamp> # </Finish> # <Parameters> # <Connect_As_Identiy>UserID</Connect_As_Identiy> # <New_Server>NewMailServer</New_Server> # <Old_Server>OldMailServer</Old_Server> # <Security_Permission_Mode>Add</Security_Permission_Mode> # </Parameters> # </Run_Details> # <Public_Folder ID="3"> # <Name>Access Issues</Name> # <Path>\Access\Access Issues</Path> # <Friendly_Web_Address>HTTP://OldMailServer/public/Access/Access Issues</Friendly_Web_Address> # <Proper_Web_Address>HTTP://OldMailServer/public/Access/Access%20Issues</Proper_Web_Address> # <Replica> # <Server_Name>cocsxchng01</Server_Name> # <Web_Address_Header>http://OldMailServer</Web_Address_Header> # </Replica> # <Replica> # <Server_Name>NewMailServer</Server_Name> # <Web_Address_Header>https://NewMailServer</Web_Address_Header> # <Item_Count>26</Item_Count> # <Byte_Count>540999</Byte_Count> # </Replica> # </Public_Folder> #----------------- Output -----------------# # # This is an example of the output that is displayed on screen. # # Item Count mismatch for folder # \Finance\Conference Call Calendar # OldMailServer : 71 # NewMailServer : 55 # Item Count mismatch for folder # \Marketing\Administration\Team A # OldMailServer : 2655 # NewMailServer : 2653 # Done. set-psdebug –strict; # Require declaration of powershell variables # Set-PSDebug -Off; # Do not require declaration of variables $strPathToFile = "U:\Development"; $strFileName = "XMLDoc.old.txt"; $xmlLog = [xml]( Get-Content (join-path -path $strPathToFile $strFileName)); $colPublicFolders = $xmlLog.Public_Folders.Public_Folder; foreach ($objPublicFolder in $colPublicFolders) { # If there was only one replica for a folder, then # ($objPublicFolder.Replica -is [array]) would return false if ($objPublicFolder.Replica -is [array]) { $Item_Count_for_First_Replica = $objPublicFolder.Replica[0].Item_Count; $colReplicas = $objPublicFolder.Replica; $mismatch = $False; # Compare the item count for each replica against # the first replica item count. foreach ($objReplica in $colReplicas) { If ($objReplica.Item_Count -ne $Item_Count_for_First_Replica) { $mismatch = $True; } } if ($mismatch -eq $True) { Write-Host "Item Count mismatch for folder`n`t" $objPublicFolder.Path; foreach ($objReplica in $colReplicas) { Write-Host "`t" $objReplica.Server_Name ": " $objReplica.Item_Count;} } } $colReplicas = $Null; $Item_Count_for_First_Replica = $Null; $objReplica = $Null; } $strPathToFile = $Null $strFileName = $Null $objPublicFolder = $Null; $colPublicFolders = $Null; $xmlLog = $Null; Write-Host "Done.";