Wednesday, May 25, 2011

Exchange Item-Level Manipulation with PowerShell

Shameless Plug

Mike Pfeiffer (blog | twitter) is a recently christened MCM for Exchange 2010.  His blog is awesome.  You should read it.  And then when you have a problem like I have, you should leverage what you've learned.

The Problem

Building on yesterday's Linked Mailbox creation issue for an Exchange server in a resource forest, I now need to perform item level manipulation for Contacts and Calenders.  We've been working on migration to Exchange 2010 from an obscure messaging system designed by a company in Redwood City, who shall not be named.  My co-worker, Andrew Healey (blog | twitter), has done an excellent job solving mail item synchronization.  But we're still stuck doing manual PST exports of non-mail items.

After completing our testing phase, we want to clear out all the non-mail items from user's mailboxes then perform fresh PST imports, thus avoiding any chance of item duplication.  In terms of native capabilities, Exchange 2010's lowest level of granularity using PowerShell is the mailbox.  So, knowing a little about EWS from another project I was considering to sync Out-of-Office replies to a Public Folder Vacation calendar, I found Mike Pfeiffer's article on this very topic.  His EWSMail module is phenomenal and incredibly well documented, but he did leave some things out on purpose.
The cmdlets in this module could use some enhancements though. They run under the security context of the logged on user, the EWS endpoint is set using autodiscover, and the Exchange version will default to Exchange 2010 SP1. If you want to extend this code, it might be useful to add parameters for specifying alternate credentials, manually setting the EWS URL, and specifying the Exchange version.
The Solution

First, we had to patch Exchange 2010 to SP1.  This saved some time (and was best practices) so that I didn't have to add version detection.  Incidentally, you can see an example where Mike instantiated an object with that exact property.

Secondly, I needed to extend his PowerShell module to allow me to pass the EWS URL as a parameter.  Adding the following snippets in the appropriate places did the job.

In the param() declarations, add the following snippet, updating the Position value accordingly:
[Parameter(Position = 4, Mandatory = $false)] [string] $WebServicesUrl
Then in the EWS Autodiscover section, I created some conditional logic:
#Determine the EWS end-point using Autodiscover
if ($WebServicesUrl -eq $null) { $service.AutodiscoverUrl($Mailbox)}
else { $service.Url = $WebServicesUrl }
It was after this that I discovered my biggest challenge, discovering what data structure each item used.  Mike's script handles Email Messages using the EmailMessage class without problems.  Typically you'll find those in the "Inbox".  Calendar items use the Appointment class and reside in "Calendars", of course.  Contacts are the most logical and use the Contact class and reside in the "Contacts" folders.  Same with Tasks, using Task, and residing in "Tasks".

Notes on the other hand were unique.  In terms of data structures, they're the same as Email Messages, using the EmailMessage class, but they'll sit in the "Notes" folder.  Go figure!
Note: Don't forget to update the default folder if none is specified in the params() block.
Rather than creating conditional logic, I chose to duplicate the main Get-* and Remove-* scripts and update the class binding line accordingly, e.g., for Contacts, I changed:
$email = [Microsoft.Exchange.WebServices.Data.EmailMessage]::Bind($service, $_.Id, $emailProps)
to
$email = [Microsoft.Exchange.WebServices.Data.Contact]::Bind($service, $_.Id, $emailProps)
Now after adding the additional Get-* and Remove-* scripts into the EWSMail.psm1 file, and dropping the whole module folder into my $env:PSModulePath, I was ready to go!

Final Details


I had to enable impersonation for my executing account:
New-ManagementRoleAssignment –Name:impersonationAssignmentName –Role:ApplicationImpersonation –User:CONTOSO\Administrator
Then launch the EMS, and run the script below and it’ll kill 2000 items at a time for all Mailboxes with a name like “Test*”.  There will be no visual confirmation of success in EMS.

Edit the text of that script to remove the Where-Object {$_.Name –like “Test*”} and it’ll do every mailbox.  See the text of that script below.
Import-Module EWSMail
$MBX = Get-Mailbox -ResultSize unlimited | Where-Object {$_.Name -like "Test*"}
$EWS = "https://exchange.corp.contoso.com/EWS/Exchange.asmx"
$Limit = 2000
$MBX | ForEach-Object {
    Get-EWSCalendarItem -Mailbox $_.WindowsEmailAddress -WebServicesUrl $EWS -ResultSize $Limit | Remove-EWSCalendarItem -WebServicesUrl $EWS -Confirm:$false
    Get-EWSContact -Mailbox $_.WindowsEmailAddress -WebServicesUrl $EWS -ResultSize $Limit | Remove-EWSContact -WebServicesUrl $EWS -Confirm:$false
    Get-EWSTask -Mailbox $_.WindowsEmailAddress -WebServicesUrl $EWS -ResultSize $Limit | Remove-EWSTask -WebServicesUrl $EWS -Confirm:$false
    Get-EWSMailMessage -Mailbox $_.WindowsEmailAddress -WebServicesUrl $EWS -ResultSize $Limit -Folder Notes | Remove-EWSMailMessage -Mailbox $_.WindowsEmailAddress -WebServicesUrl $EWS -Confirm:$false
}
Learning Points

Now, run all of this at your own risk.  And if you can do better, please do so and let me know.  More importantly, let Mike know.  Thanks to his template, I've learned a great deal about:


And I hope the rest of you never have to migrate between systems that don't provide vendor neutral protocols for all supplied services.

I do have one apology and that is that I haven't attached my scripts here as my blogging platform doesn't support attachments and to past ~500 script inline would be beastly to read.

1 comment:

  1. Awesome post. I've got to do something similar. Any way I can get a copy of your full script?

    ReplyDelete