Monday, July 23, 2012

Pending Moderation Summary Report Email EWS/Powershell Script

This is a follow-up script to my last post about Moderation , This script produces a summary email of all the Pending Moderation Request email's from an Approver's Mailbox. The Summary basically consists of a HTML table where the Subject is Hyper-linked via the "Outlook:" + HexID link so you can double click the subject to open the approval email in Outlook (as long you have rights to the Approvers mailbox).

To run the script there are three configurable variables

$MailboxName = "user@domain.com"  - This is the Mailbox where the Approval email you want to report on are located

$TimeFrame = (Get-Date).AddDays(-1) - This is the TimeFrame you want to query from eg this will just query for the last 24 hours

$ReportAddress = "user@domain.com" - This is the Email address the Reports will be sent to

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

  1. $MailboxName = "user@domain.com"  
  2. $TimeFrame = (Get-Date).AddDays(-1)  
  3. $ReportAddress = "user@domain.com"  
  4.   
  5. $rptcollection = @()  
  6. ## Load Managed API dll    
  7. Add-Type -Path "C:\Program Files\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll"    
  8.    
  9. ## Set Exchange Version    
  10. $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2    
  11.    
  12. ## Create Exchange Service Object    
  13. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
  14.    
  15. ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials    
  16.    
  17. #Credentials Option 1 using UPN for the windows Account    
  18. $psCred = Get-Credential    
  19. $creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())    
  20. $service.Credentials = $creds      
  21.    
  22. #Credentials Option 2    
  23. #service.UseDefaultCredentials = $true    
  24.    
  25. ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates    
  26.    
  27. ## Code From http://poshcode.org/624  
  28. ## Create a compilation environment  
  29. $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider  
  30. $Compiler=$Provider.CreateCompiler()  
  31. $Params=New-Object System.CodeDom.Compiler.CompilerParameters  
  32. $Params.GenerateExecutable=$False  
  33. $Params.GenerateInMemory=$True  
  34. $Params.IncludeDebugInformation=$False  
  35. $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null  
  36.   
  37. $TASource=@' 
  38.   namespace Local.ToolkitExtensions.Net.CertificatePolicy{ 
  39.     public class TrustAll : System.Net.ICertificatePolicy { 
  40.       public TrustAll() {  
  41.       } 
  42.       public bool CheckValidationResult(System.Net.ServicePoint sp, 
  43.         System.Security.Cryptography.X509Certificates.X509Certificate cert,  
  44.         System.Net.WebRequest req, int problem) { 
  45.         return true; 
  46.       } 
  47.     } 
  48.   } 
  49. '@   
  50. $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)  
  51. $TAAssembly=$TAResults.CompiledAssembly  
  52.  
  53. ## We now create an instance of the TrustAll and attach it to the ServicePointManager  
  54. $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")  
  55. [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll  
  56.  
  57. ## end code from http://poshcode.org/624  
  58.    
  59. ## 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.    
  61. #CAS URL Option 1 Autodiscover    
  62. $service.AutodiscoverUrl($MailboxName,{$true})    
  63. "Using CAS Server : " + $Service.url     
  64.     
  65. #CAS URL Option 2 Hardcoded    
  66.    
  67. #$uri=[system.URI] "https://casservername/ews/exchange.asmx"    
  68. #$service.Url = $uri      
  69.    
  70. ## Optional section for Exchange Impersonation    
  71.    
  72. #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)   
  73.   
  74. function ConvertId($EWSid){    
  75.     $aiItem = New-Object Microsoft.Exchange.WebServices.Data.AlternateId      
  76.     $aiItem.Mailbox = $MailboxName      
  77.     $aiItem.UniqueId = $EWSid   
  78.     $aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::EWSId;      
  79.     return $service.ConvertId($aiItem, [Microsoft.Exchange.WebServices.Data.IdFormat]::HexEntryId)     
  80. }    
  81.   
  82. $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)     
  83. $Inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$folderid)  
  84. $ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)   
  85. $SfClass = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass,"IPM.Note.Microsoft.Approval.Request")  
  86. $PidNameApprovalRequestor = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::InternetHeaders,"x-ms-exchange-organization-approval-requestor",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);    
  87. $VerbResponse = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,0x8524,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);    
  88. $PR_NORMALIZED_SUBJECT = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0E1D,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);       
  89. $PR_REPORT_TAG = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0031,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);  
  90.   
  91. $SfClass = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass,"IPM.Note.Microsoft.Approval.Request")  
  92. $Sfgt = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsGreaterThan([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived, $TimeFrame)  
  93.   
  94. $sfCollection = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::And);  
  95. $sfCollection.add($Sfgt)  
  96. $sfCollection.add($SfClass)  
  97.   
  98. $Propset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
  99. $Propset.add($PidNameApprovalRequestor)  
  100. $Propset.add($PR_NORMALIZED_SUBJECT)   
  101. $ivItemView.PropertySet = $Propset  
  102. #define Table  
  103. $rpReport = $rpReport + "<table><tr bgcolor=`"#95aedc`">"  
  104. $rpReport = $rpReport + "<td align=`"center`" style=`"width:15%;`" ><b>Recieved</b></td>"  
  105. $rpReport = $rpReport + "<td align=`"center`" style=`"width:20%;`" ><b>From</b></td>"  
  106. $rpReport = $rpReport + "<td align=`"center`" style=`"width:20%;`" ><b>To</b></td>"  
  107. $rpReport = $rpReport + "<td align=`"center`" style=`"width:40%;`" ><b>Subject</b></td>"  
  108. $rpReport = $rpReport + "<td align=`"center`" style=`"width:5%;`" ><b>Size(KB)</b></td>"  
  109. $rpReport = $rpReport + "</tr>"  
  110. #end   
  111.   
  112.   
  113. do{    
  114.     $fiResults = $Inbox.findItems($sfCollection,$ivItemView)  
  115.     foreach($Item in $fiResults.Items){   
  116.     $fromVal = $null  
  117.     if($Item.TryGetProperty($PidNameApprovalRequestor,[ref]$fromVal)){  
  118.         "From : " + $propval  
  119.     }  
  120.     $NormalSubject = $null  
  121.     [Void]$Item.TryGetProperty($PR_NORMALIZED_SUBJECT,[ref]$NormalSubject)  
  122.     "Recieved : " + $Item.DateTimeReceived  
  123.     "Subject : " + $NormalSubject  
  124.     $Item.Load()  
  125.     $Item.attachments[0].Load()  
  126.     $rpReport = $rpReport + " <tr>" + " "  
  127.     $rpReport = $rpReport + "<td>" + $Item.DateTimeReceived.ToString() + "</td>" + " "  
  128.     $rpReport = $rpReport + "<td>" + $fromVal + "</td>" + " "  
  129.     $rpReport = $rpReport + "<td>" + $Item.Attachments[0].Item.ToRecipients[0].Address + "</td>" + " "  
  130.     $cnvId = ConvertId($Item.Id.UniqueId)  
  131.     $rpReport = $rpReport + "<td><a href=`"outlook:" + $cnvId.UniqueId + "`">" + $NormalSubject + "</a></td>" + " "  
  132.     $rpReport = $rpReport + "<td>" + [Math]::Round($Item.Size/1KB,2) + "</td>" + " "  
  133.     $rpReport = $rpReport + "</tr>" + " "  
  134.       
  135.     }    
  136.     $ivItemView.Offset += $fiResults.Items.Count    
  137. }while($fiResults.MoreAvailable -eq $true)  
  138.   
  139. $rpReport = $rpReport + "</table>" + " "  
  140. $EmailMessage = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage($service)   
  141. #Set the Subject    
  142. $EmailMessage.Subject = "Moderation Pending Approvals"  
  143. #Add Recipients    
  144. $EmailMessage.ToRecipients.Add($ReportAddress)  
  145. $EmailMessage.Body = New-Object Microsoft.Exchange.WebServices.Data.MessageBody  
  146. $EmailMessage.Body.BodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::HTML  
  147. $EmailMessage.Body.Text = $rpReport  
  148. $EmailMessage.SendAndSaveCopy()  


Wednesday, July 11, 2012

EWS Managed API and Powershell How-To Series Part 11 Moderation

Moderation is an Exchange feature that was introduced in Exchange 2010 that allows the Human control of mail flow to a distribution group or mailbox. There is a good Blog article that describes the process at  http://blogs.technet.com/b/exchange/archive/2009/06/10/3407662.aspx. In this installment of the How-To series I'll go through all the different ways in which you can use and manipulate moderated email via EWS. 

Moderation Workflow

The moderation process flows through a workflow so I'll look at the full process and show where EWS can be used along that way. 

Before Send

The first thing you can do is even before a message is going to be sent is to work out if its going to be moderated by using MailTips. Eg the following code will access the MailTip for moderation and show you if the recipient will be moderated.

  1. $MailboxName = "sender@domain.com"    
  2.     
  3. $Mailboxes = @("user1@domain.com","user2@domain.com")     
  4.     
  5. $cred = New-Object System.Net.NetworkCredential("user1@domain.com","password@#")    
  6.   
  7. ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates    
  8.     
  9. ## Code From http://poshcode.org/624  
  10. ## Create a compilation environment  
  11. $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider  
  12. $Compiler=$Provider.CreateCompiler()  
  13. $Params=New-Object System.CodeDom.Compiler.CompilerParameters  
  14. $Params.GenerateExecutable=$False  
  15. $Params.GenerateInMemory=$True  
  16. $Params.IncludeDebugInformation=$False  
  17. $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null  
  18.   
  19. $TASource=@' 
  20.   namespace Local.ToolkitExtensions.Net.CertificatePolicy{ 
  21.     public class TrustAll : System.Net.ICertificatePolicy { 
  22.       public TrustAll() {  
  23.       } 
  24.       public bool CheckValidationResult(System.Net.ServicePoint sp, 
  25.         System.Security.Cryptography.X509Certificates.X509Certificate cert,  
  26.         System.Net.WebRequest req, int problem) { 
  27.         return true; 
  28.       } 
  29.     } 
  30.   } 
  31. '@   
  32. $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)  
  33. $TAAssembly=$TAResults.CompiledAssembly  
  34.   
  35. ## We now create an instance of the TrustAll and attach it to the ServicePointManager  
  36. $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")  
  37. [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll  
  38.   
  39. ## end code from http://poshcode.org/624  
  40.   
  41. $dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll"  
  42. [void][Reflection.Assembly]::LoadFile($dllpath)  
  43. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)  
  44. $service.TraceEnabled = $false  
  45.   
  46. $service.Credentials = $cred  
  47. $service.autodiscoverurl($MailboxName,{$true})  
  48. "using CAS : " + $service.Url  
  49.   
  50. $expRequest = @" 
  51. <?xml version="1.0" encoding="utf-8"?> 
  52. <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 
  53. <soap:Header><RequestServerVersion Version="Exchange2010_SP1" xmlns="http://schemas.microsoft.com/exchange/services/2006/types" /> 
  54. </soap:Header> 
  55. <soap:Body> 
  56. <GetMailTips xmlns="http://schemas.microsoft.com/exchange/services/2006/messages"> 
  57. <SendingAs> 
  58. <EmailAddress xmlns="http://schemas.microsoft.com/exchange/services/2006/types">$MailboxName</EmailAddress> 
  59. </SendingAs> 
  60. <Recipients> 
  61. "@  
  62.   
  63. foreach($mbMailbox in $Mailboxes){  
  64.     $expRequest = $expRequest + "<Mailbox xmlns=`"http://schemas.microsoft.com/exchange/services/2006/types`"><EmailAddress>$mbMailbox</EmailAddress></Mailbox>"   
  65. }  
  66.   
  67. $expRequest = $expRequest + "</Recipients><MailTipsRequested>ModerationStatus</MailTipsRequested></GetMailTips></soap:Body></soap:Envelope>"  
  68. $mbMailboxFolderURI = New-Object System.Uri($service.url)  
  69. $wrWebRequest = [System.Net.WebRequest]::Create($mbMailboxFolderURI)  
  70. $wrWebRequest.CookieContainer =  New-Object System.Net.CookieContainer   
  71. $wrWebRequest.KeepAlive = $false;  
  72. $wrWebRequest.Headers.Set("Pragma""no-cache");  
  73. $wrWebRequest.Headers.Set("Translate""f");  
  74. $wrWebRequest.Headers.Set("Depth""0");  
  75. $wrWebRequest.ContentType = "text/xml";  
  76. $wrWebRequest.ContentLength = $expRequest.Length;  
  77. $wrWebRequest.Timeout = 60000;  
  78. $wrWebRequest.Method = "POST";  
  79. $wrWebRequest.Credentials = $cred  
  80. $bqByteQuery = [System.Text.Encoding]::ASCII.GetBytes($expRequest);  
  81. $wrWebRequest.ContentLength = $bqByteQuery.Length;  
  82. $rsRequestStream = $wrWebRequest.GetRequestStream();  
  83. $rsRequestStream.Write($bqByteQuery, 0, $bqByteQuery.Length);  
  84. $rsRequestStream.Close();  
  85. $wrWebResponse = $wrWebRequest.GetResponse();  
  86. $rsResponseStream = $wrWebResponse.GetResponseStream()  
  87. $sr = new-object System.IO.StreamReader($rsResponseStream);  
  88. $rdResponseDocument = New-Object System.Xml.XmlDocument  
  89. $rdResponseDocument.LoadXml($sr.ReadToEnd());  
  90. $RecipientNodes = @($rdResponseDocument.getElementsByTagName("t:RecipientAddress"))  
  91. $Datanodes = @($rdResponseDocument.getElementsByTagName("t:IsModerated"))  
  92. for($ic=0;$ic -lt $RecipientNodes.length;$ic++){  
  93.     if($Datanodes[$ic]."#text" -eq $true){  
  94.         $RecipientNodes[$ic].EmailAddress + " is Moderated"  
  95.     }  
  96.     else{  
  97.         $RecipientNodes[$ic].EmailAddress + " isn't Moderated"  
  98.     }  
  99. }  
 Once Moderated

Once a message has been moderated the moderator of that Mailbox/Group will be sent an Approval Email so they can either Accept or Reject the message. This Approval Email has a message class of IPM.Note.Microsoft.Approval.Request so if we want to find all the messages of this type with a Mailbox we need to construct a SearchFilter to find message of this type.

Eg to Find all the approval messages in the Inbox of a Mailbox

  1. $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)     
  2. $Inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$folderid)  
  3. $ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)   
  4. $SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass,"IPM.Note.Microsoft.Approval.Request")  
  5. $PidNameApprovalRequestor = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::InternetHeaders,"x-ms-exchange-organization-approval-requestor",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);    
  6. $Propset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
  7. $Propset.add($PidNameApprovalRequestor)  
  8. $ivItemView.PropertySet = $Propset  
  9.   
  10. do{    
  11.     $fiResults = $Inbox.findItems($SfSearchFilter,$ivItemView)  
  12.        
  13.     foreach($Item in $fiResults.Items){    
  14.         $propval = $null  
  15.         if($Item.TryGetProperty($PidNameApprovalRequestor,[ref]$propval)){  
  16.             "From : " + $propval  
  17.         }  
  18.         "Recieved : " + $Item.DateTimeReceived  
  19.         "Subject : " + $Item.Subject          
  20.     }    
  21.     $ivItemView.Offset += $fiResults.Items.Count    
  22. }while($fiResults.MoreAvailable -eq $true)  
