Friday, September 30, 2011

Using EWS Streaming notifications in Powershell to trigger a script when new mail arrives in Exchange 2010 SP1

EWS Streaming notifications where a great addition to Exchange 2010 SP1 that went some way to filling that large emotional gap that all Exchange developers felt with the demise of Store Event sinks in Exchange 2010. There is some good information and samples of using Streaming notifications in .NET which are a good read at http://msdn.microsoft.com/en-us/library/hh312849%28v=exchg.140%29.aspx and http://blogs.msdn.com/b/exchangedev/archive/2010/12/22/working-with-streaming-notifications-by-using-the-ews-managed-api.aspx

Notifications in EWS are the method you can use to get real-time notifications of email arriving or a calendar appointments being created. To distill what happens with streaming notification in a few lines is that when you create a subscription request for a particular Exchange mailbox folder with a CAS server that connection remains open for a maximum time of 30 minutes (this bit is important). During the next 30 minutes if something happens in the folder your subscribed to the server can use the connection your app has opened to notify you that something has happened. If you understand how ActiveSync works the process is very similar using a hanging http request.  After 30 minutes you then need to re-establish the connection which re-establishes the notifications channel to your application. (this 30 minute period can be changed but 30 is the MAX)

To use this within Powershell requires using some of the event features added in version 2.0 in particular the Register-ObjectEvent cmdlet. The cmldet allows you to subscribe to the events that are generated by a Microsoft .NET Framework object the object in question in this context is the StreamingSubscriptionConnection. This object has a few events the main one that fires when a NewMail arrives or an Item is created is the "OnNotificationEvent". The other import event is the "OnDisconnect" event which will fire when the subscription period ends or something else happens on the CAS side. The third event is the OnSubscriptionError which you need to work with for error checking and reliability.

The following script uses Register-ObjectEvent to create a subscription that will start a process that will log the Subject of the Message that caused the notification to fire. What you receive from the server when a notification is fired is the ItemId of the object that fired it. To get more information about what caused the notification you need to bind to this Item Id. To achieve this the ExchangeService object is passed in via the MessageData and a normal EWS bind is done.

The OnDisconnect Event is also registered so when this event fires the StreamingSubscriptionConnection open method is called which re-establishes the registration for another 30 minutes.

Reliability as this is pretty simple example it just listens to one folder in a mailbox (you can change the subscription type to listen to all folder in a mailbox) and only works withing a currently opened powershell session. To create a reliable process that could be relied on in a business context requires that you put some form of maintenance routine around the Notification subscription which in fully blown .NET is a lot easier to handle so using powershell and notifications outside of prototyping isn't something I'd recommend. The other things to consider when using EWS notification is the default throttling limits in 2010 SP1. For EWS Subscriptions the maximum number subscription you can have by default is 20 Subs per account so if your looking a subscribing to say a large number of calendar folders your going to have problems with throttling once you go past 20 users (remembering throttling is enabled by default) this is particular acute if your trying to use ExchangeOnline where you can't change since (ouch).

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

  1. $MailboxName = "user@domain.com"  
  2. $dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"  
  3. [void][Reflection.Assembly]::LoadFile($dllpath)  
  4. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)  
  5. $service.TraceEnabled = $false  
  6.   
  7. $service.Credentials = New-Object System.Net.NetworkCredential("user@domain.com","password")  
  8. $service.AutodiscoverUrl($MailboxName ,{$true})  
  9.   
  10. $fldArray = new-object Microsoft.Exchange.WebServices.Data.FolderId[] 1  
  11. $Inboxid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)  
  12. $fldArray[0] = $Inboxid  
  13. $stmsubscription = $service.SubscribeToStreamingNotifications($fldArray, [Microsoft.Exchange.WebServices.Data.EventType]::NewMail)  
  14. $stmConnection = new-object Microsoft.Exchange.WebServices.Data.StreamingSubscriptionConnection($service, 30);  
  15. $stmConnection.AddSubscription($stmsubscription)  
  16. Register-ObjectEvent -inputObject $stmConnection -eventName "OnNotificationEvent" -Action {  
  17.     foreach($notEvent in $event.SourceEventArgs.Events){      
  18.         [String]$itmId = $notEvent.ItemId.UniqueId.ToString()  
  19.         $message = [Microsoft.Exchange.WebServices.Data.EmailMessage]::Bind($event.MessageData,$itmId)  
  20.         "Subject : " + $message.Subject + " " + (Get-Date) | Out-File c:\log2.txt -Append   
  21.     }   
  22. } -MessageData $service  
  23. Register-ObjectEvent -inputObject $stmConnection -eventName "OnDisconnect" -Action {$event.MessageData.Open()} -MessageData $stmConnection  
  24. $stmConnection.Open()  

