Monday, April 25, 2011

Using EWS to calculate the age of Items and affect of archive and retention policies in Exchange 2010

While there are plenty of cmdlets within the Exchange Management Shell to calculate the Mailbox and Folder sizes and even Item sizes within public folders there is no cmdlets you can currently use to look at the age of the content within a mailbox to see how old it is and what affect an retention policy may have for example you may want to know how much data will be shifted to an archive store(Search-Mailbox kind of does it but...). To look at Mailbox Content the EWS Managed API provides a easy entry point and the flexibility to do this . On Exchange 2010 to scan for content between a particular date range using a Content Index query via Exchange Search is the quickest and most efficient way of querying this data. The EWS Managed API allows you to perform a CI search via Exchange Search using the AQS querystring overload parameter of the FindItems method see http://msdn.microsoft.com/en-us/library/ee693615%28v=exchg.140%29.aspx.

With AQS the operator .. can be used to search between a range of values for example you could use it to search for Messages between 5 and 10 MB using a AQS String like System.Size:1mb..5mb. When searching for age based content you want to search for items between a particular date range for example to search for items 3 years old you could use 01/01/1990..01/01/2008.

What I've done is create a EWS Managed API script that put this all together it firstly does a query to get the folder hierarchy adding a few other useful properties like the folderpath and the current folder size then using measure-object this sums a collection of folder items based on the Item size. The script produces a CSV file of all the folders in a mailbox that contain items with the Daterange and what the size of those items is and what percentage of the total size of the mailbox folder is the output should look like



With this script the following two variables control the data range for the AQS query and the mailboxes to access

$Range = "01/01/1990..01/01/2008" 
$MailboxName = "user@domain.com"

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


  1. $Range = "01/01/1990..01/01/2008"  
  2. $MailboxName = "user@domain.com"  
  3.   
  4. $AQSString = "System.Message.DateReceived:" + $Range  
  5. $rptCollection = @()  
  6.   
  7.   
  8. function ConvertToString($ipInputString){  
  9.     $Val1Text = ""  
  10.     for ($clInt=0;$clInt -lt $ipInputString.length;$clInt++){  
  11.             $Val1Text = $Val1Text + [Convert]::ToString([Convert]::ToChar([Convert]::ToInt32($ipInputString.Substring($clInt,2),16)))  
  12.             $clInt++  
  13.     }  
  14.     return $Val1Text  
  15. }  
  16.   
  17.   
  18. $dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"  
  19. [void][Reflection.Assembly]::LoadFile($dllpath)  
  20. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010)  
  21. #$service.Credentials = New-Object System.Net.NetworkCredential("username","password")  
  22.   
  23. $windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()  
  24. $sidbind = "LDAP://<SID=" + $windowsIdentity.user.Value.ToString() + ">"  
  25. $aceuser = [ADSI]$sidbind  
  26. $service.AutodiscoverUrl($MailboxName,{$true})  
  27. $PR_FOLDER_TYPE = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(13825,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer);  
  28.   
  29. "Checking : " + $MailboxName   
  30. $folderidcnt = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName)  
  31. $fvFolderView =  New-Object Microsoft.Exchange.WebServices.Data.FolderView(1000)  
  32. $fvFolderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep;  
  33. $psPropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
  34. $PR_MESSAGE_SIZE_EXTENDED = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(3592,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Long);  
  35. $PR_DELETED_MESSAGE_SIZE_EXTENDED = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(26267,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Long);  
  36. $PR_Folder_Path = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(26293, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);  
  37.   
  38. $psPropertySet.Add($PR_MESSAGE_SIZE_EXTENDED);  
  39. $psPropertySet.Add($PR_Folder_Path);  
  40. $fvFolderView.PropertySet = $psPropertySet;  
  41. $sfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($PR_FOLDER_TYPE,"1")  
  42. $fiResult = $Service.FindFolders($folderidcnt,$sfSearchFilter,$fvFolderView)  
  43. foreach($ffFolder in $fiResult.Folders){  
  44.     "Processing : " + $ffFolder.displayName  
  45.     $TotalItemCount =  $TotalItemCount + $ffFolder.TotalCount;  
  46.     $FolderSize = $null;  
  47.     $FolderSizeValue = 0  
  48.     if ($ffFolder.TryGetProperty($PR_MESSAGE_SIZE_EXTENDED,[ref] $FolderSize))  
  49.     {  
  50.         $FolderSizeValue = [Int64]$FolderSize  
  51.     }  
  52.     $foldpathval = $null  
  53.     if ($ffFolder.TryGetProperty($PR_Folder_Path,[ref] $foldpathval))  
  54.     {  
  55.       
  56.     }  
  57.     $binarry = [Text.Encoding]::UTF8.GetBytes($foldpathval)  
  58.     $hexArr = $binarry | ForEach-Object { $_.ToString("X2") }  
  59.     $hexString = $hexArr -join ''  
  60.     $hexString = $hexString.Replace("FEFF""5C00")  
  61.     $fpath = ConvertToString($hexString)  
  62.     $fiFindItems = $null  
  63.     $ItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)  
  64.     $psPropertySet1 = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly)  
  65.     $psPropertySet1.Add([Microsoft.Exchange.WebServices.Data.ItemSchema]::Size)  
  66.     $ItemView.PropertySet  
  67.     $itemCollection = @()  
  68.     do{  
  69.         $fiFindItems = $ffFolder.findItems($AQSString,$ItemView)  
  70.         $ItemView.offset += $fiFindItems.Items.Count  
  71.         foreach($Item in $fiFindItems.Items){  
  72.             $rptobject = "" | select Size  
  73.             $rptobject.Size = $Item.Size  
  74.             $itemCollection +=$rptobject  
  75.         }  
  76.     }while($fiFindItems.MoreAvailable -eq $true)  
  77.     $outObj =  $itemCollection | Measure-Object Size -Sum -Average -Min -Max  
  78.     if($outObj -ne $null){  
  79.         Add-Member -InputObject $outObj -MemberType NoteProperty -Name Mailbox -Value $MailboxName  
  80.         Add-Member -InputObject $outObj NoteProperty -Name Folder -Value $ffFolder.DisplayName  
  81.         Add-Member -InputObject $outObj NoteProperty -Name FolderPath -Value $fpath  
  82.         Add-Member -InputObject $outObj NoteProperty -Name TotalFolderSize -Value $FolderSizeValue  
  83.         Add-Member -InputObject $outObj NoteProperty -Name DateRange -Value $Range  
  84.         $rptCollection += $outObj  
  85.     }  
  86. }  
  87. $rptCollection | select Mailbox,Folder,FolderPath,@{label="FolderSize(MB)";expression={[math]::Round($_.TotalFolderSize/1MB,2)}},@{label="RangeSize(MB)";expression={[math]::Round($_.Sum/1MB,2)}},@{label="RangeCount";expression={$_.Count}},@{label="PercentOfSize";expression={'{0:P0}' -f ($_.Sum/$_.TotalFolderSize)}} | export-csv c:\temp\MbAgeReport.csv -NoTypeInformation  

