Yes, this is similar to the script in my previous post but is also different in the following ways:
I would suggest you read my previous post as it covers some fundamental used in this post.
To summarize what the script below does:
Straight to code now. Create a .PS1 file with the code below:
# The script requires the EWS managed API, which can be downloaded here:# http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=c3342fb3-fbcc-4127-becf-872c746840e1# This also requires PowerShell 2.0# Make sure the Import-Module command below matches the DLL location of the API.# This path must match the install location of the EWS managed API. Change it if needed.[string]$info = "White" # Color for informational messages[string]$warning = "Yellow" # Color for warning messages[string]$error = "Red" # Color for error messages[string]$LogFile = "C:\Temp\Log.txt" # Path of the Log Filefunction SearchFolders($MailboxName){ Write-host "Searching folders in Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Searching folders in Mailbox Name:" + $MailboxName) #Change the user to Impersonate $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$MailboxName); #Number of Items to Get $FpageSize =50 $FOffset = 0 $MoreFolders =$true while ($MoreFolders) { #Setup the View to get a limited number of Items at one time $folderView = new-object Microsoft.Exchange.WebServices.Data.FolderView($FpageSize,$FOffset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $folderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep $folderView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet( [Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, [Microsoft.Exchange.WebServices.Data.FolderSchema]::FolderClass); #Create the Search Filter. $FoSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::FolderClass, "IPF.Note") $oFindFolders = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$FoSearchFilter,$folderView) foreach ($folder in $oFindFolders.Folders) { Write-host "Begining to stamp policy on Items in folder:" $folder.DisplayName -foregroundcolor $info; Add-Content $LogFile ("Begining to stamp policy on Items in folder:" + $folder.DisplayName); &{ StampPolicyOnItems($folder.Id.UniqueId) Write-host "Retention policy stamped on Items in folder:" $folder.DisplayName -foregroundcolor $info Add-Content $LogFile ("Retention policy stamped on Items in folder:" + $folder.DisplayName) } trap [System.Exception] { $IsFailure = $true; Write-host ("Error in StampPolicyOnItems: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error in StampPolicyOnItems: " + $_.Exception.Message); Write-host "Failure in stamping Retention policy on Items in folder:" $folder.DisplayName -foregroundcolor $info Add-Content $LogFile ("Failure in stamping Retention policy on Items in folder:" + $folder.DisplayName) continue; } } if ($oFindFolders.MoreAvailable -eq $false) {$MoreFolders = $false} if ($MoreFolders) {$FOffset += $FpageSize} } Write-host "Finished with Mailbox Name:" $MailboxName "Done!" -foregroundcolor $info Write-host "-------------------------------------------------" -foregroundcolor $info Add-Content $LogFile ("Done!") Add-Content $LogFile ("-------------------------------------------------" ) $service.ImpersonatedUserId = $null}function StampPolicyOnItems($fId){ #Number of Items to Get $pageSize =100 $Offset = 0 $MoreItems =$true $ItemCount=0 #PR_POLICY_TAG 0x3019 $PolicyTag = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x3019,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary); #PR_RETENTION_PERIOD 0x301A $RetentionPeriod = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301A,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer); while ($MoreItems) { #Setup the View to get a limited number of Items at one time $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($pageSize,$Offset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet( [Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::Subject); #Create the Search Filter. $oSearchFilters = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection( [Microsoft.Exchange.WebServices.Data.LogicalOperator]::And) $oSearchFilterEA1 = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, "IPM.My-ARCHIVE") $oSearchFilterEA2 = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+Not(New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+Exists($PolicyTag)) $oSearchFilters.add($oSearchFilterEA1) $oSearchFilters.add($oSearchFilterEA2) $uniqueId = new-object Microsoft.Exchange.WebServices.Data.FolderId($fId); $oFindItems = $service.FindItems($uniqueId,$oSearchFilters,$itemView) foreach ($Item in $oFindItems.Items) { $ItemCount++ Write-host "Item Number:" $ItemCount Write-host "Message Class:" $Item.ItemClass Write-host "Subject:" $Item.Subject #Same as that on the policy $Item.SetExtendedProperty($RetentionPeriod, 1095) #Change the GUID based on your policy tag $PolicyTagGUID = new-Object Guid("{92186ff7-7f4d-4efa-a09b-bbdc5aee3908}"); $Item.SetExtendedProperty($PolicyTag, $PolicyTagGUID.ToByteArray()) $Item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite) Write-host "Retention policy stamped on " $Item.ID -foregroundcolor $info Add-Content $LogFile ("Retention policy stamped on " + $Item.ID) } if ($oFindItems.MoreAvailable -eq $false) {$MoreItems = $false} if ($MoreItems) {$Offset += $pageSize} }}Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)# Set the Credentials$service.Credentials = new-object Microsoft.Exchange.WebServices.Data.WebCredentials("serviceAccount","Password","domain")# Change the URL to point to your cas server$service.Url= new-object Uri("http://YOUR-CAS/EWS/Exchange.asmx")# Set $UseAutoDiscover to $true if you want to use AutoDiscover else it will use the URL set above$UseAutoDiscover = $false#Read data from the UserAccounts.txtimport-csv UserAccounts.txt | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } SearchFolders($WindowsEmailAddress)}
# The script requires the EWS managed API, which can be downloaded here:# http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=c3342fb3-fbcc-4127-becf-872c746840e1# This also requires PowerShell 2.0# Make sure the Import-Module command below matches the DLL location of the API.# This path must match the install location of the EWS managed API. Change it if needed.
[string]$info = "White" # Color for informational messages[string]$warning = "Yellow" # Color for warning messages[string]$error = "Red" # Color for error messages[string]$LogFile = "C:\Temp\Log.txt" # Path of the Log File
function SearchFolders($MailboxName){ Write-host "Searching folders in Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Searching folders in Mailbox Name:" + $MailboxName)
#Change the user to Impersonate $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$MailboxName);
#Number of Items to Get $FpageSize =50 $FOffset = 0 $MoreFolders =$true while ($MoreFolders) { #Setup the View to get a limited number of Items at one time $folderView = new-object Microsoft.Exchange.WebServices.Data.FolderView($FpageSize,$FOffset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $folderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep $folderView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet( [Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, [Microsoft.Exchange.WebServices.Data.FolderSchema]::FolderClass);
#Create the Search Filter. $FoSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::FolderClass, "IPF.Note") $oFindFolders = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$FoSearchFilter,$folderView) foreach ($folder in $oFindFolders.Folders) { Write-host "Begining to stamp policy on Items in folder:" $folder.DisplayName -foregroundcolor $info; Add-Content $LogFile ("Begining to stamp policy on Items in folder:" + $folder.DisplayName); &{ StampPolicyOnItems($folder.Id.UniqueId)
Write-host "Retention policy stamped on Items in folder:" $folder.DisplayName -foregroundcolor $info Add-Content $LogFile ("Retention policy stamped on Items in folder:" + $folder.DisplayName) } trap [System.Exception] { $IsFailure = $true; Write-host ("Error in StampPolicyOnItems: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error in StampPolicyOnItems: " + $_.Exception.Message); Write-host "Failure in stamping Retention policy on Items in folder:" $folder.DisplayName -foregroundcolor $info Add-Content $LogFile ("Failure in stamping Retention policy on Items in folder:" + $folder.DisplayName) continue; } } if ($oFindFolders.MoreAvailable -eq $false) {$MoreFolders = $false} if ($MoreFolders) {$FOffset += $FpageSize} }
Write-host "Finished with Mailbox Name:" $MailboxName "Done!" -foregroundcolor $info Write-host "-------------------------------------------------" -foregroundcolor $info Add-Content $LogFile ("Done!") Add-Content $LogFile ("-------------------------------------------------" ) $service.ImpersonatedUserId = $null}function StampPolicyOnItems($fId){ #Number of Items to Get $pageSize =100 $Offset = 0 $MoreItems =$true $ItemCount=0
#PR_POLICY_TAG 0x3019 $PolicyTag = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x3019,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);
#PR_RETENTION_PERIOD 0x301A $RetentionPeriod = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301A,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer);
while ($MoreItems) { #Setup the View to get a limited number of Items at one time $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($pageSize,$Offset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet( [Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::Subject); #Create the Search Filter. $oSearchFilters = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection( [Microsoft.Exchange.WebServices.Data.LogicalOperator]::And) $oSearchFilterEA1 = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, "IPM.My-ARCHIVE") $oSearchFilterEA2 = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+Not(New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+Exists($PolicyTag))
$oSearchFilters.add($oSearchFilterEA1) $oSearchFilters.add($oSearchFilterEA2) $uniqueId = new-object Microsoft.Exchange.WebServices.Data.FolderId($fId); $oFindItems = $service.FindItems($uniqueId,$oSearchFilters,$itemView) foreach ($Item in $oFindItems.Items) { $ItemCount++ Write-host "Item Number:" $ItemCount Write-host "Message Class:" $Item.ItemClass Write-host "Subject:" $Item.Subject #Same as that on the policy $Item.SetExtendedProperty($RetentionPeriod, 1095)
#Change the GUID based on your policy tag $PolicyTagGUID = new-Object Guid("{92186ff7-7f4d-4efa-a09b-bbdc5aee3908}");
$Item.SetExtendedProperty($PolicyTag, $PolicyTagGUID.ToByteArray()) $Item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite) Write-host "Retention policy stamped on " $Item.ID -foregroundcolor $info Add-Content $LogFile ("Retention policy stamped on " + $Item.ID) } if ($oFindItems.MoreAvailable -eq $false) {$MoreItems = $false} if ($MoreItems) {$Offset += $pageSize} }}Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)
# Set the Credentials$service.Credentials = new-object Microsoft.Exchange.WebServices.Data.WebCredentials("serviceAccount","Password","domain")
# Change the URL to point to your cas server$service.Url= new-object Uri("http://YOUR-CAS/EWS/Exchange.asmx")
# Set $UseAutoDiscover to $true if you want to use AutoDiscover else it will use the URL set above$UseAutoDiscover = $false
#Read data from the UserAccounts.txtimport-csv UserAccounts.txt | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } SearchFolders($WindowsEmailAddress)}
UserAccounts.txt contains the list of the users mailboxes in which the items needs to be stamped. This file should exist in the same directory as the .PS1 file.The First row denotes the Field Names. Format of the text file is as below:
WindowsEmailAddressakasha@contoso.comakashb@contoso.comakashc@contoso.com
Enjoy!
This post deals with a scenario where we need to stamp Retention Policy on specific items. In this case we wanted to stamp a policy on items with a specific message class. In my previous post I had explained about Messaging records management and how to stamp the policy on a folder. Please do read my previous post if you have not yet done so.
The process of stamping Retention Policy on items is not much different, when applying the Retention Policy to an item, we need to stamp it with two important properties:
Before you begin: You will need to download and install the EWS Managed API on the box you run this script on from the link below: Exchange Web Services Managed API http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=c3342fb3-fbcc-4127-becf-872c746840e1 Read about setting up Impersonation in Exchange 2010 as the code below uses it. Configuring Exchange Impersonation (Exchange Web Services) http://msdn.microsoft.com/en-us/library/bb204095.aspx Using Exchange Impersonation XML Request/Response http://msdn.microsoft.com/en-us/library/bb204088.aspx
Using Impersonation in EWS Managed API http://msdn.microsoft.com/en-us/library/dd633680(EXCHG.80).aspx
We will stamp the properties using Exchange Web Services Managed API 1.1 in PowerShell 2.0 and below is how it is done. Create a .PS1 file with the code below:
# This script requires PowerShell 2.0# Make sure the Import-Module command below matches the DLL location of the API.# This path must match the install location of the EWS managed API. Change it if needed.[string]$info = "White" # Color for informational messages[string]$warning = "Yellow" # Color for warning messages[string]$error = "Red" # Color for error messages[string]$LogFile = "C:\Temp\Log.txt" # Path of the Log Filefunction StampPolicyOnItems($MailboxName){ Write-host "Stamping Policy on Items for Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Stamping Policy on Items for Mailbox Name:" + $MailboxName) #Change the user to Impersonate. Comment the line below if impersonation is not needed $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$MailboxName); #Number of Items to Get $pageSize =50 $Offset = 0 $MoreItems =$true $ItemCount=0 while ($MoreItems) { #Setup the View to get a limited number of Items at one time $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($pageSize,$Offset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet( [Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::Subject); #Create the Search Filter and look for items with the message class IPM.My-ARCHIVE $oSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, "IPM.My-ARCHIVE") #Look for items in the Inbox $oFindItems = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$oSearchFilter,$itemView) foreach ($Item in $oFindItems.Items) { $ItemCount++ Write-host "Item Number:" $ItemCount Write-host "Message Class:" $Item.ItemClass Write-host "Subject:" $Item.Subject #PR_POLICY_TAG 0x3019 $PolicyTag = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x3019,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary) #PR_RETENTION_PERIOD 0x301A $RetentionPeriod = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301A,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer) #Same as that on the policy $Item.SetExtendedProperty($RetentionPeriod, 1095) #Change the GUID based on your policy tag $PolicyTagGUID = new-Object Guid("{92186ff7-7f4d-4efa-a09b-bbdc5aee3908}") $Item.SetExtendedProperty($PolicyTag, $PolicyTagGUID.ToByteArray()) $Item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite) Write-host "Retention policy stamped on " $Item.ID -foregroundcolor $info Add-Content $LogFile ("Retention policy stamped on " + $Item.ID) } if ($oFindItems.MoreAvailable -eq $false) {$MoreItems = $false} if ($MoreItems) {$Offset += $pageSize} } Write-host "Stamping Policy on Items for Mailbox Name:" $MailboxName "Done!" -foregroundcolor $info Add-Content $LogFile ("Done!") Add-Content $LogFile ("-------------------------------------------------" ) #Comment the line below if impersonation is not needed $service.ImpersonatedUserId = $null}Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)# Set the Credentials,Use the account that has been granted Impersonation rights# If impersonation is not used, use the credentials required to access the mailbox$service.Credentials = new-object Microsoft.Exchange.WebServices.Data.WebCredentials("serviceAccount","Password","domain")# Change the URL to point to your cas server$service.Url= new-object Uri(“http://Your-CAS-Server/EWS/Exchange.asmx”)# Set $UseAutoDiscover to $true if you want to use AutoDiscover else it will use the URL set above$UseAutoDiscover = $false#Read data from the UserAccounts.txtimport-csv UserAccounts.txt | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } StampPolicyOnItems($WindowsEmailAddress)}
# This script requires PowerShell 2.0# Make sure the Import-Module command below matches the DLL location of the API.# This path must match the install location of the EWS managed API. Change it if needed.
function StampPolicyOnItems($MailboxName){ Write-host "Stamping Policy on Items for Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Stamping Policy on Items for Mailbox Name:" + $MailboxName)
#Change the user to Impersonate. Comment the line below if impersonation is not needed $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$MailboxName);
#Number of Items to Get $pageSize =50 $Offset = 0 $MoreItems =$true $ItemCount=0
while ($MoreItems) { #Setup the View to get a limited number of Items at one time $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($pageSize,$Offset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet( [Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::Subject);
#Create the Search Filter and look for items with the message class IPM.My-ARCHIVE $oSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, "IPM.My-ARCHIVE")
#Look for items in the Inbox
$oFindItems = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$oSearchFilter,$itemView) foreach ($Item in $oFindItems.Items) { $ItemCount++ Write-host "Item Number:" $ItemCount Write-host "Message Class:" $Item.ItemClass Write-host "Subject:" $Item.Subject
#PR_POLICY_TAG 0x3019 $PolicyTag = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x3019,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)
#PR_RETENTION_PERIOD 0x301A $RetentionPeriod = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301A,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer)
#Same as that on the policy $Item.SetExtendedProperty($RetentionPeriod, 1095)
#Change the GUID based on your policy tag $PolicyTagGUID = new-Object Guid("{92186ff7-7f4d-4efa-a09b-bbdc5aee3908}")
$Item.SetExtendedProperty($PolicyTag, $PolicyTagGUID.ToByteArray()) $Item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite) Write-host "Retention policy stamped on " $Item.ID -foregroundcolor $info Add-Content $LogFile ("Retention policy stamped on " + $Item.ID) } if ($oFindItems.MoreAvailable -eq $false) {$MoreItems = $false} if ($MoreItems) {$Offset += $pageSize} } Write-host "Stamping Policy on Items for Mailbox Name:" $MailboxName "Done!" -foregroundcolor $info Add-Content $LogFile ("Done!") Add-Content $LogFile ("-------------------------------------------------" )
#Comment the line below if impersonation is not needed $service.ImpersonatedUserId = $null}
Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)
# Set the Credentials,Use the account that has been granted Impersonation rights# If impersonation is not used, use the credentials required to access the mailbox$service.Credentials = new-object Microsoft.Exchange.WebServices.Data.WebCredentials("serviceAccount","Password","domain")
# Change the URL to point to your cas server$service.Url= new-object Uri(“http://Your-CAS-Server/EWS/Exchange.asmx”)
#Read data from the UserAccounts.txtimport-csv UserAccounts.txt | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } StampPolicyOnItems($WindowsEmailAddress)}
Here is another example of using EWS Managed API with PowerShell 2.0. In this case the requirement was to be able to find out all contacts that have Hotmail email address in any of the Email Address fields. This provided me to opportunity to demonstrate the use SearchFilterCollection and ContainsSubstring search filter from a PowerShell script. In addition to SearchFilterCollection and ContainsSubstring, this sample also shows the use of Impersonation. The use of Impersonation is necessary because the requirement was to search in multiple mailboxes.
Details on how to configure Exchange Impersonation can be found in the links below: Exchange 2010: Configuring Exchange Impersonation (Exchange Web Services) http://msdn.microsoft.com/en-us/library/bb204095.aspx Using Exchange Impersonation XML Request/Response http://msdn.microsoft.com/en-us/library/bb204088.aspx
Exchange 2007: Configuring Exchange Impersonation (Exchange Web Services) http://msdn.microsoft.com/en-us/library/bb204095(EXCHG.80).aspx Using Exchange Impersonation (Exchange Web Services) http://msdn.microsoft.com/en-us/library/bb204088(EXCHG.80).aspx
You will also need to download and install the EWS Managed API on the box you run this script on from the link below:
Exchange Web Services Managed API http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=c3342fb3-fbcc-4127-becf-872c746840e1
Now for the code. Create a .PS1 file with the code below:
# This script requires PowerShell 2.0# Make sure the Import-Module command below matches the DLL location of the API.# This path must match the install location of the EWS managed API. Change it if needed.[string]$info = "White" # Color for informational messages[string]$warning = "Yellow" # Color for warning messages[string]$error = "Red" # Color for error messages[string]$LogFile = "C:\Temp\Log.txt" # Path of the Log Filefunction SearchContacts($MailboxName){ Write-host "Searching Contacts in Mailbox :" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Searching Contacts in Mailbox :" + $MailboxName) #Change the user to Impersonate $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId( [Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$MailboxName); #Number of Items to Get $pageSize =40 $Offset = 0 $MoreItems =$true $ItemCount=0 while ($MoreItems) { #Setup the View to get a limited number of Items at one time $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($pageSize,$Offset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.ContactSchema]::GivenName, [Microsoft.Exchange.WebServices.Data.ContactSchema]::Surname, [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress1, [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress2, [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress3) #Create the Search Filter. $searchFilterEA1 = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring( [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress1,"@hotmail.com") $searchFilterEA2 = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring( [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress2,"@hotmail.com") $searchFilterEA3 = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring( [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress3,"@hotmail.com") $oSearchFilters = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection( [Microsoft.Exchange.WebServices.Data.LogicalOperator]::Or) $oSearchFilters.add($searchFilterEA1) $oSearchFilters.add($searchFilterEA2) $oSearchFilters.add($searchFilterEA3) $oFindItems = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Contacts,$oSearchFilters,$itemView) foreach ($Item in $oFindItems.Items) { if ($Item.GetType().FullName -eq "Microsoft.Exchange.WebServices.Data.Contact") { Write-host "Given Name:" $Item.GivenName Write-host "Surname:" $Item.Surname Write-host "Email Address 1:" $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress1] Write-host "Email Address 2:" $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress2] Write-host "Email Address 3:" $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress3] Add-Content $LogFile("Given Name :" + $Item.GivenName) Add-Content $LogFile("Surname :" + $Item.Surname) Add-Content $LogFile("Email Address 1:" + $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress1]) Add-Content $LogFile("Email Address 2:" + $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress2]) Add-Content $LogFile("Email Address 3:" + $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress3]) } } if ($oFindItems.MoreAvailable -eq $false) {$MoreItems = $false} if ($MoreItems) {$Offset += $pageSize} } Write-host "Searching Contacts in Mailbox :" $MailboxName "Done!" -foregroundcolor $info Add-Content $LogFile ("Done!") Add-Content $LogFile ("-------------------------------------------------" ) $service.ImpersonatedUserId = $null}Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"# This script also works on Exchange 2007. Just change the Version Header to Exchange2007_SP1$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)# Set the Credentials$service.Credentials = new-object Microsoft.Exchange.WebServices.Data.WebCredentials("serviceAccount","Password","domain")# Change the URL to point to your cas server$service.Url= new-object Uri(“http://Your-CAS-Server/EWS/Exchange.asmx”)# Set $UseAutoDiscover to $true if you want to use AutoDiscover else it will use the URL set above$UseAutoDiscover = $false#Read data from the UserAccounts.txtimport-csv UserAccounts.txt | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } SearchContacts($WindowsEmailAddress)}
function SearchContacts($MailboxName){ Write-host "Searching Contacts in Mailbox :" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Searching Contacts in Mailbox :" + $MailboxName)
#Change the user to Impersonate $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId( [Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$MailboxName);
#Number of Items to Get $pageSize =40 $Offset = 0 $MoreItems =$true $ItemCount=0 while ($MoreItems) { #Setup the View to get a limited number of Items at one time $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($pageSize,$Offset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.ContactSchema]::GivenName, [Microsoft.Exchange.WebServices.Data.ContactSchema]::Surname, [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress1, [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress2, [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress3)
#Create the Search Filter. $searchFilterEA1 = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring( [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress1,"@hotmail.com") $searchFilterEA2 = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring( [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress2,"@hotmail.com") $searchFilterEA3 = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring( [Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress3,"@hotmail.com") $oSearchFilters = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection( [Microsoft.Exchange.WebServices.Data.LogicalOperator]::Or) $oSearchFilters.add($searchFilterEA1) $oSearchFilters.add($searchFilterEA2) $oSearchFilters.add($searchFilterEA3)
$oFindItems = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Contacts,$oSearchFilters,$itemView) foreach ($Item in $oFindItems.Items) { if ($Item.GetType().FullName -eq "Microsoft.Exchange.WebServices.Data.Contact") { Write-host "Given Name:" $Item.GivenName Write-host "Surname:" $Item.Surname Write-host "Email Address 1:" $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress1] Write-host "Email Address 2:" $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress2] Write-host "Email Address 3:" $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress3] Add-Content $LogFile("Given Name :" + $Item.GivenName) Add-Content $LogFile("Surname :" + $Item.Surname) Add-Content $LogFile("Email Address 1:" + $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress1]) Add-Content $LogFile("Email Address 2:" + $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress2]) Add-Content $LogFile("Email Address 3:" + $Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress3]) } } if ($oFindItems.MoreAvailable -eq $false) {$MoreItems = $false} if ($MoreItems) {$Offset += $pageSize} } Write-host "Searching Contacts in Mailbox :" $MailboxName "Done!" -foregroundcolor $info Add-Content $LogFile ("Done!") Add-Content $LogFile ("-------------------------------------------------" ) $service.ImpersonatedUserId = $null}Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"
# This script also works on Exchange 2007. Just change the Version Header to Exchange2007_SP1$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)
# Set $UseAutoDiscover to $true if you want to use AutoDiscover else it will use the URL set above$UseAutoDiscover = $false#Read data from the UserAccounts.txtimport-csv UserAccounts.txt | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } SearchContacts($WindowsEmailAddress)}
UserAccounts.txt contains the list of the users mailboxes that need to be searched. This file should exist in the same directory as the .PS1 file.The First row denotes the Field Names. Format of the text file is as below:
One of my customer ran into this issue with the Outlook View Control. All we were doing was trying to set the Folder property to the Contacts folder so that when the control loads it shows the contacts in that folder by default. In our case it gave the error 8002801d (Library Not Registered) and displayed the Inbox folder which is the default. This problem was not happening on all the machines. The strange thing was that it showed the Inbox in the View control, to me it means that the library is registered and that is why it was able to show Inbox. Why is it not showing the contacts? What could be going wrong? Which library are we talking about? After a little debugging, as expected we found out that the LoadLibrary call was failing. I was able to get the GUID it was looking for and it was {0006F062-0000-0000-C000-000000000046} which is for OUTLCTL.DLL.
This is where Process Monitor came to our help. I decided to take a Process Monitor log on a non-working and a working machine. Below is what I see: On the non-working machine( looking for {0006F062-0000-0000-C000-000000000046} ) this is how it looked:
OUTLOOK.EXE RegQueryKey HKCU\Software\Classes SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib SUCCESSOUTLOOK.EXE RegQueryKey HKCU\Software\Classes\TypeLib SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046} SUCCESSOUTLOOK.EXE RegQueryKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046} SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0 SUCCESSOUTLOOK.EXE RegQueryKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0 SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0\409 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0\409 NAME NOT FOUNDOUTLOOK.EXE RegQueryKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0 SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0\9 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0\9 NAME NOT FOUNDOUTLOOK.EXE RegQueryKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0 SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0\0 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0\0 NAME NOT FOUNDOUTLOOK.EXE RegCloseKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0 SUCCESSOUTLOOK.EXE RegCloseKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046} SUCCESSOUTLOOK.EXE RegCloseKey HKCU\Software\Classes\TypeLib SUCCESS
OUTLOOK.EXE RegCloseKey HKCU\Software\Classes SUCCESSOUTLOOK.EXE RegQueryKey HKCU\Software\Classes\TypeLib SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046} NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046} SUCCESSOUTLOOK.EXE RegQueryKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046} SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.0 NAME NOT FOUNDOUTLOOK.EXE RegQueryKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046} SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046} NAME NOT FOUNDOUTLOOK.EXE RegEnumKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046} SUCCESSOUTLOOK.EXE RegEnumKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046} NO MORE ENTRIESOUTLOOK.EXE RegQueryKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046} SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1 SUCCESSOUTLOOK.EXE RegQueryKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1 SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\409 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\409 NAME NOT FOUNDOUTLOOK.EXE RegQueryKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1 SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\9 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\9 NAME NOT FOUNDOUTLOOK.EXE RegQueryKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1 SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0 SUCCESSOUTLOOK.EXE RegQueryKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0 SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0\win32 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0\win32 SUCCESSOUTLOOK.EXE RegCloseKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0\win32 SUCCESSOUTLOOK.EXE RegCloseKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0 SUCCESSOUTLOOK.EXE RegQueryKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1 SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0 SUCCESSOUTLOOK.EXE RegQueryKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0 SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0\win32 NAME NOT FOUNDOUTLOOK.EXE RegOpenKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0\win32 SUCCESSOUTLOOK.EXE RegQueryKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0\win32 SUCCESSOUTLOOK.EXE RegOpenKey HKCU\Software\Classes\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0\win32 NAME NOT FOUNDOUTLOOK.EXE RegQueryValue HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0\win32\(Default) SUCCESSOUTLOOK.EXE RegCloseKey HKCR\TypeLib\{0006F062-0000-0000-C000-000000000046}\1.1\0\win32 SUCCESSOUTLOOK.EXE CreateFile C:\Program Files\Microsoft Office\Office14\OUTLCTL.DLL SUCCESSOUTLOOK.EXE ReadFile C:\Program Files\Microsoft Office\Office14\OUTLCTL.DLL SUCCESSOUTLOOK.EXE ReadFile C:\Program Files\Microsoft Office\Office14\OUTLCTL.DLL SUCCESS
Moral of the story? Comparing Process Monitor logs can give you answers .
Exchange Web Services with PowerShell! In the last few days I have written quite a few scripts that use Exchange Web Services Managed API 1.1 with PowerShell, you will agree that it is not as easy as writing Exchange Web Services Managed API 1.1 with C# and it takes a bit of getting used to it. One of my customer wanted to stamp a Retention Policy Tag on a folder in the Archived mailbox in Exchange 2010. What is a Retention Policy Tag?
Messaging records management (MRM) is the records management technology in Microsoft Exchange Server 2010 that helps organizations reduce the legal risks associated with e-mail. MRM makes it easier to keep the messages needed to comply with company policy, government regulations, or legal needs, and to remove content that has no legal or business value. This is accomplished through the use of retention policies or managed folders.
Retention policies, use retention tags to apply retention settings. You create retention tags, and link them to a retention policy. Mailboxes that have a retention policy applied to them are processed by the Managed Folder Assistant, a mailbox assistant that runs on a schedule and provisions retention tags in mailboxes. More details about understanding and creating retention policies and tags can be found in the following articles on MSDN:
Now that you understand what a Retention Policy Tag is, when applied to a folder stamps it with three important properties:
We will stamp the same properties using Exchange Web Services Managed API 1.1 in PowerShell and this is how it is done:
# The script requires the EWS managed API, which can be downloaded here:# http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=c3342fb3-fbcc-4127-becf-872c746840e1# This also requires PowerShell 2.0# Make sure the Import-Module command below matches the DLL location of the API.# This path must match the install location of the EWS managed API. Change it if needed.[string]$info = "White" # Color for informational messages[string]$warning = "Yellow" # Color for warning messages[string]$error = "Red" # Color for error messages[string]$LogFile = "C:\Temp\Log.txt" # Path of the Log Filefunction StampPolicyOnFolder($MailboxName){ Write-host "Stamping Policy on folder for Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Stamping Policy on folder for Mailbox Name:" + $MailboxName) #Change the user to Impersonate $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$MailboxName); #Search for the folder you want to stamp the property on $oFolderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(1) $oSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName,$FolderName) #Uncomment the line below if the folder is in the regular mailbox #$oFindFolderResults = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$oSearchFilter,$oFolderView) #Comment the line below and uncomment the line above if the folder is in the regular mailbox $oFindFolderResults = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::ArchiveMsgFolderRoot,$oSearchFilter,$oFolderView) if ($oFindFolderResults.TotalCount -eq 0) { Write-host "Folder does not exist in Mailbox:" $MailboxName -foregroundcolor $warning Add-Content $LogFile ("Folder does not exist in Mailbox:" + $MailboxName) } else { Write-host "Folder found in Mailbox:" $MailboxName -foregroundcolor $info #PR_POLICY_TAG 0x3019 $PolicyTag = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x3019,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary); #PR_RETENTION_FLAGS 0x301D $RetentionFlags = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301D,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer); #PR_RETENTION_PERIOD 0x301A $RetentionPeriod = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301A,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer); #Bind to the folder found $oFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$oFindFolderResults.Folders[0].Id) #Same as the value in the PR_RETENTION_FLAGS property $oFolder.SetExtendedProperty($RetentionFlags, 137) #Same as the value in the PR_RETENTION_PERIOD property $oFolder.SetExtendedProperty($RetentionPeriod, 1095) #Change the GUID based on your policy tag $PolicyTagGUID = new-Object Guid("{92186ff7-7f4d-4efa-a09b-bbdc5aee3908}"); $oFolder.SetExtendedProperty($PolicyTag, $PolicyTagGUID.ToByteArray()) $oFolder.Update() Write-host "Retention policy stamped!" -foregroundcolor $info Add-Content $LogFile ("Retention policy stamped!") } $service.ImpersonatedUserId = $null}#Change the name of the folder. This is the folder the properties will be stamped on.$FolderName = "My Folder"Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)# Set the Credentials$service.Credentials = new-object Microsoft.Exchange.WebServices.Data.WebCredentials("UserName","Password","Domain")# Change the URL to point to your cas server$service.Url= new-object Uri(http://YOUR-CAS-SERVER/EWS/Exchange.asmx)# Set $UseAutoDiscover to $true if you want to use AutoDiscover else it will use the URL set above$UseAutoDiscover = $false#Read data from the UserAccounts.txt.#This file must exist in the same location as the script.import-csv UserAccounts.txt | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } StampPolicyOnFolder($WindowsEmailAddress)}
function StampPolicyOnFolder($MailboxName){ Write-host "Stamping Policy on folder for Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Stamping Policy on folder for Mailbox Name:" + $MailboxName)
#Search for the folder you want to stamp the property on $oFolderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(1) $oSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName,$FolderName)
#Uncomment the line below if the folder is in the regular mailbox #$oFindFolderResults = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$oSearchFilter,$oFolderView)
#Comment the line below and uncomment the line above if the folder is in the regular mailbox $oFindFolderResults = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::ArchiveMsgFolderRoot,$oSearchFilter,$oFolderView)
if ($oFindFolderResults.TotalCount -eq 0) { Write-host "Folder does not exist in Mailbox:" $MailboxName -foregroundcolor $warning Add-Content $LogFile ("Folder does not exist in Mailbox:" + $MailboxName) } else { Write-host "Folder found in Mailbox:" $MailboxName -foregroundcolor $info
#PR_RETENTION_FLAGS 0x301D $RetentionFlags = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301D,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer); #PR_RETENTION_PERIOD 0x301A $RetentionPeriod = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301A,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer);
#Bind to the folder found $oFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$oFindFolderResults.Folders[0].Id) #Same as the value in the PR_RETENTION_FLAGS property $oFolder.SetExtendedProperty($RetentionFlags, 137)
#Same as the value in the PR_RETENTION_PERIOD property $oFolder.SetExtendedProperty($RetentionPeriod, 1095)
#Change the GUID based on your policy tag $PolicyTagGUID = new-Object Guid("{92186ff7-7f4d-4efa-a09b-bbdc5aee3908}"); $oFolder.SetExtendedProperty($PolicyTag, $PolicyTagGUID.ToByteArray())
$oFolder.Update() Write-host "Retention policy stamped!" -foregroundcolor $info Add-Content $LogFile ("Retention policy stamped!") } $service.ImpersonatedUserId = $null}
#Change the name of the folder. This is the folder the properties will be stamped on.$FolderName = "My Folder"
# Set the Credentials$service.Credentials = new-object Microsoft.Exchange.WebServices.Data.WebCredentials("UserName","Password","Domain")
# Change the URL to point to your cas server$service.Url= new-object Uri(http://YOUR-CAS-SERVER/EWS/Exchange.asmx)
#Read data from the UserAccounts.txt.#This file must exist in the same location as the script.import-csv UserAccounts.txt | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } StampPolicyOnFolder($WindowsEmailAddress)}
UserAccounts.txt contains the list of the users mailboxes that contain the folder that is set in the $FolderName variable in the script above.The First row denotes the Field Names. Format of the text file is as below:
It all started with a customer who wanted to know which servers in the DAG setup have the copies for a specific Exchange database. There were other properties like Activation Preferences that he was interested in too.
The easiest way to get the details about the copies for a database is by using the command (Get-MailboxDatabase "Database Name").DatabaseCopies. When I did this on the CAS Server using Exchange Management Shell, I got the following output:
[PS] C:\Windows\system32>$copies = (Get-MailboxDatabase "Mailbox Database 1056078029").DatabaseCopies[PS] C:\Windows\system32>$copiesDatabaseName : Mailbox Database 1056078029HostServerName : AKAS23474121ActivationPreference : 1ParentObjectClass : msExchPrivateMDBReplayLagTime : 00:00:00TruncationLagTime : 00:00:00AdminDisplayName :ExchangeVersion : 0.10 (14.0.100.0)DistinguishedName : CN=AKAS23474121,CN=Mailbox Database 1056078029,CN=Databases,CN=Exchange Administrative Group (FY DIBOHF23SPDLT),CN=Administrative Groups,CN=First Organization,CN=Microsoft Exchange,CN=Services, CN=Configuration,DC=DOM234741,DC=LOCALIdentity : Mailbox Database 1056078029\AKAS23474121Guid : 562cb271-7cee-4130-9582-e3ce0f08cb3dObjectCategory : DOM234741.LOCAL/Configuration/Schema/ms-Exch-MDB-CopyObjectClass : {top, msExchMDBCopy}WhenChanged : 7/21/2011 4:29:06 PMWhenCreated : 7/21/2011 4:29:06 PMWhenChangedUTC : 7/21/2011 10:59:06 AMWhenCreatedUTC : 7/21/2011 10:59:06 AMOrganizationId :OriginatingServer : AKAS23474118.DOM234741.LOCALIsValid : TrueDatabaseName : Mailbox Database 1056078029HostServerName : AKAS23474120ActivationPreference : 2ParentObjectClass : msExchPrivateMDBReplayLagTime : 00:00:00TruncationLagTime : 00:00:00AdminDisplayName :ExchangeVersion : 0.10 (14.0.100.0)...
[PS] C:\Windows\system32>$copies = (Get-MailboxDatabase "Mailbox Database 1056078029").DatabaseCopies[PS] C:\Windows\system32>$copiesDatabaseName : Mailbox Database 1056078029HostServerName : AKAS23474121ActivationPreference : 1ParentObjectClass : msExchPrivateMDBReplayLagTime : 00:00:00TruncationLagTime : 00:00:00AdminDisplayName :ExchangeVersion : 0.10 (14.0.100.0)DistinguishedName : CN=AKAS23474121,CN=Mailbox Database 1056078029,CN=Databases,CN=Exchange Administrative Group (FY DIBOHF23SPDLT),CN=Administrative Groups,CN=First Organization,CN=Microsoft Exchange,CN=Services, CN=Configuration,DC=DOM234741,DC=LOCALIdentity : Mailbox Database 1056078029\AKAS23474121Guid : 562cb271-7cee-4130-9582-e3ce0f08cb3dObjectCategory : DOM234741.LOCAL/Configuration/Schema/ms-Exch-MDB-CopyObjectClass : {top, msExchMDBCopy}WhenChanged : 7/21/2011 4:29:06 PMWhenCreated : 7/21/2011 4:29:06 PMWhenChangedUTC : 7/21/2011 10:59:06 AMWhenCreatedUTC : 7/21/2011 10:59:06 AMOrganizationId :OriginatingServer : AKAS23474118.DOM234741.LOCALIsValid : True
DatabaseName : Mailbox Database 1056078029HostServerName : AKAS23474120ActivationPreference : 2ParentObjectClass : msExchPrivateMDBReplayLagTime : 00:00:00TruncationLagTime : 00:00:00AdminDisplayName :ExchangeVersion : 0.10 (14.0.100.0)...
PS C:\Users\Superman> $copies=(Get-MailboxDatabase "Mailbox Database 1056078029").DatabaseCopiesPS C:\Users\Superman> $copiesMailbox Database 1056078029\AKAS23474121Mailbox Database 1056078029\AKAS23474120
I then did a Get-Member on the $copies variable first on the local CAS box and below is what I see
[PS] C:\Windows\system32>$copies | Get-Member -MemberType Property TypeName: Microsoft.Exchange.Data.Directory.SystemConfiguration.DatabaseCopyName MemberType Definition---- ---------- ----------ActivationPreference Property System.Int32 ActivationPreference {get;}AdminDisplayName Property System.String AdminDisplayName {get;}DatabaseName Property System.String DatabaseName {get;}DistinguishedName Property System.String DistinguishedName {get;}ExchangeVersion Property Microsoft.Exchange.Data.ExchangeObjectVersion ExchangeVersion {get;}Guid Property System.Guid Guid {get;}HostServerName Property System.String HostServerName {get;}Identity Property Microsoft.Exchange.Data.ObjectId Identity {get;}IsValid Property System.Boolean IsValid {get;}ObjectCategory Property Microsoft.Exchange.Data.Directory.ADObjectId ObjectCategory {get;}...
[PS] C:\Windows\system32>$copies | Get-Member -MemberType Property TypeName: Microsoft.Exchange.Data.Directory.SystemConfiguration.DatabaseCopyName MemberType Definition---- ---------- ----------ActivationPreference Property System.Int32 ActivationPreference {get;}AdminDisplayName Property System.String AdminDisplayName {get;}DatabaseName Property System.String DatabaseName {get;}DistinguishedName Property System.String DistinguishedName {get;}ExchangeVersion Property Microsoft.Exchange.Data.ExchangeObjectVersion ExchangeVersion {get;}Guid Property System.Guid Guid {get;}HostServerName Property System.String HostServerName {get;}Identity Property Microsoft.Exchange.Data.ObjectId Identity {get;}IsValid Property System.Boolean IsValid {get;}ObjectCategory Property Microsoft.Exchange.Data.Directory.ADObjectId ObjectCategory {get;}
...
When I do Get-Member on the $copies variable on the Remote machine below is what I see
PS C:\Users\Superman> $copies | Get-Member -MemberType Properties TypeName: System.StringName MemberType Definition---- ---------- ----------Length Property System.Int32 Length {get;}
Notice the difference in TypeName. On the Local box it is “Microsoft.Exchange.Data.Directory.SystemConfiguration.DatabaseCopy” and on the Remote box it is “System.String”. Why does this happen?
When you run remote commands that generate output, the command output is transmitted across the network back to the local computer. Because most live Microsoft .NET Framework objects (such as the objects that Windows PowerShell cmdlets return) cannot be transmitted over the network, the live objects are "serialized". In other words, the live objects are converted into XML representations of the object and its properties. Then, the XML-based serialized object is transmitted across the network. On the local computer, Windows PowerShell receives the XML-based serialized object and "deserializes" it by converting the XML-based object into a standard .NET Framework object. However, the deserialized object is not a live object. It is a snapshot of the object at the time that it was serialized. This is how an MSDN article defines serialization in .NET framework. More details can be found in the article “How objects are sent to and from remote sessions”.
Luckily the data that we needed was available buy just using the Get-MailboxDatabase cmd-let by accessing the DatabaseCopies Property. To prove what is said above and show how to do it in C#, below is the code that I wrote:
using System;using System.Collections.Generic;using System.Text;using System.Management.Automation;using System.Management.Automation.Runspaces;using System.Management.Automation.Remoting;using System.Collections.ObjectModel;using System.Security;using System.Collections;namespace CallingPSCmdlet{ class Program { static void Main(string[] args) { string password = "Passowrd"; string userName = "Domain\\UserName"; System.Uri uri = new Uri(“http://CAS-SERVER/powershell?serializationLevel=Full”); System.Security.SecureString securePassword = String2SecureString(password); System.Management.Automation.PSCredential creds = new System.Management.Automation.PSCredential(userName, securePassword); Runspace runspace = System.Management.Automation.Runspaces.RunspaceFactory.CreateRunspace(); PowerShell powershell = PowerShell.Create(); PSCommand command = new PSCommand(); command.AddCommand("New-PSSession"); command.AddParameter("ConfigurationName", "Microsoft.Exchange"); command.AddParameter("ConnectionUri", uri); command.AddParameter("Credential", creds); command.AddParameter("Authentication", "Default"); PSSessionOption sessionOption = new PSSessionOption(); sessionOption.SkipCACheck = true; sessionOption.SkipCNCheck = true; sessionOption.SkipRevocationCheck = true; command.AddParameter("SessionOption", sessionOption); powershell.Commands = command; try { // open the remote runspace runspace.Open(); // associate the runspace with powershell powershell.Runspace = runspace; // invoke the powershell to obtain the results Collection<PSSession> result = powershell.Invoke<PSSession>(); foreach (ErrorRecord current in powershell.Streams.Error) Console.WriteLine("The following Error happen when opening the remote Runspace: " + current.Exception.ToString() + " | InnerException: " + current.Exception.InnerException); if (result.Count != 1) throw new Exception("Unexpected number of Remote Runspace connections returned."); // Set the runspace as a local variable on the runspace powershell = PowerShell.Create(); command = new PSCommand(); command.AddCommand("Set-Variable"); command.AddParameter("Name", "ra"); command.AddParameter("Value", result[0]); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke(); // First import the cmdlets in the current runspace (using Import-PSSession) command = new PSCommand(); command.AddScript("Import-PSSession -Session $ra"); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke(); // Now call the Get-MaiboxDatabase command = new PSCommand(); command.AddCommand("Get-MailboxDatabase"); //Change the name of the database command.AddParameter("Identity", "Mailbox Database XXXXXXXX"); powershell.Commands = command; powershell.Runspace = runspace; Collection<PSObject> results = new Collection<PSObject>(); results = powershell.Invoke(); PSMemberInfo Member = null; foreach (PSObject oResult in results) { foreach (PSMemberInfo psMember in oResult.Members) { Member = psMember; DumpProperties(ref Member); } } results = null; Member = null; } finally { // dispose the runspace and enable garbage collection runspace.Dispose(); runspace = null; // Finally dispose the powershell and set all variables to null to free up any resources. powershell.Dispose(); powershell = null; } } //Method to Dump out the Properties public static void DumpProperties(ref PSMemberInfo psMember) { // Only look at Properties if (psMember.MemberType.ToString() == "Property") { switch (psMember.Name) { case "ActivationPreference": case "DatabaseCopies": if (psMember.Value != null) { PSObject oPSObject; ArrayList oArrayList; oPSObject = (PSObject)psMember.Value; oArrayList = (ArrayList)oPSObject.BaseObject; Console.WriteLine("Member Name:" + psMember.Name); Console.WriteLine("Member Type:" + psMember.TypeNameOfValue); Console.WriteLine("----------------------"); foreach (string item in oArrayList) { Console.WriteLine(item); } Console.WriteLine("----------------------"); } break; } } } private static SecureString String2SecureString(string password) { SecureString remotePassword = new SecureString(); for (int i = 0; i < password.Length; i++) remotePassword.AppendChar(password[i]); return remotePassword; } }}
using System;using System.Collections.Generic;using System.Text;using System.Management.Automation;using System.Management.Automation.Runspaces;using System.Management.Automation.Remoting;using System.Collections.ObjectModel;using System.Security;using System.Collections;namespace CallingPSCmdlet{ class Program { static void Main(string[] args) { string password = "Passowrd"; string userName = "Domain\\UserName";
System.Uri uri = new Uri(“http://CAS-SERVER/powershell?serializationLevel=Full”); System.Security.SecureString securePassword = String2SecureString(password); System.Management.Automation.PSCredential creds = new System.Management.Automation.PSCredential(userName, securePassword);
Runspace runspace = System.Management.Automation.Runspaces.RunspaceFactory.CreateRunspace();
PowerShell powershell = PowerShell.Create(); PSCommand command = new PSCommand(); command.AddCommand("New-PSSession"); command.AddParameter("ConfigurationName", "Microsoft.Exchange"); command.AddParameter("ConnectionUri", uri); command.AddParameter("Credential", creds); command.AddParameter("Authentication", "Default"); PSSessionOption sessionOption = new PSSessionOption(); sessionOption.SkipCACheck = true; sessionOption.SkipCNCheck = true; sessionOption.SkipRevocationCheck = true; command.AddParameter("SessionOption", sessionOption); powershell.Commands = command;
try { // open the remote runspace runspace.Open(); // associate the runspace with powershell powershell.Runspace = runspace; // invoke the powershell to obtain the results Collection<PSSession> result = powershell.Invoke<PSSession>(); foreach (ErrorRecord current in powershell.Streams.Error) Console.WriteLine("The following Error happen when opening the remote Runspace: " + current.Exception.ToString() + " | InnerException: " + current.Exception.InnerException); if (result.Count != 1) throw new Exception("Unexpected number of Remote Runspace connections returned.");
// Set the runspace as a local variable on the runspace powershell = PowerShell.Create(); command = new PSCommand(); command.AddCommand("Set-Variable"); command.AddParameter("Name", "ra"); command.AddParameter("Value", result[0]); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke();
// First import the cmdlets in the current runspace (using Import-PSSession) command = new PSCommand(); command.AddScript("Import-PSSession -Session $ra"); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke();
// Now call the Get-MaiboxDatabase
command = new PSCommand(); command.AddCommand("Get-MailboxDatabase"); //Change the name of the database command.AddParameter("Identity", "Mailbox Database XXXXXXXX"); powershell.Commands = command; powershell.Runspace = runspace; Collection<PSObject> results = new Collection<PSObject>(); results = powershell.Invoke(); PSMemberInfo Member = null;
foreach (PSObject oResult in results) { foreach (PSMemberInfo psMember in oResult.Members) { Member = psMember; DumpProperties(ref Member); } } results = null; Member = null; } finally { // dispose the runspace and enable garbage collection runspace.Dispose(); runspace = null;
// Finally dispose the powershell and set all variables to null to free up any resources. powershell.Dispose(); powershell = null; } }
//Method to Dump out the Properties public static void DumpProperties(ref PSMemberInfo psMember) { // Only look at Properties if (psMember.MemberType.ToString() == "Property") { switch (psMember.Name) { case "ActivationPreference": case "DatabaseCopies": if (psMember.Value != null) { PSObject oPSObject; ArrayList oArrayList; oPSObject = (PSObject)psMember.Value; oArrayList = (ArrayList)oPSObject.BaseObject; Console.WriteLine("Member Name:" + psMember.Name); Console.WriteLine("Member Type:" + psMember.TypeNameOfValue); Console.WriteLine("----------------------"); foreach (string item in oArrayList) { Console.WriteLine(item); } Console.WriteLine("----------------------"); } break; } } } private static SecureString String2SecureString(string password) { SecureString remotePassword = new SecureString(); for (int i = 0; i < password.Length; i++) remotePassword.AppendChar(password[i]); return remotePassword; } }}
Member Name:DatabaseCopiesMember Type:Deserialized.Microsoft.Exchange.Data.Directory.SystemConfiguration.DatabaseCopy[]----------------------Mailbox Database 1056078029\AKAS23474121Mailbox Database 1056078029\AKAS23474120----------------------Member Name:ActivationPreferenceMember Type:Deserialized.System.Collections.Generic.KeyValuePair`2[[Microsoft.Exchange.Data.Directory.ADObjectId, Microsoft.Exchange.Data.Directory, Version=14.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35],[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]][]----------------------[AKAS23474121, 1][AKAS23474120, 2]----------------------
In this case the customer wanted to know the Item count, size of all the item in the Inbox that were older than 5 years. As usual, the requirement was to run the script against multiple mailboxes. I could have easily written this in C# in no time but preferred to do this in PowerShell as:
The downside is that you do not get the IntelliSense that you get with Visual Studio. With all that said lets get down straight to code. This example shows the use of Impersonation, SearchFilter, FindItems, Paging in Exchange Web Services. Create a .PS1 as below:
# The script requires the EWS managed API, which can be downloaded here:# http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=c3342fb3-fbcc-4127-becf-872c746840e1# This also requires PowerShell 2.0# Make sure the Import-Module command below matches the DLL location of the API.# This path must match the install location of the EWS managed API. Change it if needed.[string]$info = "White" # Color for informational messages[string]$warning = "Yellow" # Color for warning messages[string]$errorMessage = "Red" # Color for error messages[string]$LogFile = "C:\Temp\Log.txt" # Path of the Log Filefunction GetItemCount($MailboxName){ Add-Content $LogFile ("-------------------------------------------------" ) Write-host "Searching Inbox for Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Searching Inbox for Mailbox Name:" + $MailboxName) #Change the user to Impersonate $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$MailboxName); #Number of Items to Get $pageSize =50 $Offset = 0 $MoreItems =$true $ItemCount=0 $ItemSize=0 while ($MoreItems) { #Setup the View to get a limited number of Items at one time $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($pageSize,$Offset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow #Get the Size of the Items $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.ItemSchema]::Size); #Create the Search Filter - Subtract 5 Years from todays date. $oSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsLessThanOrEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived, ([System.Datetime]::now).AddMonths(-60)) $oFindItems = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$oSearchFilter,$itemView) $ItemCount += $oFindItems.Items.Count $i=0 #Loop through the items returned and add up the size foreach ($Item in $oFindItems.Items) { $i++ $ItemSize += $Item.Size Write-Progress -activity "Adding Item Sizes" -status "Percent added: " -PercentComplete (($i /$oFindItems.Items.Count) * 100) } if ($oFindItems.MoreAvailable -eq $false) {$MoreItems = $false} if ($MoreItems) {$Offset += $pageSize} } Write-host "Items Found in Inbox:" $ItemCount -foregroundcolor $info Add-Content $LogFile ("Items Found in Inbox:" + $ItemCount ) Write-host "Size of Items:" ($ItemSize / 1Mb) "MB" -foregroundcolor $info Add-Content $LogFile ("Size of Items:" + ($ItemSize / 1Mb) + " MB" ) Add-Content $LogFile ("-------------------------------------------------" ) $service.ImpersonatedUserId = $null}#Change the path if neededImport-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)# Set the Credentials$service.Credentials = new-object Microsoft.Exchange.WebServices.Data.WebCredentials("serviceAccount","Password","Domain")# Change the URL to point to your cas server$service.Url= new-object Uri(“http://CAS-SERVER/EWS/Exchange.asmx”)# Set $UseAutoDiscover to $true if you want to use AutoDiscover else it will use the URL set above$UseAutoDiscover = $falseGet-Mailbox | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $errorMessage; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } GetItemCount($WindowsEmailAddress)}
[string]$info = "White" # Color for informational messages[string]$warning = "Yellow" # Color for warning messages[string]$errorMessage = "Red" # Color for error messages[string]$LogFile = "C:\Temp\Log.txt" # Path of the Log File
function GetItemCount($MailboxName){ Add-Content $LogFile ("-------------------------------------------------" ) Write-host "Searching Inbox for Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Searching Inbox for Mailbox Name:" + $MailboxName)
#Number of Items to Get $pageSize =50 $Offset = 0 $MoreItems =$true $ItemCount=0 $ItemSize=0
while ($MoreItems) { #Setup the View to get a limited number of Items at one time $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($pageSize,$Offset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow
#Get the Size of the Items $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.ItemSchema]::Size); #Create the Search Filter - Subtract 5 Years from todays date. $oSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsLessThanOrEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived, ([System.Datetime]::now).AddMonths(-60)) $oFindItems = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$oSearchFilter,$itemView) $ItemCount += $oFindItems.Items.Count $i=0
#Loop through the items returned and add up the size foreach ($Item in $oFindItems.Items) { $i++ $ItemSize += $Item.Size Write-Progress -activity "Adding Item Sizes" -status "Percent added: " -PercentComplete (($i /$oFindItems.Items.Count) * 100) } if ($oFindItems.MoreAvailable -eq $false) {$MoreItems = $false} if ($MoreItems) {$Offset += $pageSize} }
Write-host "Items Found in Inbox:" $ItemCount -foregroundcolor $info Add-Content $LogFile ("Items Found in Inbox:" + $ItemCount ) Write-host "Size of Items:" ($ItemSize / 1Mb) "MB" -foregroundcolor $info Add-Content $LogFile ("Size of Items:" + ($ItemSize / 1Mb) + " MB" ) Add-Content $LogFile ("-------------------------------------------------" ) $service.ImpersonatedUserId = $null}
#Change the path if neededImport-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)
# Set the Credentials$service.Credentials = new-object Microsoft.Exchange.WebServices.Data.WebCredentials("serviceAccount","Password","Domain")
# Change the URL to point to your cas server$service.Url= new-object Uri(“http://CAS-SERVER/EWS/Exchange.asmx”)
Get-Mailbox | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $errorMessage; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } GetItemCount($WindowsEmailAddress)}
To Run the script: 1)Open the Exchange Management Shell 2)Navigate to the location where the script file is. 3)Type in .\ScriptName.PS1 and hit enter to execute the script
On Wednesday, 7/27, the Exchange Sustained Engineering team re-released Exchange 2010 SP1 RU4. This release is being tracked by KB 2579150 and will contain a download file that is unique from the previous release. This updated rollup contains all of the changes that were in the original rollup with the exception of the regression because if which the previous RU4 was pulled back.
For customers who have the previous version of RU4 and the IU 2581545 installed, there is technically no need to install this updated version of RU4 as the combination of those two releases is functionally equivalent to the updated RU4 dated Wednesday, 7/27. The only thing a customer would gain by installing the updated rollup 4 would be performing the IU maintenance now that they would need to perform later when installing RU5.
More details on the Exchange 2010 SP1 RU4 re-release can be found here and here. As for RU5, it is expected to be released by the end of August 2011.
A customer of mine wanted to create a folder under Inbox for thousands of mailboxes. He also wanted to use PowerShell to do it. What better way that using Exchange Web Service(Managed API) with Impersonation to do the job.
The list of the users are available in a text file. The First row denotes the Field Names.The file is named UserAccounts.txt. Format of the text file is as below:
# The script requires the EWS managed API, which can be downloaded here:# http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=c3342fb3-fbcc-4127-becf-872c746840e1# This also requires PowerShell 2.0# Make sure the Import-Module command below matches the DLL location of the API.# This path must match the install location of the EWS managed API. Change it if needed.[string]$info = "White" # Color for informational messages[string]$warning = "Yellow" # Color for warning messages[string]$error = "Red" # Color for error messages[string]$LogFile = "C:\Temp\Log.txt" # Path of the Log Filefunction CreateFolder($MailboxName){ Write-host "Creating Folder for Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Creating Folder for Mailbox Name:" + $MailboxName) #Change the user to Impersonate $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$MailboxName); #Create the folder object $oFolder = new-object Microsoft.Exchange.WebServices.Data.Folder($service) $oFolder.DisplayName = $FolderName #Call Save to actually create the folder $oFolder.Save([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox) Write-host "Folder Created for " $MailboxName -foregroundcolor $warning Add-Content $LogFile ("Folder Created for " + $MailboxName) $service.ImpersonatedUserId = $null}#Change the name of the folder$FolderName = "My Folder"Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)# Set the Credentials$service.Credentials = new-object Microsoft.Exchange.WebServices.Data.WebCredentials("serviceAccount","Password","Domain")# Change the URL to point to your cas server$service.Url= new-object Uri(http://CAS-Server/EWS/Exchange.asmx)# Set $UseAutoDiscover to $true if you want to use AutoDiscover else it will use the URL set above$UseAutoDiscover = $false#Read data from the UserAccounts.txtimport-csv UserAccounts.txt | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } CreateFolder($WindowsEmailAddress)}
function CreateFolder($MailboxName){ Write-host "Creating Folder for Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Creating Folder for Mailbox Name:" + $MailboxName)
#Create the folder object
$oFolder = new-object Microsoft.Exchange.WebServices.Data.Folder($service) $oFolder.DisplayName = $FolderName
#Call Save to actually create the folder $oFolder.Save([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox) Write-host "Folder Created for " $MailboxName -foregroundcolor $warning Add-Content $LogFile ("Folder Created for " + $MailboxName)
$service.ImpersonatedUserId = $null}
#Change the name of the folder$FolderName = "My Folder"
# Change the URL to point to your cas server$service.Url= new-object Uri(http://CAS-Server/EWS/Exchange.asmx)
#Read data from the UserAccounts.txtimport-csv UserAccounts.txt | foreach-object { $WindowsEmailAddress = $_.WindowsEmailAddress.ToString() if ($UseAutoDiscover -eq $true) { Write-host "Autodiscovering.." -foregroundcolor $info $UseAutoDiscover = $false $service.AutodiscoverUrl($WindowsEmailAddress) Write-host "Autodiscovering Done!" -foregroundcolor $info Write-host "EWS URL set to :" $service.Url -foregroundcolor $info } #To catch the Exceptions generated trap [System.Exception] { Write-host ("Error: " + $_.Exception.Message) -foregroundcolor $error; Add-Content $LogFile ("Error: " + $_.Exception.Message); continue; } CreateFolder($WindowsEmailAddress)}
A few articles on working with Exchange Online (Office 365 for enterprises, Live@edu) with Windows Powershell.
Install and Configure Windows PowerShellhttp://technet.microsoft.com/en-us/exchangelabshelp/cc952756
Connect Windows PowerShell to the Servicehttp://technet.microsoft.com/en-us/exchangelabshelp/cc952755
Reference to Available PowerShell Cmdlets in Exchange Onlinehttp://technet.microsoft.com/en-us/exchangelabshelp/dd575549
Administrator Role Groups in Exchange Onlinehttp://technet.microsoft.com/en-us/exchangelabshelp/ee441216
Role Based Access Control in Exchange Onlinehttp://technet.microsoft.com/en-us/exchangelabshelp/dd207274
Role Assignment Policies in Exchange Onlinehttp://technet.microsoft.com/en-us/exchangelabshelp/ee424427
This was a fun issue to work on! Some background, the customer was using System.Net.Mail to send out mails via Exchange 2007 using port 25 and port 587, all was working fine and then some updates were installed and he was no longer able to send out mails via port 587. He started to get the following exception:
Exception:Object reference not set to an instance of an object.System.Net Error: at System.Net.Mail.SmtpConnection.GetConnection(String host, Int32 port) at System.Net.Mail.SmtpTransport.GetConnection(String host, Int32 port) at System.Net.Mail.SmtpClient.GetConnection() at System.Net.Mail.SmtpClient.Send(MailMessage message)
The code was very simple:
static void Main(string[] args) { SmtpClient _client = new SmtpClient(); using (MailMessage _message = new MailMessage()) { _message.Subject = "Test message"; _message.Body = "Test message"; _message.From = new MailAddress("support@xyz.com"); _message.To.Add("akashb@xyz.com"); _client.Send(_message); } }
What we were also told was that if in the code the SmtpClient.TargetName Property was set to blank then everything worked fine! What is this property? This property gets or sets the Service Provider Name (SPN) to use for authentication when using extended protection.
We made sure we have the exact same setting on the Exchange 2007 Receive Connectors, updated the client with all the updates that were installed in the customer’s environment but we initially could not reproduce the issue. It did not matter if we set the TargetName to blank or not. We finally did reproduce the issue after I put the client machine where the code was running in the same domain as the Exchange Server and then setting the TargetName to blank did matter. Now the next step was to find out the update that caused the code to fail. We did get a hint from the SmtpClient.TargetName property and started to look for the patch that could cause the issue.
Luckily, the search did not last long and we were able to identify the patch that caused the failure!
Description of the rollup update for the .NET Framework 3.5 Service Pack 1 and the .NET Framework 2.0 Service Pack 2 on Windows XP and on Windows Server 2003 (976765 and 980773): June 8, 2010http://support.microsoft.com/kb/982167
This updates the NLC on the system to support Extended Protection. When we uninstalling the update for KB 980773 from the system the code started to work. But now we had a few questions that we needed answer for:1)Is there something wrong with Exchange 2007?2)Does Exchange 2007 not understand the request for Authentication?3)Why does it still work with port 25 and fails only with port 587?
We then decide to take a verbose log on the Exchange Receive connector and below is what we find:
>,"220 EX-01.internal.xyz.com Microsoft ESMTP MAIL Service ready at Mon, 15 May 2011 08:59:37 -0500",<,EHLO akashb,>,250-EX-01.internal.xyz.com Hello [192.168.111.111],>,250-SIZE 10485760,>,250-PIPELINING,>,250-DSN,>,250-ENHANCEDSTATUSCODES,>,250-STARTTLS,>,250-AUTH GSSAPI NTLM LOGIN,>,250-8BITMIME,>,250-BINARYMIME,>,250 CHUNKING,<,AUTH gssapi,>,334 <authentication response>,>,334 <authentication response>,*,,Inbound Negotiate failed because of IllegalMessage>,535 5.7.3 Authentication unsuccessful,-,,Remote
Firstly the OS needs to be configured to support Extended Protection and secondly, support for Extended protection was introduced in Exchange 2007 SP 3. The best would be to have Exchange 2007 SP 3 RU 3 v2 installed. In this case it turns out that neither the customer nor I had Exchange 2007 SP 3 RU 3 v2. Make sure you have your SPN’s right. The below article talks about Extended Protection.Extended Protection for Authenticationhttp://support.microsoft.com/kb/968389
After Exchange 2007 SP 3 RU 3 v2 is installed you get a new property “ExtendedProtectionPolicy” for the Receive connector. You can then use the Set-ReceiveConnector cmd-let and set the value for ExtendedProtectionPolicy to “Allow”. This also requires that RequireTLS is set to true on the connector and you must also set EnableSsl = true in the .Net code. So this answers the questions 1 & 2.
Now for the question 3, why does it still work with port 25 and fails only with port 587?
That was easy, the answer lies in EHLO. The Default receive connector (Port 25) did not have GSSAPI enabled and hence it was defaulting to NTLM(which works) instead using Negotiate. In the failing case, Negotiate was used with Extended protection, Exchange did not return the expected data and hence the error.
-Enjoy
This is the first time I ran into this issue with Exchange Web Services while sending out a Pull Subscription request! The confusing part was that apart from the subscribe request everything else was working as expected. We could connect to the mailbox using EWS Editor, browse the Inbox and other folders, Delete Items and so on…
Below is the Request that was sent out:
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Header> <t:RequestServerVersion Version="Exchange2010" /> </soap:Header> <soap:Body> <m:Subscribe> <m:PullSubscriptionRequest> <t:FolderIds> <t:DistinguishedFolderId Id="inbox" /> </t:FolderIds> <t:EventTypes> <t:EventType>CopiedEvent</t:EventType> <t:EventType>CreatedEvent</t:EventType> <t:EventType>DeletedEvent</t:EventType> <t:EventType>ModifiedEvent</t:EventType> <t:EventType>MovedEvent</t:EventType> <t:EventType>NewMailEvent</t:EventType> </t:EventTypes> <t:Timeout>90</t:Timeout> </m:PullSubscriptionRequest> </m:Subscribe> </soap:Body></soap:Envelope>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Header> <h:ServerVersionInfo MajorVersion="14" MinorVersion="1" MajorBuildNumber="289" MinorBuildNumber="6" Version="Exchange2010_SP1" xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types" xmlns="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"/> </s:Header> <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <m:SubscribeResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"> <m:ResponseMessages> <m:SubscribeResponseMessage ResponseClass="Error"> <m:MessageText>The mailbox database is temporarily unavailable.</m:MessageText> <m:ResponseCode>ErrorMailboxStoreUnavailable</m:ResponseCode> <m:DescriptiveLinkKey>0</m:DescriptiveLinkKey> </m:SubscribeResponseMessage> </m:ResponseMessages> </m:SubscribeResponse> </s:Body></s:Envelope>
Looking at the ETL trace it was found that the underlying error was MAPI_E_NETWORK_ERROR (0x80040115) and we also got to the function that was failing. This helped in searching the Source Code for Exchange 2010, the intent was to find out if there was a way we could reproduce a similar error using the Exchange Management Shell. Luckily we discovered that Move-Mailbox cmd-let would also do something similar! We discovered that the customer was unable to move mailboxes from one database to another on the same Mailbox Server and was getting the error MapiExceptionNetworkError: Unable to make connection to the server. (hr=0x800004005, ec=2423). Which server are we taking about?
In this case it turns out that the CAS was not able to contact the Mailbox Server via NetBIOS name. Pinging the Mailbox with the NetBIOS name failed! Pinging the Mailbox with the FQDN worked! What was done to fix the problem?
In this case a DNS suffix was added (DNS tab in the Advanced TCP/IP Settings) and now pinging the Mailbox with the NetBIOS name WORKED! Did the Move-Mailbox work? YES! Did the Subscribe Request work? YES!
Do you use the voting feature in Outlook 2010? Do you “Save” the item in the Application_ItemSend event? Does everything work as expected?
Private Sub Application_ItemSend(ByVal Item As Object, Cancel As Boolean) Item.SaveEnd Sub
I don’t think so. If you have not yet noticed, you loose the ability to track the voting responses that you have received. It also breaks the relationship between the Read Receipt(if you have requested one) and the sent mail message.
We now have a hotfix that addresses this issue and it can now we download from:
Description of the Outlook 2010 hotfix package (outlook-x-none)http://support.microsoft.com/kb/2547227
However we have a scenario where this is still broken! If the user right clicks on a file and does a Send To – Mail Recipient and then adds the voting options, we loose the ability to track voting responses. We have identified the problem and the fix is in progress and should be out soon.
Note: If the mail item is not saved OR you do not hook into the Application_ItemSend then don’t worry, everything will work as expected.
What we found out was that the Voting Response tracking was still broken when the user Right clicks on an file on the desktop/Hard drive and select "Send To" -> Mail Recipient and then added the voting options to the message. Fortunately this issue has also been resolved in the July 11th update.
Description of the Outlook 2010 hotfix package (Outlook-x-none.msp): July 11, 2011http://support.microsoft.com/kb/2544027
Strange but TRUE! This work in Exchange 2007 but does not in Exchange 2010. Below is the request and response that I get from a Exchange 2010 server for the ExpandGroup Exchange Web Services call:
Request:
<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Header> <t:RequestServerVersion Version="Exchange2010" /> </soap:Header> <soap:Body> <m:ExpandDL> <m:Mailbox> <t:EmailAddress>TestDDL@bhargava.com</t:EmailAddress> </m:Mailbox> </m:ExpandDL> </soap:Body> </soap:Envelope>
<?xml version="1.0" encoding="utf-8"?> <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Header> <h:ServerVersionInfo MajorVersion="14" MinorVersion="1" MajorBuildNumber="269" MinorBuildNumber="0" Version="Exchange2010_SP1" xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types" xmlns="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> </s:Header> <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <m:ExpandDLResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"> <m:ResponseMessages> <m:ExpandDLResponseMessage ResponseClass="Success"> <m:ResponseCode>NoError</m:ResponseCode> <m:DLExpansion TotalItemsInView="0" IncludesLastItemInRange="true" /> </m:ExpandDLResponseMessage> </m:ResponseMessages> </m:ExpandDLResponse> </s:Body> </s:Envelope>
The only workaround I know is of using LDAP to get the membership of a Dynamic Distribution Group if it fits your requirements.
This issue has been fixed in Exchange 2010 SP 1 RU 5. Exchange 2010 SP 1 RU 5 should be available in August 2011.Unfortunately this has been pushed to Exchange 2010 SP 1 RU 6. However, in case you need an Interim Update for SP 1 RU 4 OR RU 5 when it becomes available, open up a Service Request with Microsoft Support to obtain one.
Below are a few articles that will help you get started with developing solution for Office 365Exchange Online Overviewhttp://msdn.microsoft.com/en-us/Office365TrainingCourse_1V_3
Developing Messaging Solutions for Exchange Onlinehttp://msdn.microsoft.com/en-us/office365trainingcourse_session-10_unit
Exchange Online Technical Articleshttp://msdn.microsoft.com/en-us/library/gg193994(EXCHG.140).aspx
Office 365 Developer Training Coursehttp://msdn.microsoft.com/en-us/Office365TrainingCourse
This was an interesting issue I ran into so thought of sharing it. One of my customer had written and application to synchronize contacts using Exchange Web Services, on some mailboxes he was getting an “Internal Server Error” while trying to update contacts.
Error:Encountered unhandled exception: Microsoft.Exchange.Services.Core.Types.InternalServerErrorException: An internal server error occurred. The operation failed. ---> System.ArgumentNullException: Value cannot be null.Parameter name: address at Microsoft.Exchange.Data.SmtpAddress.IsValidSmtpAddress(String address) at Microsoft.Exchange.Data.Storage.JunkEmailRule.AddTrustedContact(String email, ADRecipientSession recipSession) at Microsoft.Exchange.Data.Storage.Contact.UpdateJunkEmailContacts() at Microsoft.Exchange.Data.Storage.Item.SaveInternal(SaveMode saveMode, Boolean commit) at Microsoft.Exchange.Services.Core.ServiceCommandBase.<>c__DisplayClass7.<SaveXsoItem>b__6(SaveMode saveModeDelegate) at Microsoft.Exchange.Services.Core.ServiceCommandBase.<>c__DisplayClass4.<ExecuteItemSave>b__3() at Microsoft.Exchange.Services.Core.ServiceCommandBase.ExecuteStoreObjectSave(SaveStoreObject saveStoreObject, Boolean useItemError) at Microsoft.Exchange.Services.Core.ServiceCommandBase.ExecuteItemSave(SaveItem saveItem, ConflictResolutionType conflictResolutionType) at Microsoft.Exchange.Services.Core.ServiceCommandBase.SaveXsoItem(Item xsoItem, SaveItem saveItem, ConflictResolutionType conflictResolutionType, PropertyDefinition[] propsToLoad)...
The exception did mention about “Value cannot be null” for the parameter “address” so we made sure that the email address was not blank.We tried to reproduce the issue with the same soap request but we were unable to. Although the format of the email address given in the contact was not correct we had no issue updating the contact in our test server. The strange part was that it was happening on only on a few mailboxes! What could be different?The call stack above gave me what I needed! Why are we trying to add a Trusted Contact? What has the Junk Email Rule got to do with it?
Digging on the exchange 2010 side I came across cmd-let’s called “Get-MailboxJunkEmailConfiguration” and “Set-MailboxJunkEmailConfiguration”. To my joy the “Set-MailboxJunkEmailConfiguration” had a parameter called ContactsTrusted(System.Boolean) and it was sent to False in my environment. I set to true and then ran the test again and guess what?
I get the same exception the customer was getting. The next step was to alter the email addresses in the SOAP request to valid ones and then, yes you guessed it right! Everything worked fine now!So you can basically have a crappy email address for a contact and get away with it if the ContactsTrusted is set to false, but if it’s set to true – You better have you email addresses for your contact right!
A few days ago I ran into an issue where the customer had written an add-in for Outlook 2010 and was working with the WordEditor property of the Inspector. The add-in was to be deployed to machines which had ONLY Outlook 2010 installed. Would this work?Yes, It should! In this case we got an exception that says that the file Microsoft.Office.Interop.Word.dll could not be found. Digging deeper into the issue we found out that it was an issue with the PIA Installer for Office 2010. The Installed did not install the Microsoft.Office.Interop.Word.dll if Word was not installed on the machine.
Is that what the installer should be doing? NO. The Office 2007 PIA installed worked just fine and even if only Outlook was installed on the machine it installed the PIA for Outlook as well as for Word. Yes, this is an issue with the Office 2010 PIA installer and will be fixed!
Now what? What are my options to solve the problem?
The only two that I see are:
1) Distribute the Microsoft.Office.Interop.Word.dll along with the setup: This is not something that we recommend. It is recommended to always install the PIA using the Official Installer. Now since the installed is not doing it’s job, look at option 2
2) Use Late Binding: I was able to get the add-in to work without the word PIA. Writing late binding code could be a pain but it is the only other solution that worked!
In the sample code below we are setting the subject and the body of the message to “This text was set by the add-in”. Notice the code to insert the text “This text was set by the add-in” into the body of the message late binding and WordEditor object.
if (activeInspector.CurrentItem is Outlook.MailItem){ Outlook.MailItem mail = ((Outlook.MailItem)activeInspector.CurrentItem); mail.Subject = "This text was set by the add-in"; if (activeInspector.EditorType == Outlook.OlEditorType.olEditorWord) { object wordDoc = activeInspector.WordEditor; object wordWin = null; IEnumerator wordDocEnumerator = null; if (null != wordDoc) { object oWindows = wordDoc.GetType().InvokeMember("Windows", System.Reflection.BindingFlags.GetProperty, null, wordDoc, null); if (null != oWindows) { wordDocEnumerator = (IEnumerator)oWindows.GetType().InvokeMember("GetEnumerator", System.Reflection.BindingFlags.InvokeMethod, null, oWindows, null); } if (wordDocEnumerator.MoveNext()) { wordWin = wordDocEnumerator.Current; object oSelection = wordWin.GetType().InvokeMember("Selection", System.Reflection.BindingFlags.GetProperty, null, wordWin, null); oSelection.GetType().InvokeMember("TypeText", System.Reflection.BindingFlags.InvokeMethod, null, oSelection, new object[] { "This text was set by the add-in" }); } else { MessageBox.Show("Could not find the word editor window to insert the text body into"); } } } else { MessageBox.Show("The editor type was not word, so the add-in cannot continue"); }}
Some good articles on binding
I will let you know when the issue with the Office 2010 PIA installed is fixed. Enjoy!
One of my customers was migrating his Exchange Client Extension code to an Outlook add-in for Outlook 2010 and wanted to modify the recipient using Outlook Object Model(OOM) and ran into issues. What is the problem? In Exchange Client Extension you could hook into events like IExchExtMessageEvents::OnCheckNames and IExchExtMessageEvents::OnChecknameComplete and then from this event you would get an IExchExtCallBack interface. Using the IExchExtCallBack interface you could use the functions GetRecipients and SetRecipients to modify the recipient collection.
The BIG question is, how can this be done using OOM?
Unfortunately there is nothing in the Outlook Object Model that allows you to modify a recipient. We do have a workaround of first reading the existing recipients, removing the existing recipients, building a new recipient list with the changes and then adding it back to the message.
Below is some sample C# code that demonstrated this. In this sample I first delete all the recipients from the message, build a fresh list and then add them back to the message and finally a tweak to get the UI to update.
private void ThisAddIn_Startup(object sender, System.EventArgs e){ this.Application.Inspectors.NewInspector += new Outlook.InspectorsEvents_NewInspectorEventHandler(Inspectors_NewInspector);}private Outlook::MailItem mailItem = null;void Inspectors_NewInspector(Outlook.Inspector Inspector){ mailItem = Inspector.CurrentItem as Outlook::MailItem; if (mailItem != null) { mailItem.BeforeCheckNames += new Outlook.ItemEvents_10_BeforeCheckNamesEventHandler(mailItem_BeforeCheckNames); }}void mailItem_BeforeCheckNames(ref bool Cancel){ // In order to be able to add recipients, set their addressEntries, and have them display, we need to // 1) Remove ALL recipients from the message // 2) Re-add them, and set their address entries // 3) To get the UI to update, add and remove a final recipient // Step 1: remove all recipients int Count = mailItem.Recipients.Count; while (Count > 0) { mailItem.Recipients.Remove(1); Count = mailItem.Recipients.Count; } // Step 2: re-add them, and set their address entries string[] RecipentAddresses = { "test1", "testdl", "test2" }; for (int i = 0; i < RecipentAddresses.Length; i++) { Outlook::Recipient recip = mailItem.Recipients.Add("test2"); int addrType = recip.Type; Outlook::AddressEntry ae = Application.Session.AddressLists["Global Address List"].AddressEntries[RecipentAddresses[i]]; recip.AddressEntry = ae; // need to set recipient type back to old value after re-adding recip.Type = addrType; recip.Resolve(); } // Step 3: (only needed in some event callbacks) // Now addresses have changed, but sometimes the UI doesn't update until // we add and remove a final recipient. // This workaround may not be needed in the BeforeCheckNames event, but is // needed in some other events if updating the recipients mailItem.Recipients.Add("pleaseIgnore"); mailItem.Recipients.Remove(mailItem.Recipients.Count);}
Have you tried using the Microsoft Office 2010 Primary Interop Assemblies Bootstrapper Package? Did you not run into any issues? Were you able to install the PIA’s with the default package? I was not so lucky!
One of my customer had created an add-in for Outlook 2010 and had included the Microsoft Office 2010 Primary Interop Assemblies Bootstrapper Package as part of his prerequisites for the add-in.The machine on which the add-in was being installed on did not have the PIA’s for any of the Office product installed. When the Setup.exe for the add-in was run, it still did not install the PIA’s. Why?
After banging my head for quite a while I discovered that the componentcheck.exe was causing the issue. It always returned a value of 1. What does that mean? It means that componentcheck.exe was finding a PIA component on the machine! What was it finding when none of the PIA’s were installed?
Componentcheck.exe checks for the following components and if it finds any one of them it returns 1.
In my case Office.dll was always there on the machine and hence componentcheck.exe was returning a value of 1. How do I get my add-in setup to install the PIA’s?
The easiest solution is not to use the componentcheck.exe and always force the installation of the PIA’s. How can that be done? Easy…just modify the product.xml and remove the nodes for 1) PackageFile that relates to ComponentCheck.exe 2) The entire InstallChecks section as it is not needed any more 3) The BypassIf install Condition related to PIAInstallAction and then rebuild you setup.
Below is how the altered product.xml would look after the modification:
<?xml version="1.0" encoding="utf-8" ?><!--*********************************************************************** Copyright (C) Microsoft Corporation. All rights reserved. THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.***********************************************************************--><Product xmlns="http://schemas.microsoft.com/developer/2004/01/bootstrapper" ProductCode="Microsoft.Office.PIARedist.2010"> <RelatedProducts> <EitherProducts> <DependsOnProduct Code="Microsoft.Net.Client.3.5" /> <DependsOnProduct Code=".NETFramework,Version=v4.0,Profile=Client" /> </EitherProducts> </RelatedProducts> <PackageFiles> <PackageFile Name="o2010pia.msi" HomeSite="Office2010PIARedistMSI" PublicKey="3082010A0282010100A2DB0A8DCFC2C1499BCDAA3A34AD23596BDB6CBE2122B794C8EAAEBFC6D526C232118BBCDA5D2CFB36561E152BAE8F0DDD14A36E284C 7F163F41AC8D40B146880DD98194AD9706D05744765CEAF1FC0EE27F74A333CB74E5EFE361A17E03B745FFD53E12D5B0CA5E0DD07BF2B7130DFC606A288575 8CB7ADBC85E817B490BEF516B6625DED11DF3AEE215B8BAF8073C345E3958977609BE7AD77C1378D33142F13DB62C9AE1AA94F9867ADD420393071E08D6746 E2C61CF40D5074412FE805246A216B49B092C4B239C742A56D5C184AAB8FD78E833E780A47D8A4B28423C3E2F27B66B14A74BD26414B9C6114604E30C882F3 D00B707CEE554D77D2085576810203010001"/> </PackageFiles> <Commands Reboot="Defer"> <Command PackageFile="o2010pia.msi" Arguments="/quiet" EstimatedInstalledBytes="7000000" EstimatedInstallSeconds="60"> <InstallConditions> <FailIf Property="AdminUser" Compare="ValueEqualTo" Value="false" String="AdminRequired" /> </InstallConditions> <ExitCodes> <ExitCode Value="0" Result="Success" /> <ExitCode Value="1641" Result="SuccessReboot" /> <ExitCode Value="3010" Result="SuccessReboot" /> <DefaultExitCode Result="Fail" FormatMessageFromSystem="true" String="GeneralFailure" /> </ExitCodes> </Command> </Commands></Product>
Error: “The transport failed to connect to the server”, Code:80040213, Source:CDO.Message.1. Does that look familiar? This was something that I kept running into while trying to send mail using CDOSYS with SSL/TLS enabled. Vikas already has a post explaining some of the details that you could refer to but there are some more finer points that need to be kept in mind for you to successfully send out mails. Below is what you should check:
1) Do a Telnet to the SMTP server on port 25 and then issue an ehlo command. This should list out the verbs that are supported by the SMTP server. If you do not see “250-STARTTLS” in the list then there is a problem with the certificate and you should fix that first. What can you do to fix it? That really depends on the resources that are available to you. You could purchase a certificate from a Trusted Root Certification Authority or generate one on you own. Assuming that we want to generate one, we could use the New-ExchangeCertificate cmdlet to generate one and associate it with SMTP. In my case my machine was called Ex200701 and my domain was “mycomany.com”. The FQDN for my machine was Ex200701.mycompany.com. Below is how I generated the certificate for my use:
Step 1: New-ExhcangeCertificate –DomainName “Ex200701, Ex200701.mycompany.com” . The FQDN specified in the above command should match the one specified in the Receive Connector. This generates a new certificate and shows you the thumbprint for the certificate. Note that thumbprint down.
Step 2: Enable-ExchangeCertificate – thumbprint “Thumbprint that you noted in the pervious Step 1” –Services SMTPThis steps associates the certificate with the SMTP service.
Step 3: Restart the Transport service.
Step 4: Do a Telnet to the SMTP server on port 25 and then issue an ehlo command. You should now see “250-STARTTLS” in verbs listed.
2) When you to a Telnet to the SMTP server on port 25 and then issue an ehlo command and you see the “250-STARTTLS” in the list then most likely the certificate is ok but be sure to verify the certificate by using the certificates snap-in and specifically looking to the “Subject Alternative Name” in the certificate details. In case you have more that one certificates installed and you want to know which one is being used by the SMTP, the simplest way to find that out is to enable Verbose logging on the Receive Connector and then looking at the Verbose logs. Below is how the logs would look like:
>,"220 Ex200701.mycompany.com Microsoft ESMTP MAIL Service ready at Fri, 25 Feb 2011 14:06:20 -0600",<,EHLO AKASHB,>,250-Ex200701.mycompany.com Hello [10.171.79.38],>,250-SIZE,>,250-PIPELINING,>,250-DSN,>,250-ENHANCEDSTATUSCODES,>,250-STARTTLS,>,250-X-ANONYMOUSTLS,>,250-AUTH GSSAPI NTLM,>,250-X-EXPS GSSAPI NTLM,>,250-8BITMIME,>,250-BINARYMIME,>,250-CHUNKING,>,250-XEXCH50,>,250 XRDST,<,STARTTLS,>,220 2.0.0 SMTP server ready,*,,Sending certificate*,CN=Ex200701,Certificate subject*,CN=Ex200701,Certificate issuer name*,76B8F8FF8379FABD49B1A3E486CE0B31,Certificate serial number*,480F5316242A7D40E25CF0F0D63827D3A02B5B15,Certificate thumbprint*,Ex200701;Ex200701.mycompany.com,Certificate alternate names
Note down the Certificate thumbprint, open all the existing certificates and find the one that has a matching thumbprint.
3) Export the certificate to a .cer file. How do you do that? That’s easy – Open the certificate by double clicking it, go to the “Details” tab and click on the “Copy to File” button and keep clicking Next with the default options selected, enter a file name to save the file as and you are done.
4) Why do I need to export the certificate? The certificate that we generated in Step 1 is a not from a Trusted Root Certification Authority and hence cannot be vaildate. The certificate will now have to be placed in the “Personal”, “Trusted Root Certification Authorities”, “Intermediate Certification Authorities” folders in certificate store on the client machine from where you are trying to send the mail from(just to be absolutely sure it is found). Please note that the pervious step and this step would not be necessary had you purchased the certificate from Trusted Root Certification Authority.
5) Now while sending the mail, make sure that you specify one of the names specified in the Domain Name parameter in Step 1 as the Server Name in the code and set the smtpusessl configuration to true. Below is the sample VBScript code I used to send mails out:
Dim iMsgDim iConfDim FldsConst cdoSendUsingPort = 2Const cdoBasic = 1Const cdoNTLM = 2set iMsg = CreateObject("CDO.Message")set iConf = CreateObject("CDO.Configuration")Set Flds = iConf.FieldsWith Flds .Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = cdoSendUsingPort .Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = "Ex200701.mycompany.com" .Item("http://schemas.microsoft.com/cdo/configuration/smtpconnectiontimeout") = 30 .Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport")=25 .Item("http://schemas.microsoft.com/cdo/configuration/smtpauthenticate")=cdoBasic .Item("http://schemas.microsoft.com/cdo/configuration/sendusername")="akashb@mycompany.com" .Item("http://schemas.microsoft.com/cdo/configuration/sendpassword")="Password123" .Item("http://schemas.microsoft.com/cdo/configuration/smtpusessl")=true .UpdateEnd WithWith iMsg Set .Configuration = iConf .From = "akashb@mycompany.com" .To = "akashb@mycompany.com" .Subject = "This is a test CDOSYS message" .HTMLBody = "Test Message" .SendEnd With' Clean up variables.Set iMsg = NothingSet iConf = NothingSet Flds = NothingMsgBox "Mail Sent!"
6) You should be successfully able to send out mails using SSL. If you still cannot send out mails, configure Outlook Express using the server & user details and remember to enable SSL for Outgoing mail.
If Outlook Express send out a test mail fine then you code should also be able to do it. If not, Outlook Express will give you the exact error you are running into and will help you trouble shoot the problem further. OOPS! I just let out a secret!
Deployment of VSTO add-ins have always been tricky. In Office 2007 we could deploy a VSTO Add-in for All users(after the hotfix) but if the assembly is not signed with a Trusted Publisher’s Certificate, each user sees the Microsoft Office Customization Installer dialog box (also know as the trust prompt) asking them if they want to install the add-in the very first time the add-in is loaded. If they choose to install the add-in, the add-in will run and they will not be prompted again. If they choose to not install the add-in, the add-in will not load and they will continue to see this trust prompt every time they open up the Office application and the add-in tries to load.
We could also avoid the Trust Prompts by creating a Inclusion List entry on a per user basis.The Registry propagation mechanism could be used to replicate the Inclusion List registry key to HKCU hive.
If you are developing your solution with Visual Studio 2010 and targeting .NET 4, an alternative to signing with a Trusted Publisher certificate and using Inclusion List is to install the add-in into the machines Program Files location. This location needs administrative privilege to write to and will be inherently trusted by VSTO, so there will be no trust prompt. We don’t have to worry about signing with a Trusted Publisher certificate and Inclusion Lists.
If you ask me? This is the BEST change in the Office Deployment space!
Have you tried install Installing a Transport Agent that is built with Exchange 2010 pre SP 1 binaries on Exchange 2010 SP 1? If not try it and you will get a error:
The TransportAgentFactory type "YourNamespace.YourAgentFactory" doesn't exist. The TransportAgentFactory type must be the Microsoft .NET class type of the transport agent factory.Parameter name: TransportAgentFactory + CategoryInfo : InvalidArgument: (:) [Install-TransportAgent], ArgumentException + FullyQualifiedErrorId : 6F5E297B,Microsoft.Exchange.Management.AgentTasks.InstallTransportAgent
This error is happening because while installing when the agent assembly is loaded, we check for all the Types associated with it and that where it fails as it is looking for the old build of the Microsoft.Exchange.Data.Common.dll and Microsoft.Exchange.Data.Transport.dll which is not there on the system. I tried copying the Microsoft.Exchange.Data.Common.dll and Microsoft.Exchange.Data.Transport.dll to the folder where the agent is installed and then I get a different error:
There is no known TransportAgentFactory interface implemented by the specified TransportAgentFactory "YourNamespace.YourAgentFactory".Parameter name: TransportAgentFactory + CategoryInfo : InvalidArgument: (:) [Install-TransportAgent], ArgumentException + FullyQualifiedErrorId : 7804297B,Microsoft.Exchange.Management.AgentTasks.InstallTransportAgent
The resolution is simple, recompile the Agent after adding references to the Microsoft.Exchange.Data.Common.dll and Microsoft.Exchange.Data.Transport.dll from the SP 1 build(14.1.214.0) and then installing the latest build.
Surprisingly an Agent compiled with Exchange 2007 binaries installs without an issue! Why?
This happens because we have a redirection policy in place for Exchange 2007. If you look into the GAC, there are two files named policy.8.0.Microsoft.Exchange.Data.Common and policy.8.0.Microsoft.Exchange.Data.Transport that redirect all old assembly(Exchange 2007) references to the latest build(14.1.214.0) and it just works. Unfortunately, we do not have one for the pre SP 1 and that the reason it fails.
The only solution as of now is to recompile the Transport Agent with the latest binaries from Exchange 2010 SP 1. The issue is being worked upon and we might have a FIX in the near future!
The error CDO.Message.1 (0x80040220) -The "SendUsing" configuration value is invalid is back again on IIS 7.5 and the reasons are also the same! inadequate permissions on the IIS Metabase due to which CDOSYS is unable to read the location of the Pickup Directory from the IIS Metabase. What has changed? The default Application Pool Identity in IIS 7.5 changed from NetworkService to AppPoolIdentity. For every Application Pool you create, the IIS Admin Process (WAS) will create a virtual account with the name of the new Application Pool and run the Application Pool's worker processes under this account. It is this account that is not able to read the location of the Pickup Directory from the IIS Metabase. You can read more about AppPoolIdentity here.
What are the available options to resolve the error? AFAIK there are 3 ways to resolve the error:
1) This is the easiest one, configure the application pool identity under which the site is running to NetworkService instead of AppPoolIdentity. From what I have seen the NetworkService account has “Special Permissions” on the /LM/SmtpSvc/ and /LM/SmtpSvc/1/ nodes in the IIS Metabase.
2) In case you do not want to change any permissions and are ok with code changes, then you can specify the smtpserverpickupdirectory and set its value to the path of pickup directory. Below is how we do it on VB Script.
'Send using the Pickup directory on the IIS server.Dim iMsgDim iConfDim FldsDim strHTMLConst cdoSendUsingPickup = 1set iMsg = CreateObject("CDO.Message")set iConf = CreateObject("CDO.Configuration")Set Flds = iConf.FieldsWith Flds .Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = cdoSendUsingPickup 'The path may differ in your environment .Item("http://schemas.microsoft.com/cdo/configuration/smtpserverpickupdirectory")="c:\Inetpub\mailroot\pickup" .UpdateEnd With' Build HTML for message body.strHTML = "<HTML>"strHTML = strHTML & "<HEAD>"strHTML = strHTML & "<BODY>"strHTML = strHTML & "<b> This is the test HTML message body</b></br>"strHTML = strHTML & "</BODY>"strHTML = strHTML & "</HTML>"With iMsg Set .Configuration = iConf .To = "test@test.com" .From = "test@test.com" .Subject = "This is a test CDOSYS message (Sent via Pickup)" .HTMLBody = strHTML .SendEnd With' Clean up variables.Set iMsg = NothingSet iConf = NothingSet Flds = NothingMsgBox "Mail Sent!"
As promised below is the code to call a .PS1 script and pass parameters to it. Most of the code is very similar to my previous post that showed how to call Exchange & PowerShell cmdlet.
using System;using System.Collections.Generic;using System.Text;using System.Management.Automation;using System.Management.Automation.Runspaces;using System.Management.Automation.Remoting;using System.Collections.ObjectModel;using System.Security;namespace CallingScriptPS{ class Program { static void Main(string[] args) { string password = "Password"; string userName = "Domain\\Administrator"; System.Uri uri = new Uri("http://CAS-SERVER/powershell?serializationLevel=Full"); System.Security.SecureString securePassword = String2SecureString(password); System.Management.Automation.PSCredential creds = new System.Management.Automation.PSCredential(userName, securePassword); Runspace runspace = System.Management.Automation.Runspaces.RunspaceFactory.CreateRunspace(); PowerShell powershell = PowerShell.Create(); PSCommand command = new PSCommand(); command.AddCommand("New-PSSession"); command.AddParameter("ConfigurationName", "Microsoft.Exchange"); command.AddParameter("ConnectionUri", uri); command.AddParameter("Credential", creds); command.AddParameter("Authentication", "Default"); PSSessionOption sessionOption = new PSSessionOption(); sessionOption.SkipCACheck = true; sessionOption.SkipCNCheck = true; sessionOption.SkipRevocationCheck = true; command.AddParameter("SessionOption", sessionOption); powershell.Commands = command; try { // open the remote runspace runspace.Open(); // associate the runspace with powershell powershell.Runspace = runspace; // invoke the powershell to obtain the results Collection<PSSession> result = powershell.Invoke<PSSession>(); foreach (ErrorRecord current in powershell.Streams.Error) { Console.WriteLine("Exception: " + current.Exception.ToString()); Console.WriteLine("Inner Exception: " + current.Exception.InnerException); } if (result.Count != 1) throw new Exception("Unexpected number of Remote Runspace connections returned."); // Set the runspace as a local variable on the runspace powershell = PowerShell.Create(); command = new PSCommand(); command.AddCommand("Set-Variable"); command.AddParameter("Name", "ra"); command.AddParameter("Value", result[0]); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke(); // First import the cmdlets in the current runspace (using Import-PSSession) powershell = PowerShell.Create(); command = new PSCommand(); command.AddScript("Import-PSSession -Session $ra"); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke(); // Now run get-ExchangeServer System.Collections.ObjectModel.Collection<PSObject> results = new System.Collections.ObjectModel.Collection<PSObject>(); powershell = PowerShell.Create(); powershell.Runspace = runspace; //Change the Path to the Script to suit your needs System.IO.StreamReader sr = new System.IO.StreamReader("..\\..\\Script.ps1"); powershell.AddScript(sr.ReadToEnd()); powershell.Runspace.SessionStateProxy.SetVariable("proc", "C*"); powershell.Runspace.SessionStateProxy.SetVariable("mbx", "*MBX"); results = powershell.Invoke(); if (powershell.Streams.Error.Count > 1) { foreach (ErrorRecord er in powershell.Streams.Error) Console.WriteLine(er.ErrorDetails); } else { foreach (PSObject ps in results) { Console.WriteLine(ps.Properties["Name"].Value.ToString()); } } } finally { // dispose the runspace and enable garbage collection runspace.Dispose(); runspace = null; // Finally dispose the powershell and set all variables to null to free // up any resources. powershell.Dispose(); powershell = null; } } private static SecureString String2SecureString(string password) { SecureString remotePassword = new SecureString(); for (int i = 0; i < password.Length; i++) remotePassword.AppendChar(password[i]); return remotePassword; } }}
Below is what the Script.PS1 looks like:
get-process |where {$_.Name -like $proc}get-exchangeserver | Where {$_.Name -like $mbx}
The two parameters that we pass in are called “proc” & “mbx”. Note:This code will not work if we do not call the New-PSSession & the Import-PSSession cmdlet’s for reasons explained in my previous post.
Will my Exchange 2007 PowerShell Managed code work with Exchange 2010 as is?
Unfortunately the answer is NO, fortunately there are not many changes that you will have to make. The management experience given by Exchange 2010 through PowerShell has been moved all the way from Local to Remote. Dave Vespa has a detailed post that explains the difference between the Runspace’s.
Only exchange cmdlets will work in this remoting scenario, you will not be able to run most of the powershell cmdlets. The powershell cmdlets that work in the remoting scenario are Out-Default, Get-FormatData, Select-Object, Measure-Object, Exit-PSSession. If you find more please do let me know...
Yes, this does mean that you will not be able to run cmdlets like Where-Object and .PS1 scripts in the Remote Runspace. Is that a limitation? I don’t think so. We can very easily get around it by create a new Session and Importing it.
For Exchange 2007 we had to add the Microsoft.Exchange.Management.PowerShell.Admin, now we will have to use New-PSSession and Import-PSSession and then we can do what ever we want to. Below is a code sample that shows how to use New-PSSession, Import-PSSession and using Where-Object in the now Local Runspace :
using System;using System.Collections.Generic;using System.Text;using System.Management.Automation;using System.Management.Automation.Runspaces;using System.Management.Automation.Remoting;using System.Collections.ObjectModel;using System.Security;namespace CallingPSCmdlet{ class Program { static void Main(string[] args) { string password = "Password!"; string userName = "Domain\\Administrator"; System.Uri uri = new Uri("http://Exchange-Server/powershell?serializationLevel=Full"); System.Security.SecureString securePassword = String2SecureString(password); PSCredential creds = new PSCredential(userName, securePassword); Runspace runspace = RunspaceFactory.CreateRunspace(); PowerShell powershell = PowerShell.Create(); PSCommand command = new PSCommand(); command.AddCommand("New-PSSession"); command.AddParameter("ConfigurationName", "Microsoft.Exchange"); command.AddParameter("ConnectionUri", uri); command.AddParameter("Credential", creds); command.AddParameter("Authentication", "Default"); PSSessionOption sessionOption = new PSSessionOption(); sessionOption.SkipCACheck = true; sessionOption.SkipCNCheck = true; sessionOption.SkipRevocationCheck = true; command.AddParameter("SessionOption", sessionOption); powershell.Commands = command; try { // open the remote runspace runspace.Open(); // associate the runspace with powershell powershell.Runspace = runspace; // invoke the powershell to obtain the results Collection<PSSession> result = powershell.Invoke<PSSession>(); foreach (ErrorRecord current in powershell.Streams.Error) { Console.WriteLine("Exception: " + current.Exception.ToString()); Console.WriteLine("Inner Exception: " + current.Exception.InnerException); } if (result.Count != 1) throw new Exception("Unexpected number of Remote Runspace connections returned."); // Set the runspace as a local variable on the runspace powershell = PowerShell.Create(); command = new PSCommand(); command.AddCommand("Set-Variable"); command.AddParameter("Name", "ra"); command.AddParameter("Value", result[0]); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke(); // First import the cmdlets in the current runspace (using Import-PSSession) powershell = PowerShell.Create(); command = new PSCommand(); command.AddScript("Import-PSSession -Session $ra"); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke(); // Now run get-ExchangeServer powershell = PowerShell.Create(); command = new PSCommand(); command.AddScript("Get-ExchangeServer | where-object{$_.Name -like \"*MBX\"}"); powershell.Commands = command; powershell.Runspace = runspace; Collection<PSObject> results = new Collection<PSObject>(); results = powershell.Invoke(); foreach (PSObject PSresult in results) { Console.WriteLine(PSresult.Properties["Name"].Value.ToString()); } } finally { // dispose the runspace and enable garbage collection runspace.Dispose(); runspace = null; // Finally dispose the powershell and set all variables to null to free // up any resources. powershell.Dispose(); powershell = null; } } private static SecureString String2SecureString(string password) { SecureString remotePassword = new SecureString(); for (int i = 0; i < password.Length; i++) remotePassword.AppendChar(password[i]); return remotePassword; } }}