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  

Tuesday, April 05, 2011

Displaying the OOF log in a mailbox using Exchange Web Services and Powershell

On a Exchange 2007 and 2010 when changes are made to the Out of Office setting these changes are logged into an Item sitting in the Non_IPM_Subtree folder of a mailbox which has a message class of IPM.Microsoft.OOF.Log .

The log of changes is written to the body of this message and can be useful if for any reason you need a historical record of the oof setting for a particular mailbox. For instance if someone set an inappropriate OOF message and you need some evidence of this. You could also sequence this data to get picture over a certain time period of what users had their oof set and for how long it was enabled and did it say anything useful.

I've put together a script to show how to get at this log using the EWS Managed API and powershell and I've also included a parser to parse the log entries into an object to make it easier to export this data to csv or email etc.

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

$MailboxName = "user@domain"
$rptCollection = @()

$dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"
[void][Reflection.Assembly]::LoadFile($dllpath)
$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)
$service.TraceEnabled = $false


$windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$sidbind = "LDAP://<SID=" + $windowsIdentity.user.Value.ToString() + ">"
$aceuser = [ADSI]$sidbind
# $service.Credentials = New-Object System.Net.NetworkCredential("user","password")
$service.AutodiscoverUrl($MailboxName ,{$true})

$nonipmRoot = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root,$MailboxName)
$ivItemView = new-object Microsoft.Exchange.WebServices.Data.ItemView(2)
$sfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass, "IPM.Microsoft.OOF.Log")
$psPropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$psPropertySet.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text;
$fiItems = $service.finditems($nonipmRoot,$sfSearchFilter,$ivItemView)
foreach($itItem in $fiItems.Items){

$itItem.load($psPropertySet)
$itItem.Body.Text.Split("`r") | ForEach-Object{
$line = $_
if($line.indexof("Mailbox:")-gt 0){
$elen = $line.indexof("Mailbox:")
if($elen -gt 0){
$datetime = $line.SubString(1,$elen-3)
}
}
if($line.indexof("Mailbox:")-gt 0){
$slen = $line.indexof("Mailbox:")
$elen = $line.indexof("OofState:",$slen)
if($slen -gt 0){
$slen += 10
$mailbox = $line.SubString($slen,($elen-$slen)-3)
}
}
if($line.indexof("OofState:")-gt 0){
$slen = $line.indexof("OofState:")
$elen = $line.indexof("ExternalAudience:",$slen)
if($slen -gt 0){
$slen += 10
$OofState = $line.SubString($slen,($elen-$slen)-2)
}
}
if($line.indexof("ExternalAudience:")-gt 0){
$slen = $line.indexof("ExternalAudience:")
$elen = $line.indexof("InternalReply:",$slen)
if($slen -gt 0){
$slen += 18
$ExternalAudience = $line.SubString($slen,($elen-$slen)-2)
}
}
if($line.indexof("InternalReply:")-gt 0){
$slen = $line.indexof("InternalReply:")
$elen = $line.indexof("ExternalReply:",$slen)
if($slen -gt 0){
$slen += 15
$InternalReply = $line.SubString($slen,($elen-$slen)-2)
}
}
if($line.indexof("ExternalReply:")-gt 0){
$slen = $line.indexof("ExternalReply:")
$elen = $line.indexof("SetByLegacyClient:",$slen)
if($slen -gt 0){
$slen += 15
$ExternalReply = $line.SubString($slen,($elen-$slen)-2)
}
}
if($line.indexof("SetByLegacyClient:")-gt 0){
$slen = $line.indexof("SetByLegacyClient:")
$elen = $line.indexof("comment:")
if($slen -gt 0){
$slen += 20
$SetByLegacyClient = $line.SubString($slen,($elen-$slen)-3)
}
}
if($line.indexof("comment:")-gt 0){
$slen = $line.indexof("comment")
$elen = $line.indexof("'",$slen)
if($slen -gt 0){
$slen += 9
$comment = $line.SubString($slen,($elen-$slen))
}
}
$mbcomb = "" | select datetime,mailbox,OofState,ExternalAudience,InternalReply,ExternalReply,SetByLegacyClient,comment
$mbcomb.datetime = $datetime
$mbcomb.mailbox = $mailbox
$mbcomb.OofState = $OofState
$mbcomb.ExternalAudience = $ExternalAudience
$mbcomb.InternalReply = $InternalReply
$mbcomb.ExternalReply = $ExternalReply
$mbcomb.SetByLegacyClient = $SetByLegacyClient
$mbcomb.comment = $comment
$rptCollection += $mbcomb
}
}
$rptCollection

Saturday, April 02, 2011

Reading custom MAPI properties in a Transport Agent

