How to get info about your ActiveSync, EWS and WebDAV clients before migrating to Exchange 2010
When you're moving from Exchange 2007 to Exchange 2010 there are a few gotchas that it's worth watching out for in the planning stages before you change over Client Access Namespaces or start migrating Mailboxes. If you don't you might find you have broken Exchange 2007 clients, due to the CAS changes, and find some clients can't connect after their mailboxes are migrated.
When it comes to the Windows version of Outlook, you can find out about currently logged-in clients using the Get-LogonStatistics command; however you also need to be aware of other clients using protocols like ActiveSync, Exchange Web Services and of course WebDAV.
ActiveSync clients will mostly be fine with CAS Namespace changes, as depending on version they should be automatically proxied from Exchange 2010 back to Exchange 2007 or should autodiscover; however some clients like the iPhone don't work properly and you need to consider a workaround.
Exchange Web Services clients, for the most part, shouldn't have issues thanks to AutoDiscover, however it's good to understand what clients you have out there already so you can test and plan for any issues. There's a number of iPhone apps that use EWS out there that your users may have bought and I've seen some funny issues myself with the EWS version Mac Mail on Snow Leopard that may require a client visit.
Finally, WebDAV. There's little to explain about WebDAV apart from it's not supported in Exchange 2010! You need to find these clients (think Entourage 2004 and 2008) and upgrade them.
Unfortunately there isn't anything built-in to Exchange 2007 or 2010 to examine this data, but the good news is it should all be available to you via the IIS log files. Whilst logparser is pretty good, personally I wanted the data collected and grouped all in one go ready to use. And that's where this script came from…
What information does the script output provide?
The output from the script is in CSV format, so it's easy to use in Excel for further data processing. The CSV file itself has the following fields:
Username: Logon name of the user
ActiveSyncUser: If the user uses an ActiveSync mobile device
ActiveSyncProxyUser: If the user is currently being proxied through to this client access server
ActiveSyncClients: A semi-colon separated list of the clients in use, eg. iPad;htchero;
ActiveSyncLastAccess: Last date found for ActiveSync use
EWSUser: If the user uses some sort of Exchange Web Services client
EWSPCOutlook: Version information if the user has the Windows version of Outlook 2007 or 2010
EWSMacMail: Version information if user has the EWS version of Mac Mail
EWSMacOutlook: Version information if the user has Mac Outlook 2011
EWSEntourage: Version information if the user has Entourage 2008, Web Services Edition
EWSOther: A semi-colon separated list of any other EWS clients the user has
EWSLastAccess: Last date found for EWS use
WebDavUser: If the user uses some sort of WebDAV Exchange client
WebDavClient: A semi-colon separated list of WebDAV client software and versions in use
WebDavLastAccess: Last date found for WebDAV use
In an actual export, the above looks a little bit like this (apart from the blurry usernames!)
How to use the script
Example one - Parses log files from the default log directory "C:\WINDOWS\system32\LogFiles\W3SVC1" to "C:\output.csv"
Example two - Parses the last 30 days of log files from the current directory to cas_results.csv in the current directory, and saves the state to state.xml in the current directory (could take a LONG time!)
Limitations
So far, only tested with Exchange 2007 IIS log files.. Any probs, let me know.
Script Code
.SYNOPSIS
Parses IIS Log files for records relating to ActiveSync, Exchange Web Services and WebDAV
Steve Goodman
.DESCRIPTION
Looks at logs files and produces a CSV file with summary data listing:
* Activesync clients, whether it is proxied by another CAS and ActiveSync device type
* EWS clients with columns for PC Outlook, Mac Outlook and Entourage 2008 EWS Edition and other clients
* WebDAV clients including client versions.
.PARAMETER LogFilePath
Path to base directory of IIS Log files, e.g. "C:\WINDOWS\system32\LogFiles\W3SVC1"
.PARAMETER Days
How many days log files to look back by
.PARAMETER OutputCSVFile
File to write CSV output to
.PARAMETER SaveStateFile
File to save or load internal state to. Useful when looking at multiple CAS servers or to update output later on based on only more recent logfiles
.EXAMPLE
Parses log files from the default log directory "C:\WINDOWS\system32\LogFiles\W3SVC1" to "C:\output.csv"
.\ExIISLogParser.ps1
.EXAMPLE
Parses the last 30 days of log files from the current directory to cas_results.csv in the current directory, and saves the state to state.xml in the current directory
.\ExIISLogParser.ps1 -LogFilePath ".\" -Days 30 -OutputCSVFile ".\cas_results.csv" -SaveStateFile ".\state.xml"
#>
param(
[parameter(Position=0,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Full path to log file directory")][string]$LogFilePath = "C:\WINDOWS\system32\LogFiles\W3SVC1",
[parameter(Position=1,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Last write date of logs to search back by")][int]$Days=0,
[parameter(Position=2,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="CSV file for output")][string]$OutputCSVFile="C:\output.csv",
[parameter(Position=3,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Script state XML file")][string]$SaveStateFile
)
if (!(Test-Path $LogFilePath))
{
throw "LogFilePath does not exist"
}
if ((Test-Path $OutputCSVFile))
{
throw "OutputCSVFile already exists"
}
[hashtable]$Users = @{}
if ($SaveStateFile)
{
if ((Test-Path $SaveStateFile))
{
$Users = Import-Clixml -Path $SaveStateFile
}
}
$EarliestLogDate = (Get-Date).Subtract([timespan]"$($Days).00:00").Date
[array]$Files = Get-Item -Path "$($LogFilePath)\*.log" | Where {$_.LastWriteTime -gt $EarliestLogDate}
for ($i = 0; $i -lt $Files.Count; $i++)
{
Write-Progress -id 1 -activity "Overall Progress" -status "File $($i) of $($Files.Count)" -percentComplete (($i/$Files.Count*100)*0.9);
Write-Progress -id 2 -activity "Log $($Files[$i].Name)" -status "Loading" -percentComplete 0
$Log = Get-Content $Files[$i] | Where {$_ -like "*/Microsoft-Server-ActiveSync/*" -or $_ -like "*/EWS/*" -or $_ -like "*/exchange/*"}
for ($j = 0; $j -lt $Log.Count; $j++)
{
Write-Progress -id 2 -activity "Current Log $($Files[$i].Name)" -status "Working" -percentComplete ($j/$Log.Count*100);
# Clear Username
$Username=$null
# Split up log line
$arrLog = $Log[$j].Split(" ");
# Extract username first
if ($arrLog[5] -eq "/Microsoft-Server-ActiveSync/Proxy")
{
foreach ($QSPart in $arrLog[6].Split("&"))
{
if ($QSPart -like "User=*")
{
$Username =($QSPart.Split("="))[1]
}
}
} else {
# Not proxied, just get the username provided direct
$Username = $arrLog[8]
}
# Only process if it's an authenticated user
if ($Username)
{
# Take off domain or UPN suffix
if ($Username -like "*@*")
{
$Username = ($Username.Split("@"))[0]
} elseif ($Username -like "*\*") {
$Username = ($Username.Split("\"))[1];
} elseif ($Username -like "*%40*") {
$Username = ($Username.Split("%40"))[0]
} elseif ($Username -like "*/*") {
$Username = ($Username.Split("/"))[1]
}
# Make username lower case
$Username = $Username.ToLower()
# Update data for user
switch -wildcard ($arrLog[5])
{
"/EWS/*"
{
# Get EWS Client
$EWSClient = $arrLog[10].Replace("+"," ")
# Check if already found or create new hashtable item
if (!$Users.Contains($Username))
{
[hashtable]$obj = @{ActiveSyncUser=0; ActiveSyncProxyUser=0; ActiveSyncClients=@{}; ActiveSyncLastAccess=""; EWSUser=0; EWSClients=@{}; EWSLastAccess=""; WebDavUser=0; WebDavClients=@{}; WebDavLastAccess=""}
$Users.Add($Username,$obj)
}
# Set variables
$Users[$Username]["EWSUser"]=1
if (!$Users[$Username]["EWSClients"].Contains($EWSClient))
{
$Users[$Username]["EWSClients"].Add($EWSClient,1)
}
$Users[$Username]["EWSLastAccess"]=$arrLog[0]
break
}
"/Microsoft-Server-ActiveSync/*"
{
# Check if already found or create new hashtable item
if (!$Users.Contains($Username))
{
[hashtable]$obj = @{ActiveSyncUser=0; ActiveSyncProxyUser=0; ActiveSyncClients=@{}; ActiveSyncLastAccess=""; EWSUser=0; EWSClients=@{}; EWSLastAccess=""; WebDavUser=0; WebDavClients=@{}; WebDavLastAccess=""}
$Users.Add($Username,$obj)
}
# Set variables
# Is a ActiveSync user?
$Users[$Username]["ActiveSyncUser"]=1
# Is a proxy user?
if ($arrLog[5] -eq "/Microsoft-Server-ActiveSync/Proxy")
{
$Users[$Username]["ActiveSyncProxyUser"]=1
}
# Client Info
foreach ($QSPart in $arrLog[6].Split("&"))
{
if ($QSPart -like "DeviceType=*")
{
$ASClient = ($QSPart.Split("="))[1]
if (!$Users[$Username]["ActiveSyncClients"].Contains($ASClient))
{
$Users[$Username]["ActiveSyncClients"].Add($ASClient,1)
}
}
}
# Last Access Date
$Users[$Username]["ActiveSyncLastAccess"]=$arrLog[0]
break
}
"/exchange/*"
{
# Check if already found or create new hashtable item
if (!$Users.Contains($Username))
{
[hashtable]$obj = @{ActiveSyncUser=0; ActiveSyncProxyUser=0; ActiveSyncClients=@{}; ActiveSyncLastAccess=""; EWSUser=0; EWSClients=@{}; EWSLastAccess=""; WebDavUser=0; WebDavClients=@{}; WebDavLastAccess=""}
$Users.Add($Username,$obj)
}
# Set variables
$Users[$Username]["WebDavUser"]=1
$WDClient = $arrLog[10].Replace("+"," ")
if (!$Users[$Username]["WebDavClients"].Contains($WDClient))
{
$Users[$Username]["WebDavClients"].Add($WDClient,1)
}
$Users[$Username]["WebDavLastAccess"]=$arrLog[0]
break
}
}
}
}
}
Write-Progress -id 1 -activity "Overall Progress" -status "Preparing Output" -percentComplete 95;
[array]$Output=$null
$Users.GetEnumerator() | Foreach {
$OutputItem = New-Object Object
$OutputItem | Add-Member NoteProperty Username $_.Key
$OutputItem | Add-Member NoteProperty ActiveSyncUser $_.Value["ActiveSyncUser"]
$OutputItem | Add-Member NoteProperty ActiveSyncProxyUser $_.Value["ActiveSyncProxyUser"]
$ActiveSyncClients = $null
$_.Value["ActiveSyncClients"].GetEnumerator() | % { $ActiveSyncClients += "$($_.Key); "}
$OutputItem | Add-Member NoteProperty ActiveSyncClients $ActiveSyncClients
$OutputItem | Add-Member NoteProperty ActiveSyncLastAccess $_.Value["ActiveSyncLastAccess"]
$OutputItem | Add-Member NoteProperty EWSUser $_.Value["EWSUser"]
$EWSPCOutlook = ""
$EWSMacMail = ""
$EWSMacOutlook = ""
$EWSEntourage = ""
$EWSOther = ""
$_.Value["EWSClients"].GetEnumerator() | foreach {
$EWSClient = $_.Key
switch -wildcard ($EWSClient)
{
"Microsoft Office*"
{
$EWSPCOutlook=$EWSClient
break
}
"Mac*ExchangeWebServices*"
{
$EWSMacMail=$EWSClient
break
}
"MacOutlook*"
{
$EWSMacOutlook=$EWSClient
break
}
"Entourage*"
{
$EWSEntourage=$EWSClient
break
}
default
{
$EWSOther+="$($EWSClient); "
}
}
}
$OutputItem | Add-Member NoteProperty EWSPCOutlook $EWSPCOutlook
$OutputItem | Add-Member NoteProperty EWSMacMail $EWSMacMail
$OutputItem | Add-Member NoteProperty EWSMacOutlook $EWSMacOutlook
$OutputItem | Add-Member NoteProperty EWSEntourage $EWSEntourage
$OutputItem | Add-Member NoteProperty EWSOther $EWSOther
$OutputItem | Add-Member NoteProperty EWSLastAccess $_.Value["EWSLastAccess"]
$OutputItem | Add-Member NoteProperty WebDavUser $_.Value["WebDavUser"]
$WebDavClients=$null
$_.Value["WebDavClients"].GetEnumerator() | % { $WebDavClients += "$($_.Key); "}
$OutputItem | Add-Member NoteProperty WebDavClients $WebDavClients
$OutputItem | Add-Member NoteProperty WebDavLastAccess $_.Value["WebDavLastAccess"]
$Output += $OutputItem
}
if ($SaveStateFile)
{
$Users = Export-Clixml -Path $SaveStateFile
}
$Output[1..($Output.Count)] | Select Username,ActiveSyncUser,ActiveSyncProxyUser,ActiveSyncClients,ActiveSyncLastAccess,EWSUser,EWSPCOutlook,EWSMacMail,EWSMacOutlook,EWSEntourage,EWSOther,EWSLastAccess,WebDavUser,WebDavClients,WebDavLastAccess | Export-Csv -Path $OutputCSVFile -NoClobber -NoTypeInformation
Script Download
Comparison of Outlook 2010, OWA 2010 and Outlook 2011 Features [Updated]
With the recent release of Office 2011 for the Mac, Outlook makes a welcome return, spearheading the way toward full feature parity between Outlook on Windows and Mac. Finally, will your Mac users will be able to do everything their Windows counterparts can? Is Outlook 2011 for Mac just an improvement on Entourage 2008 EWS edition, and will Mac users need something like VMware Fusion plus Outlook 2010 to gain all the features they need for full interoperability with Exchange?
And with OWA becoming truly cross-platform, does this provide enough features to by-pass the desktop install of Outlook completely on the Mac and provide email access to Mac users via the browser?
The answer to both those questions isn't a straightforward one and depends on what you actually need…
After a few weeks of usage, Outlook 2011 feels more like Entourage than than it does Outlook - it seems obvious what code it's based upon and it's certainly not inheriting anything from the Windows version. But as a mail client it feels more accomplished than older versions of Entourage did, and is easy to work with if you're used to Outlook on a Windows PC.
When it comes to OWA 2010 SP1 - There has been a lot of improvement over previous versions, but it seems that the OWA and Outlook teams don't work closely together, as UI inconsistencies are common both in the way features work and are named (e.g. Calendar sharing features). This is muddled further as OWA's Options / Exchange Control Panel is essential for Outlook 2010 operation, for example when configuring Unified Messaging features.
Getting down to the nitty gritty of the features however and it's a complex story. Outlook 2011 doesn't have feature parity with either Outlook 2010, OWA 2010 or even Outlook 2007. It's got a smattering of features from all three. OWA 2010 fairs a lot better when it comes to Exchange 2010 feature support (obviously), but misses out useful features like contacts importing that are present in competitor webmail applications like GMail.
To make comparisons a little easier, I've went through and checked/tested the features that are most important to me and compiled a short comparison table. It’s now updated to cover Outlook 2011 SP1, but if you think anything is missing or needs amendment, let me know in the comments.
Outlook Comparison Table
| Feature | Feature Description | Outlook 2010 | OWA 2010 SP1 | Outlook 2011 |
| AD Rights Management Services | Protects messages from unauthorised access. | Yes | Yes | Yes |
| Address Book Defaults | Allows a default address list (such as Contacts, GAL) to be chosen | Yes | Yes (Limited) | No |
| Autodiscover Compatibility | Automatic setup of Exchange account | Yes | N/A | Yes |
| Auto Archive | Automatically moves older items to a local PST | Yes | N/A | No |
| Calendar Preview in Meeting Requests | Meeting requests show a preview of adjacent or conflicting appointments | Yes | No | Yes |
| Conversation Actions | Ignore and Clean Up/Delete buttons | Yes | Yes | No |
| Conversation View | Improved Conversation Threading | Yes | Yes | Yes |
| Cross Forest Mailbox Move Support | Auto reconfigure using auto discover | Yes | Yes | Yes |
| CSV Contacts Import/Export | Import/ Export CSV files in a standard format | Yes | No | Yes |
| Distribution Group Management | Management of memberships of Exchange Distribution Groups | Yes | Yes | No |
| Distribution Group Expansion | Expand DGs before sending to see all members | Yes | No | No |
| Enhanced Out of Office | Set external/internal message and from/to date range | Yes | Yes | Yes |
| Export PST | Allow PST files to be created and data exported | Yes | No | No |
| Federated Sharing Compatibility | Integration to allow access to remote calendar, share local calendar | Yes | Yes | Yes |
| Group Schedule View | Combine Multiple Calendars in One View | Yes | No | Yes |
| Import PST | Allow PST files to be imported to the mailbox | Yes | No | Yes |
| Instant Search | Fast searching using pre-built search index | Yes | Yes | Yes (Using Spotlight) |
| Integrated E-Mail Archive | Online Archive Access | Yes | Yes | No |
| Internet Calendar Sharing/Subscription | iCal Calendar Sharing/Server Side Subscription | Yes | Yes | No |
| MailTips | Server side mail tips for OOF, Large DG, permissions to send or comments | Yes | Yes | No |
| Multiple Exchange Accounts | Multiple accounts with different servers or domains | Yes | No | Yes |
| Offline Address Book | OAB access and download | Yes | N/A | Yes |
| Office Communicator Integration | Start converations video, call IM | Yes | Yes (IM) | Yes |
| Outlook Contact Card Photo and Voice Description | GAL Photos and UM Name along with availability | Yes | No | No |
| Outlook Social Connector | Plugins for Facebook, LinkedIn | Yes | No | No |
| Quick Actions | Macros to perform multiple or common actions at once | Yes | No | No |
| Quick View | Allows viewing of compatible attachments without leaving Outlook | Yes | Yes | Yes (OS 10.6 required) |
| Resend Message | Resend button allows you to resend an email | Yes | No | Yes (with SP1) |
| Retention and Archive Policy Management | Set and view retention policy for folders | Yes | Yes | No |
| Ribbon Interface | Office Fluent User Interface | Yes | No | Yes |
| Right-Hand Preview Pane | Message preview shown to the right of messages | Yes | Yes | No |
| Roaming Autocomplete List | Stored Nicknames on the server | Yes | Yes | Yes |
| RSS Aggregator | Subscribe to RSS Feeds from Outlook | Yes | No | No |
| Scheduling Assistance / Availability Service Integration | Real time calendar view for meeting and room bookings | Yes | Yes | Yes |
| Server-side Junk E-mail List | Modification of server side junk email allow/block list | Yes | Yes | No |
| Server-side rules | Modification of server side inbox rules | Yes | Yes | Yes (with SP1) |
| Text Messaging through ActiveSync | Access and Send Text Messages via compatible WinMo 6.5 Device | Yes | Yes | No |
| Unified Inbox | Combined view of Inbox across multiple email accounts. | No | N/A | Yes |
| Voicemail Play On Phone | UM play voicemail on phone | Yes | Yes | No |
| Voting Buttons | Allows approval of moderated messages or voting on user generated emails. | Yes | No | No |
Using Powershell to import contacts into Exchange and Outlook Live
When performing migrations between different systems, there’s always the case where the tools available don’t do the job out of the box – and although IMAP migration tools for Exchange and Outlook Live can be great for moving mail, there isn’t a decent free solution for importing contacts.
For a recent migration from a Unix system (Dovecot + SquirrelMail) to Outlook Live, I came across this very scenario. While Microsoft provide IMAP migration tools to move mail to Outlook Live (which I was lucky enough to beta test before it was widely available), no tools are provided to move other data such as contacts and calendars.
While this isn’t the actual code I used (I wrote my original code in C#), I’ve re-visited what I’ve done and listened to what others say they need. What I’ve heard is it would be useful to have some Powershell code that out-of-the box can import contacts into any Exchange mailbox. The reason that Powershell is the right language for code like this is that it’s fairly easy for the enterprising administrator to modify to their needs.
Getting started
Before you can run this script, there are a few things you need to set up first. Don’t worry though! It’s nothing too onerous – all is needed to get going is two things:
- Setup of EWS impersonation, unless you only want to work with accounts you know the username/password for.
- Installing the Managed API DLL onto your computer.
Setup of Exchange Web Services Impersonation
Exchange Web Services is the programmatic interface to each user’s Exchange mailbox. Most actions that can be performed in Outlook can be performed via EWS – so much so, in fact, that Apple’s Mail.app in Snow Leopard and the latest update for Entourage 2008 use EWS as the backend for the mail clients themselves.
If you’re planning on doing a mass-import of contacts, or don’t know the user’s password you’re importing, then setup of EWS impersonation is the step you need to take to allow a trusted account to switch to the user you want to import.
In Exchange 2010, setup of Exchange impersonation is managed via RBAC. Full details of how to setup impersonal are on the Microsoft site, but if you just want to get it setup for a single admin/service account, org-wide, use the following command substituing serviceaccount for your service account:
On Exchange 2007, it’s very different, and is managed by AD permissions on individual Client Access Servers. Again – full details on the Microsoft site, but to add the permission to all CAS servers, the following code will suffice, again substituting serviceaccount for your Service account:
Installing the Exchange Web Services Managed API
Next, whatever environment you are in, you need a copy of the DLL file that provides the bits and pieces that make this all work. The easiest way to get up and running is to download the API (choosing the right version for your platform), and install it to it’s default location.
Download the EWS Managed API from the Microsoft site
Once downloaded and installed, you should be good to go with script.
Using the script Import-MailboxContacts.ps1
So, you’ve got impersonation setup if needed, got the EWS DLL on your machine. You should be good to go!
For each mailbox you want to import contacts from you need a separate CSV file. The format of the file is intentionally the same as Outlook will export in, mainly because a lot of apps already export in the format (Gmail does, for example) so you should find it easy to get test data, but if you haven’t I’ve included a sample file to get you started.
Along with the data, you also need, at a minimum, the mailbox email address to import to. This is used when impersonating, and for Autodiscover. If you’re not logged on as the user you want to use for the import, you can specify a username and password. Additionally, there’s a variety of options to specify the EWS Url manually, switching to the Exchange 2007 version of the API etc.
Examples
The first example is a straightforward import into the current logged-on user’s mailbox. We’re specifying the Email Address and the URL to EWS (to bypass Autodiscover):
As you can see in the above example – it’s all fairly straightforward. You’ll also see the output shows the contacts that have been created – you can use standard Powershell pipeling to export this data, pipe it to another command or filter it.
The second example is slightly more complicated, and reflects the main use case. We’re connecting to a remote exchange organisation, using Autodiscover to find the EWS address, enabling impersonation and providing credentials at the command line:
In addition to the options above, you can also specify the parameters -Exchange2007 $true and use -EWSManagedApiDLLFilePath to specify a different path the the EWS DLL.
Powershell Code
Finally, you’ll need a copy of the code to do all this. At the bottom of the post you’ll find a ZIP file containing the Import-MailboxContacts.ps1 file and a sample CSV file, and below you’ll see the code itself. Happy importing!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | param([string]$CSVFileName,[string]$EmailAddress,[string]$Username,[string]$Password,[string]$Domain,[bool]$Impersonate,[string]$EwsUrl,[string]$EWSManagedApiDLLFilePath,[bool]$Exchange2007); # # Import-MailboxContacts.ps1 # # By Steve Goodman, Use at your own risk. # # Parameters # Mandatory: # -CSVFileName : Filename of the CSV file to import contacts for this user from. Same format as Outlook Export. # -EmailAddress : Account SMTP email address. Required, but only used when impersonating or with Autodiscover - otherwise uses the user you login as # Optional: # -Impersonate : Set to $true to use impersonation. # -Username : The username to use. If this isn't specified (along with Password), attempts to use the logged on user. # -Password : Used with above # -Domain : Used with above - optional. # -EwsUrl : The URL for EWS if you don't want to use Autodiscover. Typically https://casserver/EWS/Exchange.asmx # -EWSManagedApiDLLFilePath : (Optional) Overwrite the filename and path to the DLL for EWS Managed API. By default, uses the default install location. # -Exchange2007 : Set to $true to use the Exchange 2007 SP1+ version of the Managed API. # # Contact Mapping - this maps the attributes in the CSV file (left) to the attributes EWS uses. # NB: If you change these, please note "First Name" is specified at line 102 as a required attribute and # "First Name" and "Last Name" are hard coded at lines 187-197 when constructing NickName and FileAs. $ContactMapping=@{ "First Name" = "GivenName"; "Middle Name" = "MiddleName"; "Last Name" = "Surname"; "Company" = "CompanyName"; "Department" = "Department"; "Job Title" = "JobTitle"; "Business Street" = "Address:Business:Street"; "Business City" = "Address:Business:City"; "Business State" = "Address:Business:State"; "Business Postal Code" = "Address:Business:PostalCode"; "Business Country/Region" = "Address:Business:CountryOrRegion"; "Home Street" = "Address:Home:Street"; "Home City" = "Address:Home:City"; "Home State" = "Address:Home:State"; "Home Postal Code" = "Other:Home:PostalCode"; "Home Country/Region" = "Address:Home:CountryOrRegion"; "Other Street" = "Address:Other:Street"; "Other City" = "Address:Other:City"; "Other State" = "Address:Other:State"; "Other Postal Code" = "Address:Other:PostalCode"; "Other Country/Region" = "Address:Other:CountryOrRegion"; "Assistant's Phone" = "Phone:AssistantPhone"; "Business Fax" = "Phone:BusinessFax"; "Business Phone" = "Phone:BusinessPhone"; "Business Phone 2" = "Phone:BusinessPhone2"; "Callback" = "Phone:CallBack"; "Car Phone" = "Phone:CarPhone"; "Company Main Phone" = "Phone:CompanyMainPhone"; "Home Fax" = "Phone:HomeFax"; "Home Phone" = "Phone:HomePhone"; "Home Phone 2" = "Phone:HomePhone2"; "ISDN" = "Phone:ISDN"; "Mobile Phone" = "Phone:MobilePhone"; "Other Fax" = "Phone:OtherFax"; "Other Phone" = "Phone:OtherTelephone"; "Pager" = "Phone:Pager"; "Primary Phone" = "Phone:PrimaryPhone"; "Radio Phone" = "Phone:RadioPhone"; "TTY/TDD Phone" = "Phone:TtyTddPhone"; "Telex" = "Phone:Telex"; "Anniversary" = "WeddingAnniversary"; "Birthday" = "Birthday"; "E-mail Address" = "Email:EmailAddress1"; "E-mail 2 Address" = "Email:EmailAddress2"; "E-mail 3 Address" = "Email:EmailAddress3"; "Initials" = "Initials"; "Office Location" = "OfficeLocation"; "Manager's Name" = "Manager"; "Mileage" = "Mileage"; "Notes" = "Body"; "Profession" = "Profession"; "Spouse" = "SpouseName"; "Web Page" = "BusinessHomePage"; "Contact Picture File" = "Method:SetContactPicture" } # CSV File Checks # Check filename is specified if (!$CSVFileName) { throw "Parameter CSVFileName must be specified"; } # Check file exists if (!(Get-Item -Path $CSVFileName -ErrorAction SilentlyContinue)) { throw "Please provide a valid filename for parameter CSVFileName"; } # Check file has required fields and check if is a single row, or multiple rows $SingleItem = $false; $CSVFile = Import-Csv -Path $CSVFileName; if ($CSVFile."First Name") { $SingleItem = $true; } else { if (!$CSVFile[0]."First Name") { throw "File $($CSVFileName) must specify at least the field 'First Name'"; } } # Check email address if (!$EmailAddress) { throw "Parameter EmailAddress must be specified"; } if (!$EmailAddress.Contains("@")) { throw "Parameter EmailAddress does not appear valid"; } # Check EWS Managed API available if (!$EWSManagedApiDLLFilePath) { $EWSManagedApiDLLFilePath = "C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll" } if (!(Get-Item -Path $EWSManagedApiDLLFilePath -ErrorAction SilentlyContinue)) { throw "EWS Managed API not found at $($EWSManagedApiDLLFilePath). Download from http://bit.ly/9O7gLo"; } # Load EWS Managed API [void][Reflection.Assembly]::LoadFile("C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll"); # Create Service Object if ($Exchange2007) { $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1) } else { $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010) } # Set credentials if specified, or use logged on user. if ($Username -and $Password) { if ($Domain) { $service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($Username,$Password,$Domain); } else { $service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($Username,$Password); } } else { $service.UseDefaultCredentials = $true; } # Set EWS URL if specified, or use autodiscover if no URL specified. if ($EwsUrl) { $service.URL = New-Object Uri($EwsUrl); } else { try { $service.AutodiscoverUrl($EmailAddress); } catch { throw; } } # Perform a test - try and get the default, well known contacts folder. if ($Impersonate) { $service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress); } try { $ContactsFolder = [Microsoft.Exchange.WebServices.Data.ContactsFolder]::Bind($service, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Contacts); } catch { throw; } # Add contacts foreach ($ContactItem in $CSVFile) { # If impersonate is specified, do so. if ($Impersonate) { $service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress); } $ExchangeContact = New-Object Microsoft.Exchange.WebServices.Data.Contact($service); if ($ContactItem."First Name" -and $ContactItem."Last Name") { $ExchangeContact.NickName = $ContactItem."First Name" + " " + $ContactItem."Last Name"; } elseif ($ContactItem."First Name" -and !$ContactItem."Last Name") { $ExchangeContact.NickName = $ContactItem."First Name"; } elseif (!$ContactItem."First Name" -and $ContactItem."Last Name") { $ExchangeContact.NickName = $ContactItem."Last Name"; } $ExchangeContact.DisplayName = $ExchangeContact.NickName; $ExchangeContact.FileAs = $ExchangeContact.NickName; $BusinessPhysicalAddressEntry = New-Object Microsoft.Exchange.WebServices.Data.PhysicalAddressEntry; $HomePhysicalAddressEntry = New-Object Microsoft.Exchange.WebServices.Data.PhysicalAddressEntry; $OtherPhysicalAddressEntry = New-Object Microsoft.Exchange.WebServices.Data.PhysicalAddressEntry; # This uses the Contact Mapping above to save coding each and every field, one by one. Instead we look for a mapping and perform an action on # what maps across. As some methods need more "code" a fake multi-dimensional array (seperated by :'s) is used where needed. foreach ($Key in $ContactMapping.Keys) { # Only do something if the key exists if ($ContactItem.$Key) { # Will this call a more complicated mapping? if ($ContactMapping[$Key] -like "*:*") { # Make an array using the : to split items. $MappingArray = $ContactMapping[$Key].Split(":") # Do action switch ($MappingArray[0]) { "Email" { $ExchangeContact.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::($MappingArray[1])] = $ContactItem.$Key; } "Phone" { $ExchangeContact.PhoneNumbers[[Microsoft.Exchange.WebServices.Data.PhoneNumberKey]::($MappingArray[1])] = $ContactItem.$Key; } "Address" { switch ($MappingArray[1]) { "Business" { $BusinessPhysicalAddressEntry.($MappingArray[2]) = $ContactItem.$Key; $ExchangeContact.PhysicalAddresses[[Microsoft.Exchange.WebServices.Data.PhysicalAddressKey]::($MappingArray[1])] = $BusinessPhysicalAddressEntry; } "Home" { $HomePhysicalAddressEntry.($MappingArray[2]) = $ContactItem.$Key; $ExchangeContact.PhysicalAddresses[[Microsoft.Exchange.WebServices.Data.PhysicalAddressKey]::($MappingArray[1])] = $HomePhysicalAddressEntry; } "Other" { $OtherPhysicalAddressEntry.($MappingArray[2]) = $ContactItem.$Key; $ExchangeContact.PhysicalAddresses[[Microsoft.Exchange.WebServices.Data.PhysicalAddressKey]::($MappingArray[1])] = $OtherPhysicalAddressEntry; } } } "Method" { switch ($MappingArray[1]) { "SetContactPicture" { if (!$Exchange2007) { if (!(Get-Item -Path $ContactItem.$Key -ErrorAction SilentlyContinue)) { throw "Contact Picture File not found at $($ContactItem.$Key)"; } $ExchangeContact.SetContactPicture($ContactItem.$Key); } } } } } } else { # It's a direct mapping - simple! if ($ContactMapping[$Key] -eq "Birthday" -or $ContactMapping[$Key] -eq "WeddingAnniversary") { [System.DateTime]$ContactItem.$Key = Get-Date($ContactItem.$Key); } $ExchangeContact.($ContactMapping[$Key]) = $ContactItem.$Key; } } } # Save the contact $ExchangeContact.Save(); # Provide output that can be used on the pipeline $Output_Object = New-Object Object; $Output_Object | Add-Member NoteProperty FileAs $ExchangeContact.FileAs; $Output_Object | Add-Member NoteProperty GivenName $ExchangeContact.GivenName; $Output_Object | Add-Member NoteProperty Surname $ExchangeContact.Surname; $Output_Object | Add-Member NoteProperty EmailAddress1 $ExchangeContact.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress1] $Output_Object; } |