26 comments:

Alex said...

Can this script be modified to work with exchange 2007 sp3? I running into the following error.

Exception calling "FindItems" with "2" argument(s): "The parameter queryString is only valid for Exchange Server versio
n Exchange2010 or a later version."
At C:\mbagereport.ps1:70 char:33
+ $fiItems = $ffFolder.findItems <<<< ($AQSString,$ItemView)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

Is there something that I can replace so that it will work with exchange 2007?

Anonymous said...

I am looking to report on a count of emails modified by a certain date. Can this be done through EWS?

Anonymous said...

Great Script, I really like what it does.
I do have one question regarding Range Count, which seems to be limited by 1000 items. And if it is limited by 1000, is Range Size calculated based on the number of Range Count(items)? Also if the folder has more than a 1000 would any items over 1000 be calculated?
Thanks Again

Jason Parker said...

Awesome and perfect as usual Glen! Plan to use this for a client who needs to determine how much data is in each mailbox (older than xx and between xx and xx). Thanks!

Michael DeGrushe said...

How could I modify this script to run against the Archive mailbox instead?

Glen said...

Modify the line

$folderidcnt = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName)

to

$folderidcnt = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::ArchiveMsgFolderRoot,$MailboxName)

Cheers
Glen

Jason said...

I am newer to scripting etc, can this be ran for an entire DB?

I have been trying to find a good count of items that will be affected by retention rules. So far this is the best i have found!

Glen said...

Everything you do in Exchange is in the context of mailboxes there is no such thing a DB wide API's. So if you want to run it on each mailboxes within a store you need to run it individually for each mailbox or put it inside a Get-Mailbox loop etc

Jason said...

Thanks, I was able to get it to loop through all of the mailboxes.

Ratoka said...

This is exactly what I need, but I am having issues getting it to run. I have the EWS API installed, and am running the script from Exchange 2010 shell. I am receiving an error on $service.AutodiscoverUrl($MailboxName,{$true}):
Exception calling "AutodiscoverUrl" with "2" argument(s): "The Autodiscover service returned an error."

Can you provide any guidence on why this would happen?

Glen said...

It sounds like what your setting $MailboxName to isn't a property string try using the ToString() method when you do this eg

Get-Mailbox -ResultSize Unlimited | ForEach-Object{
$MailboxName = $_.PrimarySMTPAddress.ToString()
}

Ratoka said...

You're right. Now what is odd is that it will run against my user account, but no other accounts. Any other account shows this:

Exception calling "FindFolders" with "3" argument(s): "The specified object was not found in the store."
At C:\Users\dheath\Desktop\MailboxArchiveData.PS1:41 char:33
+ $fiResult = $Service.FindFolders <<<< ($folderidcnt,$sfSearchFilter,$fvFolderView)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