One thing of note about this example is the use of the ms-exchange-organization-approval-requestor Header which contains the Original sender address of the Message that has been moderated.


Approving or Rejecting Emails

***********Disclaimer this bit is unsupported ************

There are no supported methods of Approving Moderated emails in EWS but using a little reverse engineering to work out what properties need to be set the following method seems to work okay.

Important Properties on Approval/Rejection Emails

ItemClass eg

$EmailMessage.ItemClass = "IPM.Note.Microsoft.Approval.Reply.Approve"
 

$EmailMessage.ItemClass = "IPM.Note.Microsoft.Approval.Reply.Reject" 


Subject - Use the Normalized subject from the approval Request email and then append Accept or Reject eg

 $NormalSubject = $null
 [Void]$Item.TryGetProperty($PR_NORMALIZED_SUBJECT,[ref]$NormalSubject)



$EmailMessage.Subject = "Approve: " + $NormalSubject
$EmailMessage.Subject = "Reject: " + $NormalSubject



RecipientTo - Needs to be set to the Microsoft Exchange Approval Assistant Address

$EmailMessage.ToRecipients.Add($Item.Sender.Address)


Extended Properties

VerbReponse - (not sure about this one)
PR_ReportTag - This is a critical property you get from the Approval Request which is used to correlate the Approval Response.