Friday, September 16, 2011

Getting Size and Item details for single folders in Exchange 2003 via Powershell

While this is now something you can do in one line in Exchange 2007 and 2010 if your migrating or managing 2000/3 you may want to get the Size and Item Count details of one particular mailbox folder for all users on a server eg how big the Inbox,SentItems or Calendar folder is. In Exchange 2003 you could get the Mailbox Size via WMI but to get an individual folder size you need to access the Mailbox itself (eg you can use pfdavadmin etc). To do this via Power shell for all users on the server you first need an ADSI query to get all the mailboxes on a particular server then you can use with WebDAV via the virtual admin root or MAPI via RDO\Redemption  http://www.dimastr.com/redemption/ which works well in Powershell. I have two scripts that use show how do to this using each of these API's which I've posted a downloadable copy here

To run these scripts you need to use the Netbios name of the server as a commandline parameter the RDO version of the script looks like


$snServerName = $args[0]

$Global:rptCollection = @()

function QueryMailbox($mb){
$mb
$rdoSession = new-object -com Redemption.RDOSession
$rdoSession.LogonExchangeMailbox($SmtpAddress,$snServerName)
$calendar = $rdoSession.Stores.DefaultStore.GetDefaultFolder(9)




$Itmcnt = "" | select DisplayName,SMTPAddress,ItemCount,Size
$Itmcnt.DisplayName = $displayName
$Itmcnt.SMTPAddress = $SmtpAddress
$Itmcnt.ItemCount = $calendar.Items.Count
$Itmcnt.Size = [System.Math]::Round(($calendar.Fields(235405315) /1024),2)
$Global:rptCollection += $Itmcnt
$Itmcnt
$rdoSession.logoff()
}

