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.

Tuesday, May 17, 2011

The Power of PowerShell

Today I was faced with the challenge of creating an enterprise worth of Linked Mailboxes on Exchange 2010.  The Exchange server was sitting in a resource forest, adjacent to the production forest with two domains.  Linked mailboxes allow us to have a disabled User account in the resource forest, but grant authorization to the resource to a remote user account in a different trusted forest.  All this and more, just to migrate off of a previously unheard-of MTA.

I tested things out manually with a test mailbox, following Microsoft's article on the topic without any trouble.  Quickly noting the PowerShell example, I thought, "Hey, let's pipe in a list of user accounts from the production forest's Active Directory servers."

It was a great idea until:


[PS] C:\>Get-ADUser -Filter {Name -eq "Test"} -SearchBase "DC=contoso,DC=com" -Server ad01.contoso.com:3268
Get-ADUser : Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running.

Uh oh.  Can I use a different cmdlet to connect to Server 2008 RTM?  Nope, not that I could find.  Active Directory Web Services was introduced with Windows Server 2008 R2.  So how about a different tool?  Remember CSVDE?  I had used it once before to perform a parallel Active Directory migration.  Not pretty, but it gets the job done.

A little hack here, and a little test there and viola:
csvde -f users.csv -d "OU=Users,DC=contoso,DC=com" -s AD01 -r "(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=2))" -l "name,cn,sn,description,givenName,displayName,proxyAddresses,sAMAccountName,userPrincipalName"

Okay, I've got a CSV with some pretty useful information.  How do I loop through it with PowerShell to make those linked mailboxes?  A little searching through Technet, and I find J. Vosloo's article on a similar procedure for migrating mailboxes.  Import-CSV is a pretty powerful tool.  Now I can filter and purify my data in Excel, then run my script without any trouble.

$CSV = Import-CSV -path ".\users.csv"Foreach ($line in $CSV) {                $UPN = $line.sAMAccountName + "@resource.fabrikam.com"                $LMA = "CONTOSO\" + $line.sAMAccountName                New-Mailbox -Name $line.name -Alias $line.sAMAccountName -UserPrincipalName $UPN -SamAccountName $line.sAMAccountName -FirstName $line.givenName -Initials '' -LastName $line.sn -LinkedMasterAccount $LMA -LinkedDomainController 'ad01.contoso.com' }

And there we go!  All my mailboxes are created and linked.  There's more fun things you can do, like specify a particular OU for your target mailboxes based off of the OU of the originating Master Account, but that's left as an exercise to the reader.

Tuesday, May 3, 2011

Playing "What Device Is It?"

If you ever have to guess a device on the network without logging into it via common protocols (e.g., HTTP, HTTPS, FTP, Telnet, SMTP, etc), you can actually find out its NIC vendor.

First ping the device to get it into your ARP cache.

C:\>ping 172.16.100.254

Pinging 172.16.100.254 with 32 bytes of data:
Reply from 172.16.100.254: bytes=32 time<1ms TTL=64
Reply from 172.16.100.254: bytes=32 time<1ms TTL=64
Reply from 172.16.100.254: bytes=32 time<1ms TTL=64
Reply from 172.16.100.254: bytes=32 time<1ms TTL=64

Ping statistics for 172.16.100.254:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 0ms, Maximum = 0ms, Average = 0ms

Then, leverage ARP to check it’s MAC address.

C:\>arp -a | find ".254"
  172.16.100.254        00-90-7f-3c-e0-b8     dynamic

Then use Coffer’s MAC Find tool to find out the manufacturer using the first 6 digits of the MAC address.



Coffer's MAC Find lookup results

I've had to leverage this many a time when hunting down rogue devices.  Unfortunately, its less common for workstations to have a workstation OEM branded NIC, but embedded devices tend to yield  more conclusive results.