$PR_REPORT_TAG = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0031,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);
$VerbResponse = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,0x8524,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String); 

$ReportID = $null
[Void]$Item.TryGetProperty($PR_REPORT_TAG,[ref]$ReportID)
$EmailMessage.SetExtendedProperty($VerbResponse,"Reject")

$EmailMessage.SetExtendedProperty($PR_REPORT_TAG,$ReportID)

So to put this together as an Example if we had a Message in the Inbox with a Subject of  Egg and we wanted to approve this message we could use the following

  1. $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)     
  2. $Inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$folderid)  
  3. $ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)   
  4. $SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass,"IPM.Note.Microsoft.Approval.Request")  
  5. $PidNameApprovalRequestor = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::InternetHeaders,"x-ms-exchange-organization-approval-requestor",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);    
  6. $VerbResponse = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,0x8524,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);    
  7. $PR_NORMALIZED_SUBJECT = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0E1D,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);       
  8. $PR_REPORT_TAG = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0031,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);  
  9.   
  10.   
  11.   
  12. $Propset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
  13. $Propset.add($PidNameApprovalRequestor)  
  14. $Propset.add($PR_NORMALIZED_SUBJECT)   
  15. $Propset.add($PR_REPORT_TAG)  
  16. $ivItemView.PropertySet = $Propset  
  17.   
  18. do{    
  19.     $fiResults = $Inbox.findItems($SfSearchFilter,$ivItemView)  
  20.     foreach($Item in $fiResults.Items){    
  21.         $propval = $null  
  22.         if($Item.TryGetProperty($PidNameApprovalRequestor,[ref]$propval)){  
  23.             "From : " + $propval  
  24.         }  
  25.     "Recieved : " + $Item.DateTimeReceived  
  26.     "Subject : " + $Item.Subject  
  27.     $NormalSubject = $null  
  28.     [Void]$Item.TryGetProperty($PR_NORMALIZED_SUBJECT,[ref]$NormalSubject)  
  29.     $ReportID = $null  
  30.     [Void]$Item.TryGetProperty($PR_REPORT_TAG,[ref]$ReportID)  
  31.     $Item.Load()  
  32.     if($NormalSubject -eq "Egg"){  
  33.     # Create Email Object    
  34.     $EmailMessage = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage($service)   
  35.     #Set the Subject    
  36.     $EmailMessage.Subject = "Approve: " + $NormalSubject  
  37.     #Add Recipients    
  38.     $EmailMessage.ToRecipients.Add($Item.Sender.Address)    
  39.     #Send Message SendandSave will save the Message to the Sender Sent Item Folder    
  40.     $EmailMessage.ItemClass = "IPM.Note.Microsoft.Approval.Reply.Approve"  
  41.     $EmailMessage.SetExtendedProperty($VerbResponse,"Approve")  
  42.     $EmailMessage.SetExtendedProperty($PR_REPORT_TAG,$ReportID)  
  43.     $EmailMessage.SendAndSaveCopy() }   
  44.     }    
  45.     $ivItemView.Offset += $fiResults.Items.Count    
  46. }while($fiResults.MoreAvailable -eq $true)  