I tried uncommenting the $service.Credentials = New-Object System.Net.NetworkCredential("","") line and entered my own login info (full Exchange admin) but the same result.

Thank you for your help; I really appreciate it!

Glen said...

"The specified object was not found in the store."

This means you don't have access rights to the Mailbox your trying to access. You need to grant FullAccess to whatever account your using to those mailboxes via Add-MailboxPermission. (Exchange Administration rights have no affect eg you need to be able to access the other mailbox from within Outlook). The Other option is to use EWS Impersonation

Cheers
Glen

Ratoka said...

That was it exactly. Thank you so much for your help with this and for the excellent script!

Anonymous said...

Hi Glen,

Awesome script. Can you please tell me what to edit in the script if we wanted to just target a specific folder like "Inbox", "Sent Items", or "Deleted Items" ? Thank you.

Glen Scales said...

Have a look at http://gsexdev.blogspot.com.au/2012/10/item-age-sample-one-reporting-on-item.html which shows how to do reporting on one folder for multiple users.

Cheers
Glen

Anonymous said...

Thanks Glen. That script doesn't do the size calculation like the one this does. :) That is more useful for us.

Anonymous said...

Ok..got it to just do the Inbox now by using example from your other script. Now, I get this error. Our users have lots of items in their Inbox.

Exception calling "FindItems" with "2" argument(s): "The search operation could not be completed within the allotted time limit. Please try to narrow do
your scope to reduce the result set."
At D:\powershellscripts\retention\retentionstudy.ps1:70 char:47
+ $fiFindItems = $ffFolder.findItems <<<< ($AQSString,$ItemView)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

Glen Scales said...

That means the FastSearchTimeout in the throttling policy has been breached. Easiest way to fix that is just disable throttling on the admin user that is running the script eg http://docs.blackberry.com/en/admin/deliverables/12142/Disable_client_throttling_Exchange_10_963026_11.jsp (replace BESUser with your admin user)

Cheers
Glen

Anonymous said...

Will give that a try. Thanks again Glen.

Anonymous said...

Hello Glen,

If I choose a single folder like "Sent Items" by modifying the line like this:

$folderidcnt = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::SentItems,$MailboxName)


I get an error message:

Export-Csv : Could not find a part of the path 'C:\temp\MbAgeReport.csv'.
At D:\powershellscripts\retention\retentionstudy.ps1:88 char:334
+ $rptCollection | select Mailbox,Folder,FolderPath,@{label="FolderSize(MB)";expression={[math]::Round($_.TotalFolderSize/1MB,2)}},@{label="RangeSize(
MB)";expression={[math]::Round($_.Sum/1MB,2)}},@{label="RangeCount";expression={$_.Count}},@{label="PercentOfSize";expression={'{0:P0}' -f ($_.Sum/$_.Tota
lFolderSize)}} | export-csv <<<< c:\temp\MbAgeReport.csv -NoTypeInformation
+ CategoryInfo : OpenError: (:) [Export-Csv], DirectoryNotFoundException
+ FullyQualifiedErrorId : FileOpenFailure,Microsoft.PowerShell.Commands.ExportCsvCommand



I'm assuming this has something to do with the space between "Sent items" because it works fine for folder "Inbox". Any suggestions ?

Glen Scales said...

The error is indicating that there is problem with the Output path to the file eg there is no c:\temp directory

Cheers
Glen

Anonymous said...

Hi Glen,

Even after adding account to same throttling policy as our BES servers, I still get error:

"The request failed. The operation has timed out"

Glen Scales said...

The only thing you can really do is simplify the search and then do more processing at the client side. Eg your could also get rid of search and just page through ever item in the folder to build the same information list it will take longer but you also do a lot more at the same time in regards to calculations. eg http://gsexdev.blogspot.com.au/2012/10/reporting-on-item-age-count-and-size-in.html

Cheers
Glen

Anonymous said...

Hi Glen,
I'm trying to empty a subfolder of the contacts folder but I have an error, the user define is the script has full access on the mailboxes:
Exception calling "FindFolders" with "3" argument(s): "The request failed. The underlying connection was closed: An une
xpected error occurred on a send."
At L:\SYWI\MailProcs\ImportCrisisContacts\EmptyCrisisContactsFolder.ps1:51 char:42
+ $findFolderResults = $service.FindFolders <<<< ($ContactsFolderid,$SfSearchFilter,$fvFolderView)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException.
I'm using exchange 2010_SP2

Anonymous said...

Hi Glen,
I know you posted this four years ago but Im getting errors trying to run this on a 2008R2 box thats running Exchange 2010. I think the error is .Net related to the "loadfile" command:

Exception calling "LoadFile" with "1" argument(s): "The system cannot find the file specified. (Exception from HRESULT:
0x80070002)"
At C:\Users\a.admin\Desktop\mbagereport.ps1:19 char:38
+ [void][Reflection.Assembly]::LoadFile <<<< ($dllpath)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException