Tuesday, August 20, 2013

Public Folder EWS How-To rollup Part 1

As Public Folders are now new again in Exchange 2013, I thought I'd put together a post that covered how you can go about accessing and doing certain things with Public folders via EWS and Powershell. Although the underlying way in which Public Folders are now delivered in Exchange 2013 is vastly different from previous version of Exchange the way in which you access them Programmatically in EWS hasn't changed since Exchange 2007. So if your worried about your 2007 EWS code not working against 2013 don't only some minor changes maybe needed. (although if you have WebDAV code your going to need to rewrite this).

Choosing the Right API Method for what you want to do

The big thing when your considering creating a Public Folder application is to understand and choose the correct API for what you want to do. Eg if you want to write an application that is going to manage Public Folders eg Size/creation/deletion/Mail-Enable and folders permissions then EWS or MAPI are not the correct API to use as these are client API's. For any admin style applications you should be using the Exchange Management Shell cmdlets which give you a fully functional entry point to do administration tasks and also more importantly the ability to assert Delegated Exchange Admin Rights. Which means you can manage the folder without the need to have explicit client permissions on the folders. When you using a client API like EWS or MAPI you will only be able to perform client based actions where you have rights granted on the DACL of that particular folder (or inherited client permissions from the Parent). Unlike WebDAV in 2003,2007 there is no Virtual Admin root with EWS where Delegated Admin rights can be asserted so the EMS cmdlets via Remote Powershell is the best solution for Admin Apps.

EWS Public Folder operations

There are no dedicated Public Folder operations in EWS, so everything you do is with a Subset of the existing Mailbox operations. There is a full list of EWS Operations that work with Public Folder and notes around the restriction in http://msdn.microsoft.com/en-us/library/exchange/jj945067%28v=exchg.150%29.aspx. One of the big improvements in 2013 is now the ability to use Content Indexes to query Public Folder content which includes the ability to scan Attachment content where an IFilter is available for that attachment type.

Getting the Public Folder Hierarchy

When you want to return the full folder hierarchy of  a Mailbox with EWS you use a FindFolder operation where you set the FolderView to tell the server to perform a deep traversal which means that all folders and subfolder will be queried. If you had more then 1000 folders in a Mailbox you would need to make multiple request to get all the folders back paged 1000 at a time to ensure you stayed under the throttling limits. With Public Folders because Deep Traversals aren't supported in a FindFolder operation it presents a challenge to getting the Full folder hierarchy.Those people who are migrating from a MAPI application where they have used "GetHierarchyTable" in MAPI, may find EWS a little irritating as there is no easy way of quickly returning a full folder hierarchy in a short time amount of time like MAPI. However its not all bad news, one example of how to get the hierarchy is the method used in OWA in 2013 which makes use of multiple FindFolder operations to show the Public Folder tree. OWA just does an on demand query when the user clicks each client node which means there is a slight pause while the operation completes. It just means living with a slightly less responsive UI.

In Powershell if you want to get the Full Folder hierarchy you would need to do multiple shallow traversals like the following example

  1. function ConvertToString($ipInputString){    
  2.     $Val1Text = ""    
  3.     for ($clInt=0;$clInt -lt $ipInputString.length;$clInt++){    
  4.             $Val1Text = $Val1Text + [Convert]::ToString([Convert]::ToChar([Convert]::ToInt32($ipInputString.Substring($clInt,2),16)))    
  5.             $clInt++    
  6.     }    
  7.     return $Val1Text    
  8. }   
  9.   
  10. function GetPublicFolders{  
  11.     param (  
  12.             $RootFolderId = "$( throw 'FolderId is a mandatory Parameter' )"  
  13.           )  
  14.     process{  
  15.         $RootFolderId  
  16.         $folderCollection = @()  
  17.         #Define Extended properties    
  18.         $PR_FOLDER_TYPE = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(13825,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer);    
  19.         #Define the FolderView used for Export should not be any larger then 1000 folders due to throttling    
  20.         $fvFolderView =  New-Object Microsoft.Exchange.WebServices.Data.FolderView(1000)    
  21.         #Deep Transval will ensure all folders in the search path are returned    
  22.         $fvFolderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Shallow;    
  23.         $psPropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)    
  24.         $PR_Folder_Path = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(26293, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);    
  25.         #Add Properties to the  Property Set    
  26.         $psPropertySet.Add($PR_Folder_Path);    
  27.         $fvFolderView.PropertySet = $psPropertySet;    
  28.         #The Search filter will exclude any Search Folders    
  29.         $sfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($PR_FOLDER_TYPE,"1")    
  30.         $fiResult = $null    
  31.         do {    
  32.             $fiResult = $Service.FindFolders($RootFolderId,$sfSearchFilter,$fvFolderView)    
  33.             foreach($ffFolder in $fiResult.Folders){    
  34.                 $foldpathval = $null    
  35.                 $folderCollection += $ffFolder  
  36.                 #Try to get the FolderPath Value and then covert it to a usable String     
  37.                 if ($ffFolder.TryGetProperty($PR_Folder_Path,[ref] $foldpathval))    
  38.                 {    
  39.                     $binarry = [Text.Encoding]::UTF8.GetBytes($foldpathval)    
  40.                     $hexArr = $binarry | ForEach-Object { $_.ToString("X2") }    
  41.                     $hexString = $hexArr -join ''    
  42.                     $hexString = $hexString.Replace("FEFF""5C00")    
  43.                     $fpath = ConvertToString($hexString)    
  44.                 }    
  45.                 "FolderPath : " + $fpath    
  46.                 if($ffFolder.ChildFolderCount -gt 0){  
  47.                     $Childfolders = GetPublicFolders -RootFolderId $ffFolder.Id  
  48.                     foreach($Childfolder in $Childfolders){  
  49.                         $folderCollection += $Childfolder  
  50.                     }  
  51.                 }  
  52.             }  
  53.             $fvFolderView.Offset += $fiResult.Folders.Count  
  54.         }while($fiResult.MoreAvailable -eq $true)    
  55.         return $folderCollection  
  56.     }  
  57. }  
  58.   
  59. $pfRoot = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::PublicFoldersRoot)    
  60. $folderHi = GetPublicFolders -RootFolderId $pfRoot  
  61. #Example Use
  62. foreach($fld in $folderHi){  
  63.     $fld.DisplayName  
  64. }  
Binding to a specific Public folder

If you want to bind to a specific public folder given a path as \FolderLevel1\Level2\Level3\Target you need to know the particular folders EWSId. There are a few ways of getting the FolderId if your using Outlook you could take the HexEntryId can get from OWA and convert that into an EWSId eg the following is an example of using the AlternatePublicFolderId to covert the HexEntryId to an EWSId

  1. function ConvertId{      
  2.     param (  
  3.             $HexId = "$( throw 'HexId is a mandatory Parameter' )"  
  4.           )  
  5.     process{  
  6.         $aiItem = New-Object Microsoft.Exchange.WebServices.Data.AlternatePublicFolderId       
  7.         $aiItem.FolderId = $HexId     
  8.         $aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::HexEntryId        
  9.         $convertedId = $service.ConvertId($aiItem, [Microsoft.Exchange.WebServices.Data.IdFormat]::EwsId)   
  10.         return $convertedId.UniqueId  
  11.     }  
  12. }  
The other method you can use is to search for your target Folder using multi find folder operations and by splitting the path you want to search for eg.

  1. function FolderIdFromPath{  
  2.     param (  
  3.             $FolderPath = "$( throw 'Folder Path is a mandatory Parameter' )"  
  4.           )  
  5.     process{  
  6.         ## Find and Bind to Folder based on Path    
  7.         #Define the path to search should be seperated with \    
  8.         #Bind to the MSGFolder Root    
  9.         $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::PublicFoldersRoot)     
  10.         $tfTargetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)    
  11.         #Split the Search path into an array    
  12.         $fldArray = $FolderPath.Split("\")  
  13.          #Loop through the Split Array and do a Search for each level of folder  
  14.         for ($lint = 1; $lint -lt $fldArray.Length; $lint++) {  
  15.             #Perform search based on the displayname of each folder level  
  16.             $fvFolderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(1)  
  17.             $SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName,$fldArray[$lint])  
  18.             $findFolderResults = $service.FindFolders($tfTargetFolder.Id,$SfSearchFilter,$fvFolderView)  
  19.             if ($findFolderResults.TotalCount -gt 0){  
  20.                 foreach($folder in $findFolderResults.Folders){  
  21.                     $tfTargetFolder = $folder                 
  22.                 }  
  23.             }  
  24.             else{  
  25.                 "Error Folder Not Found"   
  26.                 $tfTargetFolder = $null   
  27.                 break   
  28.             }      
  29.         }   
  30.         if($tfTargetFolder -ne $null){ 
  31.             return $tfTargetFolder.Id.UniqueId.ToString() 
  32.         } 
  33.     } 
  34. } 
  35.  
  36. #Example use 
  37. $fldId = FolderIdFromPath -FolderPath "\firstlevel\secondlevel\target"  
  38. $SubFolderId =  new-object Microsoft.Exchange.WebServices.Data.FolderId($fldId)  
  39. $PubFolder  = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$SubFolderId)  
  40. $PubFolder  
Once you have bound to the Folder you can then perform any of the normal Item operations which I covered in the following post 

In Part 2 I'll go into some more depth some of the harder aspects of Public Folders.

5 comments:

Queryingsql.com said...

Hi,

I want to apply a rule so that if any incoming mail with specified condition comes to a particuler exchange server mailID.I want to store that and assign a unique number as CARE001,care002........

Is it possible??
Plz help.

Glen Scales said...

Look at Inbox rules http://msdn.microsoft.com/en-us/library/exchange/hh298418%28v=exchg.140%29.aspx

Cheers
Glen

Anonymous said...

How to get Public Folder Server Name property. I checked in MAPI Tool but didnt find. Exchange server version : 2010

Which can help me find excat location of the folder in physical drive. So, I need physical server name.

Please help me.

Anonymous said...

I am having a different problem - I am having a EWS object folder - with uniqueid etc
and can't seem to be able to convert it into a path - I'd like to convert it back into hexid or straight into a path for a specific folder.
I can't seem to get that to work at all. My whole script is running perfectly but I can't get the paths in any way. I am stuck with EWS objects with their uniqueids and parentids etc, but I need paths in the output.

Glen Scales said...

What does your code look like ? it should work okay