If we wanted to do the same thing this time we wanted to reject a message that was pending approval with the Subject of Chicken and this time say why we are rejecting it you could use

  1. $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)     
  2. $Inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$folderid)  
  3. $ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)   
  4. $SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass,"IPM.Note.Microsoft.Approval.Request")  
  5. $PidNameApprovalRequestor = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::InternetHeaders,"x-ms-exchange-organization-approval-requestor",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);    
  6. $VerbResponse = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,0x8524,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);    
  7. $PR_NORMALIZED_SUBJECT = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0E1D,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);       
  8. $PR_REPORT_TAG = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0031,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);  
  9.   
  10.   
  11.   
  12. $Propset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
  13. $Propset.add($PidNameApprovalRequestor)  
  14. $Propset.add($PR_NORMALIZED_SUBJECT)   
  15. $Propset.add($PR_REPORT_TAG)  
  16. $ivItemView.PropertySet = $Propset  
  17.   
  18. do{    
  19.     $fiResults = $Inbox.findItems($SfSearchFilter,$ivItemView)  
  20.       
  21.     foreach($Item in $fiResults.Items){    
  22.         $propval = $null  
  23.         if($Item.TryGetProperty($PidNameApprovalRequestor,[ref]$propval)){  
  24.             "From : " + $propval  
  25.         }  
  26.     "Recieved : " + $Item.DateTimeReceived  
  27.     "Subject : " + $Item.Subject  
  28.     $NormalSubject = $null  
  29.     [Void]$Item.TryGetProperty($PR_NORMALIZED_SUBJECT,[ref]$NormalSubject)  
  30.     $ReportID = $null  
  31.     [Void]$Item.TryGetProperty($PR_REPORT_TAG,[ref]$ReportID)  
  32.     $Item.Load()  
  33.     if($NormalSubject -eq "Chicken"){  
  34.     # Create Email Object    
  35.     $EmailMessage = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage($service)   
  36.     #Set the Subject    
  37.     $EmailMessage.Subject = "Reject: " + $NormalSubject  
  38.     #Add Recipients    
  39.     $EmailMessage.ToRecipients.Add($Item.Sender.Address)    
  40.     #Send Message SendandSave will save the Message to the Sender Sent Item Folder     
  41.     $EmailMessage.ItemClass = "IPM.Note.Microsoft.Approval.Reply.Reject"  
  42.     $EmailMessage.SetExtendedProperty($VerbResponse,"Reject")  
  43.     $EmailMessage.SetExtendedProperty($PR_REPORT_TAG,$ReportID)  
  44.     $EmailMessage.Body = "Sorry no Chicken"  
  45.     $EmailMessage.SendAndSaveCopy() }   
  46.     }    
  47.     $ivItemView.Offset += $fiResults.Items.Count    
  48. }while($fiResults.MoreAvailable -eq $true)  