function GetUsers(){
$root = [ADSI]'LDAP://RootDSE'
$cfConfigRootpath = "LDAP://" + $root.ConfigurationNamingContext.tostring()
$dfDefaultRootPath = "LDAP://" + $root.DefaultNamingContext.tostring()
$configRoot = [ADSI]$cfConfigRootpath
$dfRoot = [ADSI]$dfDefaultRootPath
$searcher = new-object System.DirectoryServices.DirectorySearcher($configRoot)
$searcher.Filter = '(&(objectCategory=msExchExchangeServer)(cn=' + $snServerName  + '))'
$searcher.PropertiesToLoad.Add("cn")
$searcher.PropertiesToLoad.Add("gatewayProxy")
$searcher.PropertiesToLoad.Add("legacyExchangeDN")
$searcher1 = $searcher.FindAll()
foreach ($server in $searcher1){
    $snServerEntry = New-Object System.DirectoryServices.directoryentry
        $snServerEntry = $server.GetDirectoryEntry()
    $snServerName = $snServerEntry.cn
    $snExchangeDN = $snServerEntry.legacyExchangeDN
}
$searcher.Filter = '(&(objectCategory=msExchRecipientPolicy)(cn=Default Policy))'
$searcher1 = $searcher.FindAll()
foreach ($recppolicies in $searcher1){
         $gwaddrrs = New-Object System.DirectoryServices.directoryentry
        $gwaddrrs = $recppolicies.GetDirectoryEntry()
    foreach ($address in $gwaddrrs.gatewayProxy){
        if($address.Substring(0,5) -ceq "SMTP:"){$dfAddress = $address.Replace("SMTP:@","")}
    }   
   
}
$arMbRoot = "https://" + $snServerName + "/exadmin/admin/" + $dfAddress + "/mbx/"
$gfGALQueryFilter =  "(&(&(&(& (mailnickname=*)(!msExchHideFromAddressLists=TRUE)(| (&(objectCategory=person)" `
+ "(objectClass=user)(msExchHomeServerName=" + $snExchangeDN + ")) )))))"
$dfsearcher = new-object System.DirectoryServices.DirectorySearcher($dfRoot)
$dfsearcher.Filter = $gfGALQueryFilter
$searcher2 = $dfsearcher.FindAll()
foreach ($uaUsers in $searcher2){
         $uaUserAccount = New-Object System.DirectoryServices.directoryentry
        $uaUserAccount = $uaUsers.GetDirectoryEntry()
    foreach ($address in $uaUserAccount.proxyaddresses){
        if($address.Substring(0,5) -ceq "SMTP:"){$uaAddress = $address.Replace("SMTP:","")}
    }
    $SmtpAddress = $uaAddress
    $displayName = $uaUserAccount.DisplayName[0]
    QueryMailbox($uaAddress)
}
}

GetUsers
$Global:rptCollection | Export-Csv -NoTypeInformation c:\mailbox.csv

Wednesday, September 07, 2011

Create Search Folders using the EWS Managed API in a Mailbox or Archive Store using Powershell

Search folders are one of the suite of Exchange search options you can use programatically or to provide users with different views of their mailbox data in Outlook or OWA. Essentially a search folder is like a regular mailbox folder, except that it contains only links to messages in other folders that meet the criteria set in the search filter restriction which means that search folders are great for nonchanging, nondynamic queries. Search folders like Search Filters make use of restrictions in Exchange rather then using the Content Index which means that this comes along with some compromises ,if you are going to use search folders (or a lot of search filters) defiantly have a read of  http://technet.microsoft.com/en-us/library/cc535025%28EXCHG.80%29.aspx .

To create Search Folders using the EWS Managed API its pretty simple for search folders to be visible in Outlook you need to use the SearchFolders WellKnownFolderName enumeration which in a Mailbox relates to the Finder folder in the Non_IPM_Subtree.

Eg the following will create a Search folder to find Items greater than 5 MB in a Mailbox

$MailboxName = "user@domain.com"

$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]::Exchange2010_SP1)
$service.TraceEnabled = $false

$service.Credentials = New-Object System.Net.NetworkCredential("user@domain.com","password")
$service.AutodiscoverUrl($MailboxName ,{$true})

$svFldid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::SearchFolders,$MailboxName)
$SearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsGreaterThan([Microsoft.Exchange.WebServices.Data.ItemSchema]::Size, (5*1024*1024));
$SearchFolder = new-object Microsoft.Exchange.WebServices.Data.SearchFolder($service);
$searchFolder.SearchParameters.RootFolderIds.Add([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot);
$searchFolder.SearchParameters.Traversal = [Microsoft.Exchange.WebServices.Data.SearchFolderTraversal]::Deep;
$searchFolder.SearchParameters.SearchFilter = $SearchFilter
$searchFolder.DisplayName = "Bigger then 5 MB";
$searchFolder.Save($svFldid);

If you want to create a Search folder in the Archive Store there's no enumeration for the SearchFolders root folder in the Archive Store so you first need to find the Finder folder in the Archive Store then you can use this when you create the Search Folder for example the following will create a Search folder to find Items greater than 5 MB in the Archive Store.

$MailboxName = "user@domain.com"

$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]::Exchange2010_SP1)
$service.TraceEnabled = $false

$service.Credentials = New-Object System.Net.NetworkCredential("user@domain.com","password")
$service.AutodiscoverUrl($MailboxName ,{$true})

$FolderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1);
$fsf = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, "Finder");
$ffResult = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::ArchiveRoot,$fsf, $FolderView)

$SearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsGreaterThan([Microsoft.Exchange.WebServices.Data.ItemSchema]::Size, (5*1024*1024));
$SearchFolder = new-object Microsoft.Exchange.WebServices.Data.SearchFolder($service);
$searchFolder.SearchParameters.RootFolderIds.Add([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::ArchiveMsgFolderRoot);
$searchFolder.SearchParameters.Traversal = [Microsoft.Exchange.WebServices.Data.SearchFolderTraversal]::Deep;
$searchFolder.SearchParameters.SearchFilter = $SearchFilter
$searchFolder.DisplayName = "Bigger then 5 MB";
$searchFolder.Save($ffResult.folders[0].Id);