In Exchange 2007 and 2010 all mail that is sent and received must transit its way through a Hub server where if your a developer you can create a transport agent to perform additional tasks on those messages before the are passed out of your org or into a mailbox. Sometimes these tasks may involve custom mapi properties that you may have created by using customized forms in Outlook or OWA or possibly a custom EWS application. When it comes to accessing these custom properties in a Transport Agent it involves using the TNEFReader in a transport agent to parse TNEF part of a message.

TNEF for those uninitiated stands for Transport Neutral Encapsulation Format which is a serialization format that Exchange uses to send the Mapi properties of a Item via email there is a protocol document that covers this http://msdn.microsoft.com/en-us/library/cc425498%28v=EXCHG.80%29.aspx. What the TNEFReader does is allows a Transport Agent to parse all the different property structures and data-types contained within the TNEF data stream.

One of the harder things to understand when dealing with Mapi properties is that they come in two different types. The following http://msdn.microsoft.com/en-us/library/cc979184.aspx page describes the difference between tagged properties and named properties. And there are different properties for different parts of a messages for instance there are Item properties for the Item, Recipient properties in the recipients collection for each recipients and attachment properties for each attachment. So when you are parsing an Item its not just as simple as reading one particular property collection (depending on what your trying to do).

For custom properties these should always be named properties and for the most they will use the PS_PUBLIC_STRINGS property set (but you may have created your own custom propset GUID) and either a long ID (LID) or String value for the PropertyName. This can all be confirmed using a Mapi Editor like MFCMapi or OutlookSpy to look at an item where this property has been set.

When you have gathered all this information your ready to use the property in your Transport Agent. The first thing to do when parsing a TNEF part in a transport agent is to work out at what level or properties you want to parse eg the Message properties or the attachment properties or possibly an embedded attachments properties (an Item attachment). The TNEFReader parses the properties in a forward direction to test if a property is a named property you use the

reader.PropertyReader.IsNamedProperty

If you want to check the propset GUID

reader.PropertyReader.PropertyNameId.PropertySetGuid

To see if this property is using a LID or String

reader.PropertyReader.PropertyNameId.Kind == TnefNameIdKind.Name

To demonstrate this I've put together a sample that pushes a particular custom property into a x-header which i find it useful for a number of things. I've put a download of the code here the agent looks like

using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.Diagnostics;
using Microsoft.Exchange.Data.Transport;
using Microsoft.Exchange.Data.Mime;
using Microsoft.Exchange.Data.ContentTypes.Tnef;
using Microsoft.Exchange.Data.Transport.Email;
using Microsoft.Exchange.Data.Transport.Routing;
using Microsoft.Exchange.Data.Common;
using System.IO;
using Microsoft.Win32;
using System.Reflection;

namespace ExchangeRoutingAgents
{
public class MapiPropAgentFactory : RoutingAgentFactory
{
public override RoutingAgent CreateAgent(SmtpServer server)
{
RoutingAgent mpAgent = new MapiPropAgent();
return mpAgent;
}
}
}

public class MapiPropAgent : RoutingAgent
{
public MapiPropAgent()
{
this.OnRoutedMessage += new RoutedMessageEventHandler(MapiPropAgent_OnRoutedMessage);
}
void MapiPropAgent_OnRoutedMessage(RoutedMessageEventSource esEvtsource, QueuedMessageEventArgs qmQueuedMessage)
{
String myPropString = "myExtraInfo";
MimePart tnefPart = qmQueuedMessage.MailItem.Message.TnefPart;
if (tnefPart != null)
{
//Check the Mimeheader to see if the X-header exists
MimeDocument mdMimeDoc = qmQueuedMessage.MailItem.Message.MimeDocument;
HeaderList hlHeaderlist = mdMimeDoc.RootPart.Headers;
Header myPropHeader = hlHeaderlist.FindFirst(myPropString);
TnefReader reader = new TnefReader(tnefPart.GetContentReadStream(), 0, TnefComplianceMode.Loose);
while (reader.ReadNextAttribute())
{
//Find Message Level TNEF attributes
if (reader.AttributeTag == TnefAttributeTag.MapiProperties)
{

while (reader.PropertyReader.ReadNextProperty())
{
if (reader.PropertyReader.IsNamedProperty)
{
if (reader.PropertyReader.PropertyNameId.Name == myPropString)
{

String myPropStringValue = reader.PropertyReader.ReadValueAsString();
if (myPropHeader == null)
{
MimeNode lhLasterHeader = hlHeaderlist.LastChild;
TextHeader nhNewHeader = new TextHeader(myPropString, myPropStringValue);
hlHeaderlist.InsertBefore(nhNewHeader, lhLasterHeader);
lhLasterHeader = null;
nhNewHeader = null;
}
}
}

}

}
}
reader.Dispose();
}


}


}