Sunday, July 01, 2012

Reporting and Clearing the SyncIssues, Conflicts, LocalFailures and ServerFailures using EWS and Powershell

The SyncIssues, Conflicts, LocalFailures and ServerFailures folders in a Mailbox are folders that "contain logs and items that Microsoft Outlook has been unable to synchronize with your Microsoft Exchange Server" which are described thoroughly in http://office.microsoft.com/en-us/outlook-help/synchronization-error-folders-HP001040042.aspx.

From a operational perspective over a period of time these folders can fill up with items for a number of reasons or because of certain problems or third party products. So in this post I'm going to look at how you can use EWS to report and delete the content in these folders.

Getting Access to the Folders
There are two ways you could access these folders in EWS the first would be to do a conventional search for the displayname of the folder's using a FindFolder operation and a couple of Shallow traversals starting at the MsgFolderRoot. Or the other way which can deal with Localized foldernames and also reduces the number of operations is to use the PR_ADDITIONAL_REN_ENTRYIDS property http://msdn.microsoft.com/en-us/library/cc842240.aspx on the Root folder (or the Non_IPM_Subtree). 

The PR_ADDITIONAL_REN_ENTRYIDS is a multivalued Binary Array extended Mapi property which contains the HexEntryID for each of the Folders. To make use of these Id's you need to first convert the BinaryArray value to a String Hex value with BitConverter Class. Then use the EWS ConvertID operation to convert the Hexid to an EWSId the you can Bind to the Folder.

The Script as posted uses EWS Impersonation

If you want to customize which mailboxes it reports on then just change the Get-Mailbox line

Get-Mailbox -ResultSize Unlimited | ForEach-Object{  

eg if you want to limit to only checking one server you could use

Get-Mailbox -ResultSize Unlimited -Server servernameblah | ForEach-Object{

You could do similar with other filter properties such as Database or OU

The script produces a CSV report of the Size and Item Count of each of these folders, if you want to delete all the Items within these folders you need to add one line to the script that will delete the Content of the folder that you want to effect eg for the SyncIssueFolder add


$SyncIssueFolder.Empty([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete, $true);

after the size report section. You need to add a separate Empty for Each folder you want to clear. On 2007 you can't use the Empty method so would need add code to enumerate the Items so you can do a batch delete I covered this in the Folders HowTo http://gsexdev.blogspot.com.au/2012/01/ews-managed-api-and-powershell-how-to_23.html

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

  1. $rptCollection = @()  
  2. ## Load Managed API dll    
  3. Add-Type -Path "C:\Program Files\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll"    
  4.     
  5. ## Set Exchange Version    
  6. $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2    
  7.     
  8. ## Create Exchange Service Object    
  9. $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
  10.     
  11. ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials    
  12.     
  13. #Credentials Option 1 using UPN for the windows Account    
  14. $psCred = Get-Credential    
  15. $creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())    
  16. $service.Credentials = $creds       
  17.     
  18. #Credentials Option 2    
  19. #service.UseDefaultCredentials = $true    
  20.     
  21. ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates    
  22.     
  23. ## Code From http://poshcode.org/624  
  24. ## Create a compilation environment  
  25. $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider  
  26. $Compiler=$Provider.CreateCompiler()  
  27. $Params=New-Object System.CodeDom.Compiler.CompilerParameters  
  28. $Params.GenerateExecutable=$False  
  29. $Params.GenerateInMemory=$True  
  30. $Params.IncludeDebugInformation=$False  
  31. $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null  
  32.   
  33. $TASource=@' 
  34.   namespace Local.ToolkitExtensions.Net.CertificatePolicy{ 
  35.     public class TrustAll : System.Net.ICertificatePolicy { 
  36.       public TrustAll() {  
  37.       } 
  38.       public bool CheckValidationResult(System.Net.ServicePoint sp, 
  39.         System.Security.Cryptography.X509Certificates.X509Certificate cert,  
  40.         System.Net.WebRequest req, int problem) { 
  41.         return true; 
  42.       } 
  43.     } 
  44.   } 
  45. '@   
  46. $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)  
  47. $TAAssembly=$TAResults.CompiledAssembly  
  48.   
  49. ## We now create an instance of the TrustAll and attach it to the ServicePointManager  
  50. $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")  
  51. [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll  
  52.   
  53. ## end code from http://poshcode.org/624  
  54.     
  55. ## 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    
  56.     
  57.      
  58.       
  59.       
  60.   
  61. Get-Mailbox -ResultSize Unlimited | ForEach-Object{     
  62.     $MailboxName = $_.PrimarySMTPAddress.ToString()    
  63.     "Processing Mailbox : " + $MailboxName    
  64.     if($service.url -eq $null){    
  65.         ## 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    
  66.     
  67.         #CAS URL Option 1 Autodiscover    
  68.         $service.AutodiscoverUrl($MailboxName,{$true})    
  69.         "Using CAS Server : " + $Service.url     
  70.             
  71.         #CAS URL Option 2 Hardcoded    
  72.         #$uri=[system.URI] "https://casservername/ews/exchange.asmx"    
  73.         #$service.Url = $uri      
  74.     }    
  75.     
  76.     $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)   
  77.     $PR_MESSAGE_SIZE_EXTENDED = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(3592,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Long);    
  78.   
  79.     $PR_ADDITIONAL_REN_ENTRYIDS = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x36D8, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::BinaryArray);   
  80.     $Propset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
  81.     $Propset.add($PR_ADDITIONAL_REN_ENTRYIDS)  
  82.     $Propset.add($PR_MESSAGE_SIZE_EXTENDED)  
  83.     #Sync Folders  
  84.   
  85.     $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root,$MailboxName)     
  86.     $RootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid,$Propset)  
  87.     $objVal = $null  
  88.   
  89.     function ConvertFolderid($hexId){  
  90.         $aiItem = New-Object Microsoft.Exchange.WebServices.Data.AlternateId    
  91.         $aiItem.Mailbox = $MailboxName    
  92.         $aiItem.UniqueId = $hexId  
  93.         $aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::HexEntryId;    
  94.         return $service.ConvertId($aiItem, [Microsoft.Exchange.WebServices.Data.IdFormat]::EWSId)   
  95.     }  
  96.   
  97.     if($RootFolder.TryGetProperty($PR_ADDITIONAL_REN_ENTRYIDS,[ref]$objVal)){  
  98.         if($objVal[0] -ne $null){  
  99.             $rptobj = "" | Select MailboxName,SyncIssuesCount,SyncIssuesSize,ConflictsCount,ConflictsSize,LocalFailuresCount,LocalFailuresSize,ServerFailuresCount,ServerFailuresSize  
  100.             $rptobj.MailboxName = $MailboxName  
  101.             $cfid = ConvertFolderid([System.BitConverter]::ToString($objVal[0]).Replace("-",""))  
  102.             if($cfid.UniqueId -ne $null){  
  103.             $ConflictsFolderId = new-object Microsoft.Exchange.WebServices.Data.FolderId($cfid.UniqueId.ToString())  
  104.             $ConflictsFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$ConflictsFolderId,$Propset)  
  105.             $ConflictsFolder.DisplayName  
  106.             $folderSize = $null  
  107.             if($ConflictsFolder.TryGetProperty($PR_MESSAGE_SIZE_EXTENDED,[ref]$folderSize)){  
  108.                 $rptobj.ConflictsCount = $ConflictsFolder.TotalCount  
  109.                 $rptobj.ConflictsSize = [Math]::Round($folderSize/1MB)   
  110.                 "ItemCount  : " + $ConflictsFolder.TotalCount  
  111.                 "FolderSize : " + [Math]::Round($folderSize/1MB) + " MB"  
  112.             }  
  113.             $siId = ConvertFolderid([System.BitConverter]::ToString($objVal[1]).Replace("-",""))  
  114.             $SyncIssuesFolderID = new-object Microsoft.Exchange.WebServices.Data.FolderId($siId.UniqueId.ToString())  
  115.             $SyncIssueFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$SyncIssuesFolderID,$Propset)  
  116.             $SyncIssueFolder.DisplayName  
  117.             if($SyncIssueFolder.TryGetProperty($PR_MESSAGE_SIZE_EXTENDED,[ref]$folderSize)){  
  118.                 $rptobj.SyncIssuesCount = $SyncIssueFolder.TotalCount  
  119.                 $rptobj.SyncIssuesSize = [Math]::Round($folderSize/1MB)  
  120.                 "ItemCount  : " + $SyncIssueFolder.TotalCount  
  121.                 "FolderSize : " + [Math]::Round($folderSize/1MB) + " MB"  
  122.             }  
  123.             $lcId = ConvertFolderid([System.BitConverter]::ToString($objVal[2]).Replace("-",""))  
  124.             $localFailureId = new-object Microsoft.Exchange.WebServices.Data.FolderId($lcId.UniqueId.ToString())  
  125.             $localFailureFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$localFailureId,$Propset)  
  126.             $localFailureFolder.DisplayName  
  127.             if($localFailureFolder.TryGetProperty($PR_MESSAGE_SIZE_EXTENDED,[ref]$folderSize)){  
  128.                 $rptobj.LocalFailuresCount = $localFailureFolder.TotalCount  
  129.                 $rptobj.LocalFailuresSize = [Math]::Round($folderSize/1MB)  
  130.                 "ItemCount  : " + $localFailureFolder.TotalCount  
  131.                 "FolderSize : " + [Math]::Round($folderSize/1MB) + " MB"  
  132.             }  
  133.             $sfid = ConvertFolderid([System.BitConverter]::ToString($objVal[3]).Replace("-",""))  
  134.             $ServerFailureId = new-object Microsoft.Exchange.WebServices.Data.FolderId($sfid.UniqueId.ToString())  
  135.             $ServerFailureFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$ServerFailureId,$Propset)  
  136.             $ServerFailureFolder.DisplayName  
  137.             if($ServerFailureFolder.TryGetProperty($PR_MESSAGE_SIZE_EXTENDED,[ref]$folderSize)){  
  138.                 $rptobj.ServerFailuresCount = $ServerFailureFolder.TotalCount  
  139.                 $rptobj.ServerFailuresSize = [Math]::Round($folderSize/1MB)  
  140.                 "ItemCount  : " + $ServerFailureFolder.TotalCount  
  141.                 "FolderSize : " + [Math]::Round($folderSize/1MB) + " MB"  
  142.             }  
  143.             $rptCollection += $rptobj  
  144.             }  
  145.         }  
  146.     }  
  147. }  
  148. $rptCollection  
  149. $rptCollection | Export-Csv -NoTypeInformation -Path c:\temp\SyncFolderReport.csv