Monday, January 07, 2013

Cleaning out the Skeletons in your mailbox by deleting all the Empty folders with EWS and Get-MailboxFolderStatistics

One of the things you may find if your using Archive or Retention policies a lot is that you end up with skeleton folders in your mailbox. Which are basically the old folders trees that are all now empty because the items have been moved to the Archive or aged out of the Mailbox. To see your skeletons you can use the Get-MailboxFolderStatistics cmdlet and something like this to identify user created folders that are empty.

get-mailboxfolderstatistics $MailboxName | Where-Object{$_.FolderType -eq "User Created" -band $_.ItemsInFolderAndSubFolders -eq 0}

If you want to delete those folders there is no cmdlet to do this so you need to use EWS. Following on from my last post we can take the FolderId from Get-MailboxFolderStatistics convert this Id to a EWSId so we can then bind to the folder in EWS and then delete it using the Delete method.

I've put together a sample script to show how to do this by first using the Get-MailboxFolderStatistics and then in EWS ConvertId and Delete (a soft folder deletion).

The script itself isn't really intelligent in that it doesn't delete a Sub-folder before the Root folder if both are empty (it just depends they way they comes out). It just relies on the Try-Catch to ignore any errors and uses the EWS Folder TotalCount property as a secondary check.

As this script deletes things (some of which maybe be hard to recover if you don't want them deleted) you should do your own testing and use at your own risk.

I've put a download copy of this script here the code itself looks like

  1. ## Get the Mailbox to Access from the 1st commandline argument  
  3. $MailboxName = $args[0]  
  5. ## Load Managed API dll    
  6. Add-Type -Path "C:\Program Files\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll"    
  8. ## Set Exchange Version    
  9. $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2    
  11. ## Create Exchange Service Object    
  12. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
  14. ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials    
  16. #Credentials Option 1 using UPN for the windows Account    
  17. $psCred = Get-Credential    
  18. $creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())    
  19. $service.Credentials = $creds        
  21. #Credentials Option 2    
  22. #service.UseDefaultCredentials = $true    
  24. ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates    
  26. ## Code From  
  27. ## Create a compilation environment  
  28. $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider  
  29. $Compiler=$Provider.CreateCompiler()  
  30. $Params=New-Object System.CodeDom.Compiler.CompilerParameters  
  31. $Params.GenerateExecutable=$False  
  32. $Params.GenerateInMemory=$True  
  33. $Params.IncludeDebugInformation=$False  
  34. $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null  
  36. $TASource=@' 
  37.   namespace Local.ToolkitExtensions.Net.CertificatePolicy{ 
  38.     public class TrustAll : System.Net.ICertificatePolicy { 
  39.       public TrustAll() {  
  40.       } 
  41.       public bool CheckValidationResult(System.Net.ServicePoint sp, 
  42.         System.Security.Cryptography.X509Certificates.X509Certificate cert,  
  43.         System.Net.WebRequest req, int problem) { 
  44.         return true; 
  45.       } 
  46.     } 
  47.   } 
  48. '@   
  49. $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)  
  50. $TAAssembly=$TAResults.CompiledAssembly  
  52. ## We now create an instance of the TrustAll and attach it to the ServicePointManager  
  53. $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")  
  54. [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll  
  56. ## end code from  
  58. ## Set the URL of the CAS (Client Access Server) to use two options are availbe to use Autodiscover to find the CAS URL or Hardcode the CAS to use    
  60. #CAS URL Option 1 Autodiscover    
  61. $service.AutodiscoverUrl($MailboxName,{$true})    
  62. "Using CAS Server : " + $Service.url     
  64. #CAS URL Option 2 Hardcoded    
  66. #$uri=[system.URI] "https://casservername/ews/exchange.asmx"    
  67. #$service.Url = $uri      
  69. ## Optional section for Exchange Impersonation    
  71. #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)   
  73. function ConvertId{      
  74.     param (  
  75.             $OwaId = "$( throw 'OWAId is a mandatory Parameter' )"  
  76.           )  
  77.     process{  
  78.         $aiItem = New-Object Microsoft.Exchange.WebServices.Data.AlternateId        
  79.         $aiItem.Mailbox = $MailboxName        
  80.         $aiItem.UniqueId = $OwaId     
  81.         $aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::OwaId        
  82.         $convertedId = $service.ConvertId($aiItem, [Microsoft.Exchange.WebServices.Data.IdFormat]::EwsId)   
  83.         return $convertedId.UniqueId  
  84.     }  
  85. }  
  87. ## Get Empty Folders from EMS  
  89. get-mailboxfolderstatistics $MailboxName | Where-Object{$_.FolderType -eq "User Created" -band $_.ItemsInFolderAndSubFolders -eq 0} | ForEach-Object{  
  90.     # Bind to the Inbox Folder  
  91.     "Deleting Folder " + $_.FolderPath    
  92.     try{  
  93.         $folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId((Convertid $_.FolderId))     
  94.         $ewsFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)       
  95.         if($ewsFolder.TotalCount -eq 0){  
  96.             $ewsFolder.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::SoftDelete)  
  97.             "Folder Deleted"  
  98.         }  
  99.     }  
  100.     catch{  
  102.     }  
  103. }  


Anonymous said...

Hi Glen, thank you for the code samples.
However, we ran into some trouble with the convertID function and got an "corrupt data" exception when handing over the FolderID from get-mailboxfolderstatistics.
We had to change
$aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::OWAId
$aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::StoreId
which made it work perfectly.
Any ideas why this is needed? Folders were created by OWA 2010 and Outlook 2010. Regards, Robert

Glen Scales said...

Are you running from a Remote powershell session or directly in the Exchange Management Shell session. The difference is that you may have some issue if you try to use the Types directly vs just having a string in remote powershell. eg you may just need to use $_.FolderId.ToString(). In theory the Storied shouldn't work


Unknown said...

I love the script, it's listing all the empty folders but for some reason doesn't delete them. it says it deletes them but no joy :( - I've tried the OWAid to StoreId change and service impersonation to no avail.

Glen Scales said...

Two things i would try is I put a secondary check in to make sure that it would never delete a folder that had items in it so echo out

write-host $ewsFolder.TotalCount

The other thing would be to echo the exception if you getting one so change


Let me know how it goes


David Carr said...

Great script. I am sure our management would be happy with us using this to help our environment be clean.

Anonymous said...

Hi Glen,

I have modified your script to search for and delete a specific user created folder

get-mailboxfolderstatistics $MailboxName | Where-Object{$_.FolderType -eq "User Created" -band $_.Name -eq "Test"}

Also, I am using version 2.0 of the Managed API with Exchange 2010_SP2 and impersonation. Unfortunately the script is not working for me.

To troubleshoot the issue,I added the $_.Exception.Message to script here is the output:

Exception calling "ConvertId" with "2" argument(s): "The response received from the service didn't contain valid XML."

For additional troubleshooting, I also enabled EWS tracing $service.traceenabled = $true. Here is the trace output:

POST /EWS/Exchange.asmx HTTP/1.1
Content-Type: text/xml; charset=utf-8
Accept: text/xml
User-Agent: ExchangeServicesClient/15.00.0516.014
Accept-Encoding: gzip,deflate


HTTP/1.1 500 System.ServiceModel.ServiceActivationException
Persistent-Auth: true
Content-Length: 0
Cache-Control: private
Date: Fri, 03 May 2013 17:40:05 GMT
Server: Microsoft-IIS/7.5
X-AspNet-Version: 2.0.50727
X-Powered-By: ASP.NET

Non-textual response

Do you have any suggestions about why the script is getting this exception?

Thank you,
Dave Campbell

Glen Scales said...

Upload has been updated to reflect issues with URL encoded FolderId

Anonymous said...


I'm trying to run this against a mailbox in Office 365. When it hits the line:

$convertedId = $service.ConvertId($aiItem, [Microsoft.Exchange.WebServices.Data.IdFormat]::EwsId)

I get "exception calling convertid with "2" arguments".

Any ideas?

Thanks in advance,


Glen Scales said...

Are you using the update script from this URLEnocdes the Id's and it works okay for me against Office365


Anonymous said...

Assuming that is the one the download link points to, yes.

Anonymous said...

Nice. Just had a user with 33,000 folders or which 25,000 were empty (yes you read that correclty!)

Unknown said...

I could not get this working (2010 SP3), kept getting: error message Exception calling "ConvertId" with "2" argument(s): "The SMTP address format is invalid."

I thought I'd leave what I did for the next person, I used some old school VBA from within outlook, it was pretty effective, the below post pointed me in the right direction:

Anonymous said...

I'm trying to run this script against Exchange 2007 using EWS API 2.2. It seems to work up until the very end when it should perform the actual deletion. I updated the API DLL path to 2.2 (rather than 1.2), and changed the EWS version to Exchange2007_SP1. It says "Deleting folder " and has the right folder name displayed, but doesn't actually delete the folder.

Does any of the coding in the try{...} section change from EWS API 1.2 to 2.2? I've got hundreds of mailboxes with a folder I need to remove.