This is the second of the Powershell script samples from my MEC talk last week. The idea behind this script is to track the movement of Items in a Mailbox between folders using Pull Notifications in EWS, you can read more about how Pull notifications work here . This script takes advantage of the fact that when you enable Single Item Recovery any deletes you make in a Mailbox (hard or soft) the Items aren't deleted rather moved to the Dumpster v2 RecoverableItems folders. So if you track all the move notification events you can track both the movement of Messages by rules, users or deletes. eg the results look like
I've created two different versions of this script, the first version subscribes to all folders in a Mailbox and runs continuously against the Mailbox and does a GetEvents request every minute to retrieve the latest events from all folders in a Mailbox and then logs that to file.
The second version just subscribes to the Inbox folder and saves the subscription information out to file when you run it the first time. Then the next time you run the script it will use the saved SubscriptionId and Watermark value to get all the events since the script was last run. Pull Subscriptions have a timeout of 1440 minutes (which is 1 day) so if you are going to run this version you need to run it with a maximum gap of 1 day. The idea of this second version was just to track inbox fan-out of messages rather then subscribing to all folders like the first version.
Both of these scripts use EWS Impersonation which must be configured for the account your going to run the script as. When you run the script you need to pass in the Mailbox to run against and the timeout value in minutes for subscription which needs to between 1 and 1440 eg
./pullSubTrackWm.ps1 jcool@msgdevelop.onmicrosoft.com 1440
I've put a download of both scripts here the code itself looks like
I've created two different versions of this script, the first version subscribes to all folders in a Mailbox and runs continuously against the Mailbox and does a GetEvents request every minute to retrieve the latest events from all folders in a Mailbox and then logs that to file.
The second version just subscribes to the Inbox folder and saves the subscription information out to file when you run it the first time. Then the next time you run the script it will use the saved SubscriptionId and Watermark value to get all the events since the script was last run. Pull Subscriptions have a timeout of 1440 minutes (which is 1 day) so if you are going to run this version you need to run it with a maximum gap of 1 day. The idea of this second version was just to track inbox fan-out of messages rather then subscribing to all folders like the first version.
Both of these scripts use EWS Impersonation which must be configured for the account your going to run the script as. When you run the script you need to pass in the Mailbox to run against and the timeout value in minutes for subscription which needs to between 1 and 1440 eg
./pullSubTrackWm.ps1 jcool@msgdevelop.onmicrosoft.com 1440
I've put a download of both scripts here the code itself looks like
- ## Get the Mailbox to Access from the 1st commandline argument
- $MailboxName = $args[0]
- $duration = $args[1]
- $sw = [system.diagnostics.stopwatch]::startNew()
- $Error.Clear()
- $Script:changeLog = "c:\temp\ChangeLog-$(get-date -f yyyy-MM-dd).csv";
- if(-Not(Test-Path -Path $Script:changeLog)){
- Add-Content -Path $Script:changeLog ("EventTime,MessageDateTimeReceived,Subject,MovedFrom,MovedTo,LastModifiedName")
- }
- #Add-Content -Path $Script:changeLog ("Start Log" + (Get-Date).ToString())
- ## Load Managed API dll
- Add-Type -Path "C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll"
- Add-Type -AssemblyName System.Runtime.Serialization
- ## Set Exchange Version
- $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2
- ## Create Exchange Service Object
- $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)
- ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials
- #Credentials Option 1 using UPN for the windows Account
- $psCred = Get-Credential
- $creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())
- $service.Credentials = $creds
- #Credentials Option 2
- #service.UseDefaultCredentials = $true
- ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates
- ## Code From http://poshcode.org/624
- ## Create a compilation environment
- $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider
- $Compiler=$Provider.CreateCompiler()
- $Params=New-Object System.CodeDom.Compiler.CompilerParameters
- $Params.GenerateExecutable=$False
- $Params.GenerateInMemory=$True
- $Params.IncludeDebugInformation=$False
- $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null
- $TASource=@'
- namespace Local.ToolkitExtensions.Net.CertificatePolicy{
- public class TrustAll : System.Net.ICertificatePolicy {
- public TrustAll() {
- }
- public bool CheckValidationResult(System.Net.ServicePoint sp,
- System.Security.Cryptography.X509Certificates.X509Certificate cert,
- System.Net.WebRequest req, int problem) {
- return true;
- }
- }
- }
- '@
- $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)
- $TAAssembly=$TAResults.CompiledAssembly
- ## We now create an instance of the TrustAll and attach it to the ServicePointManager
- $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")
- [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll
- ## end code from http://poshcode.org/624
- ## 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
- #CAS URL Option 1 Autodiscover
- $service.AutodiscoverUrl($MailboxName,{$true})
- "Using CAS Server : " + $Service.url
- #CAS URL Option 2 Hardcoded
- #$uri=[system.URI] "https://casservername/ews/exchange.asmx"
- #$service.Url = $uri
- ## Optional section for Exchange Impersonation
- $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)
- #check Anchor header for Exchange 2013/Office365
- if($service.HttpHeaders.ContainsKey("X-AnchorMailbox")){
- $service.HttpHeaders["X-AnchorMailbox"] = $MailboxName
- }else{
- $service.HttpHeaders.Add("X-AnchorMailbox", $MailboxName);
- $service.HttpHeaders.Add("X-PreferServerAffinity","true");
- }
- "AnchorMailbox : " + $service.HttpHeaders["X-AnchorMailbox"]
- $FolderCollection = New-Object System.Collections.Hashtable
- #Define Function to convert String to FolderPath
- function ConvertToString($ipInputString){
- $Val1Text = ""
- for ($clInt=0;$clInt -lt $ipInputString.length;$clInt++){
- $Val1Text = $Val1Text + [Convert]::ToString([Convert]::ToChar([Convert]::ToInt32($ipInputString.Substring($clInt,2),16)))
- $clInt++
- }
- return $Val1Text
- }
- #Define Extended properties
- $PR_FOLDER_TYPE = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(13825,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer);
- $folderidcnt = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName)
- #Define the FolderView used for Export should not be any larger then 1000 folders due to throttling
- $fvFolderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1000)
- #Deep Transval will ensure all folders in the search path are returned
- $fvFolderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep;
- $psPropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
- $PR_Folder_Path = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(26293, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);
- #Add Properties to the Property Set
- $psPropertySet.Add($PR_Folder_Path);
- $fvFolderView.PropertySet = $psPropertySet;
- #The Search filter will exclude any Search Folders
- $sfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($PR_FOLDER_TYPE,"1")
- $fiResult = $null
- #The Do loop will handle any paging that is required if there are more the 1000 folders in a mailbox
- do {
- $fiResult = $Service.FindFolders($folderidcnt,$sfSearchFilter,$fvFolderView)
- foreach($ffFolder in $fiResult.Folders){
- $foldpathval = $null
- #Try to get the FolderPath Value and then covert it to a usable String
- if ($ffFolder.TryGetProperty($PR_Folder_Path,[ref] $foldpathval))
- {
- $binarry = [Text.Encoding]::UTF8.GetBytes($foldpathval)
- $hexArr = $binarry | ForEach-Object { $_.ToString("X2") }
- $hexString = $hexArr -join ''
- $hexString = $hexString.Replace("FEFF", "5C00")
- $fpath = ConvertToString($hexString)
- }
- "FolderPath : " + $fpath
- $FolderCollection.Add($ffFolder.Id.UniqueId,$fpath)
- }
- $fvFolderView.Offset += $fiResult.Folders.Count
- }while($fiResult.MoreAvailable -eq $true)
- function GetEventsRequest{
- param (
- $SubscriptionId="$( throw 'SubscriptionId is a mandatory Parameter' )",
- $Watermark="$( throw 'Credentials is a mandatory Parameter' )",
- $ImpersonationHeader="$( throw 'ImpersonationHeader is a mandatory Parameter' )"
- )
- process{
- Write-Host($SubscriptionId)
- $request = @"
- <?xml version="1.0" encoding="utf-8"?>
- <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
- xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
- <soap:Header>
- <t:RequestServerVersion Version="Exchange2010_SP2"/>
- <t:ExchangeImpersonation>
- <t:ConnectingSID>
- <t:SmtpAddress>$ImpersonationHeader</t:SmtpAddress>
- </t:ConnectingSID>
- </t:ExchangeImpersonation>
- </soap:Header>
- <soap:Body>
- <GetEvents xmlns="http://schemas.microsoft.com/exchange/services/2006/messages">
- <SubscriptionId>$SubscriptionId</SubscriptionId>
- <Watermark>$Watermark</Watermark>
- </GetEvents>
- </soap:Body>
- </soap:Envelope>
- "@
- return $request
- }
- }
- $DeletionsID = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::RecoverableItemsDeletions,$MailboxName);
- $Deletions = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$DeletionsID)
- $evEvents = new-object Microsoft.Exchange.WebServices.Data.EventType[] 6
- $evEvents[0] = [Microsoft.Exchange.WebServices.Data.EventType]::Copied
- $evEvents[1] = [Microsoft.Exchange.WebServices.Data.EventType]::Created
- $evEvents[2] = [Microsoft.Exchange.WebServices.Data.EventType]::Deleted
- $evEvents[3] = [Microsoft.Exchange.WebServices.Data.EventType]::Modified
- $evEvents[4] = [Microsoft.Exchange.WebServices.Data.EventType]::Moved
- $evEvents[5] = [Microsoft.Exchange.WebServices.Data.EventType]::NewMail
- $psSub = ""
- if(Test-Path -Path ("c:\temp\" + $MailboxName + "PullSubWM.wm")){
- $psSub = Import-Csv -Path ("c:\temp\" + $MailboxName + "PullSubWM.wm")
- $request = GetEventsRequest -SubscriptionId $psSub.SubscriptionID -Watermark $psSub.Watermark -ImpersonationHeader $MailboxName
- $mbMailboxFolderURI = New-Object System.Uri($service.url)
- $wrWebRequest = [System.Net.WebRequest]::Create($mbMailboxFolderURI)
- $wrWebRequest.CookieContainer = New-Object System.Net.CookieContainer
- $wrWebRequest.KeepAlive = $false;
- $wrWebRequest.Useragent = "EWS Script"
- $wrWebRequest.Headers.Set("Pragma", "no-cache");
- $wrWebRequest.Headers.Set("Translate", "f");
- $wrWebRequest.Headers.Set("Depth", "0");
- $wrWebRequest.ContentType = "text/xml";
- $wrWebRequest.ContentLength = $expRequest.Length;
- $wrWebRequest.Timeout = 60000;
- $wrWebRequest.Method = "POST";
- $wrWebRequest.Credentials = $creds
- $bqByteQuery = [System.Text.Encoding]::ASCII.GetBytes($request);
- $wrWebRequest.ContentLength = $bqByteQuery.Length;
- $rsRequestStream = $wrWebRequest.GetRequestStream();
- $rsRequestStream.Write($bqByteQuery, 0, $bqByteQuery.Length);
- $rsRequestStream.Close();
- $wrWebResponse = $wrWebRequest.GetResponse();
- $rsResponseStream = $wrWebResponse.GetResponseStream()
- $sr = new-object System.IO.StreamReader($rsResponseStream);
- $rdResponseDocument = New-Object System.Xml.XmlDocument
- $rdResponseDocument.LoadXml($sr.ReadToEnd());
- $rdResponseDocument.Envelope.Body.GetEventsResponse.ResponseMessages.GetEventsResponseMessage.ResponseClass
- if($rdResponseDocument.Envelope.Body.GetEventsResponse.ResponseMessages.GetEventsResponseMessage.ResponseClass -eq "Error"){
- $rdResponseDocument.Envelope.Body.GetEventsResponse.ResponseMessages.GetEventsResponseMessage | fl
- Remove-Item ("c:\temp\" + $MailboxName + "PullSubWM.wm")
- Write-Host ("Removed old watermarkFile")
- }
- else{
- if($rdResponseDocument.Envelope.Body.GetEventsResponse.ResponseMessages.GetEventsResponseMessage.ResponseClass -eq "Success"){
- $pullSubSav = "" | Select Watermark,SubscriptionID
- $wmark2 = $rdResponseDocument.getElementsByTagName("t:Watermark")
- $pullSubSav.Watermark = $wmark2.Item(($wmark2.Count-1))."#text"
- $pullSubSav.SubscriptionID = $rdResponseDocument.Envelope.Body.GetEventsResponse.ResponseMessages.GetEventsResponseMessage.Notification.SubscriptionId
- $pullSubSav | Export-Csv -Path ("c:\temp\" + $MailboxName + "PullSubWM.wm") -NoTypeInformation
- Write-Host "Subscription saved"
- $movedEvents = $rdResponseDocument.getElementsByTagName("t:MovedEvent")
- if($movedEvents.Count -gt 0){
- try{
- for($intmc=0;$intmc -lt $movedEvents.Count;$intmc++){
- $item = [Microsoft.Exchange.WebServices.Data.Item]::Bind($service, $movedEvents.Item($intmc).ItemId.Id);
- $ItemEvt = "" | Select EventTime,MessageDateTimeReceived,Subject,MovedFrom,MovedTo,LastModifiedName
- $ItemEvt.EventTime = $movedEvents.Item($intmc).TimeStamp
- Write-Host ("Processing : " + $item.Subject)
- write-host ("Last Modified by :" + $item.LastModifiedName)
- $ItemEvt.Subject = $item.Subject
- $ItemEvt.LastModifiedName = $item.LastModifiedName
- $ItemEvt.MessageDateTimeReceived = $item.DateTimeReceived
- #$movedEvents.Item($intmc).OldItemId.Id
- if($FolderCollection.containsKey($movedEvents.Item($intmc).OldParentFolderId.Id)){
- Write-Host ("Moved From Folder " + $FolderCollection[$movedEvents.Item($intmc).OldParentFolderId.Id])
- $ItemEvt.MovedFrom = $FolderCollection[$movedEvents.Item($intmc).OldParentFolderId.Id]
- }
- if($FolderCollection.containsKey($movedEvents.Item($intmc).ParentFolderId.Id)){
- Write-Host ("Moved To Folder " + $FolderCollection[$movedEvents.Item($intmc).ParentFolderId.Id])
- $ItemEvt.MovedTo = $FolderCollection[$movedEvents.Item($intmc).ParentFolderId.Id]
- }
- else{
- if ($movedEvents.Item($intmc).ParentFolderId.Id -eq $Deletions.Id.UniqueId)
- {
- Write-Host ("Moved to Recoverable Items - Deleted Items");
- }
- $ItemEvt.MovedTo = "Moved to Recoverable Items - Deleted Items"
- }
- Add-Content -Path $Script:changeLog ($ItemEvt.EventTime + ",`"" + $ItemEvt.MessageDateTimeReceived + "`",`"" + $ItemEvt.Subject + "`"," + $ItemEvt.MovedFrom + "," + $ItemEvt.MovedTo + "," + $ItemEvt.LastModifiedName)
- }
- }
- catch{
- Write-Host ($Error | fl)
- $Error.Clear()
- }
- }
- }
- }
- }
- else{
- $InboxId = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)
- $fldArray = new-object Microsoft.Exchange.WebServices.Data.FolderId[] 1
- $fldArray[0] = $InboxId
- $pullsub = $service.SubscribeToPullNotifications($fldArray,$duration, $WaterMark, $evEvents);
- $gEvents = $pullsub.GetEvents();
- $pullSubSav = "" | Select Watermark,SubscriptionID
- $pullSubSav.Watermark = $pullsub.Watermark
- $pullSubSav.SubscriptionID = $pullsub.Id
- $pullSubSav | Export-Csv -Path ("c:\temp\" + $MailboxName + "PullSubWM.wm") -NoTypeInformation
- Write-Host "Subscription saved"
- }