#Author: Jos Lieben (OGD) #Date: 20-12-2016 #Script home: www.lieben.nu #Copyright: Leave this header intact, credit the author, otherwise free to use #Purpose: onetime copy of local AD contacts to Office 365 #Requires –Version 3 #example call: .\O365ContactImporter.ps1 <#Notes / features REQUIREMENTS: -Active Directory PS Module available -Properly configured parameters (see below) -Sufficient rights in AD and O365 for the accounts used TODO: -also import phone, address, etc CHANGELOG: V0.01 13-12-2016 JosL: initial version V0.02 19-12-2016 JosL: no version checks V0.03 20-12-2016 JosL: update contact if it already exists and isn't read-only V0.03 20-12-2016 JosL: don't validate email addresses for contacts against the accepted domains list #> Param( [Parameter(Mandatory=$true)][String]$o365login, [Parameter(Mandatory=$true)][String]$o365password, [Switch]$readOnly, #supply this switch to ensure no actual data is changed. [Switch]$debugMode #if you add this switch, a transcript log will be written to the same folder as the log folder ) #region SCRIPT CONFIGURATION: $sourceOU = "OU=Groups,OU=2 XXX Sweden,DC=XXX,DC=LOCAL" #sourceOU: LDAP path to one or more source OU's to sync to O365, seperate multiple OU's with a semicolon (;) $logPath = "c:\ogd\O365ContactImporter.log" #the log file can contain important hints if there are issues, and can be used to roll back changes, clean up as often as you like $verbose = $True #more verbose logging and on screen display ####END OF CONFIGURATION, DON'T TOUCH ANYTHING FROM HERE ON $totalWarnings = 0 $totalErrors = 0 $totalChanges = 0 $totalChanged = 0 $ADObjectCache = @() $version = "0.03" $WarningActionPreference = "Stop" $ErrorActionPreference = "Stop" #endregion if($debugMode){ $transcriptPath = "$($logPath)-debug" Start-Transcript -Path $transcriptPath -append } function abort_CI{ Param( [Switch]$failed ) if($failed){ log -text "Script execution terminated prematurely because of an error, see earlier log entries for details" -color "Red" }else{ log -text "Script execution terminated normally, there were $totalErrors errors and $totalWarnings warnings" -color "Green" } if($debugMode){Stop-Transcript} $global:logFileStreamWriter.Dispose() $global:logFileStream.Dispose() Get-PSSession | Remove-PSSession -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Confirm:$False Get-Variable | Remove-Variable -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Confirm:$False Exit } function log{ param ( [Parameter(Mandatory=$true)][String]$text, [Parameter(Mandatory=$true)][String]$color, [Switch]$append, [Switch]$error, [Switch]$warning ) if($error){ $script:totalErrors++ $text = "ERROR | $text" } elseif($warning){ $script:totalWarnings++ $text = "WARNING | $text" } else{ $text = "INFO | $text" } try{ if($append){$text = "$text, $($global:Error[0].Exception) at $($global:Error[0].InvocationInfo.ScriptLineNumber) $($global:Error[0].ErrorDetails)"} }catch{$text = "$text,failed to append error message"} try{ $global:logFileStreamWriter.WriteLine("$(Get-Date) | $text") }catch{$Null} Write-Host $text -ForegroundColor $color } function cacheADObjects{ try{ $ADObjects = @(Get-ADObject -Filter {(ObjectClass -eq "user" -or ObjectClass -eq "Group" -or ObjectClass -eq "contact")} -Properties proxyAddresses,name,DistinguishedName,DisplayName,ObjectGuid,mail,cn,managedBy,authOrig,dLMemSubmitPerms -ErrorAction Stop) }catch{ Throw "Failed to cache AD objects. $($Error[0]) $ADObjects" } return $ADObjects } function retrieveADContacts{ Param( [Parameter(Mandatory=$true)]$sourceOU ) #retrieve current groups in AD $sourceOUs = $sourceOU.Split(";",[System.StringSplitOptions]::RemoveEmptyEntries) $adContacts = @() $starttime = Get-Date foreach($ou in $sourceOUs){ try{ $adContacts += Get-ADObject -LDAPFilter “objectClass=Contact” -SearchBase $ou -SearchScope Subtree -Properties * -ErrorAction Stop }catch{ Throw "Failed to load contacts from $ou $($Error[0])" } } if($adContacts[0].DisplayName.Length -gt 0){ if($adContacts.Count -gt 1){ $adContacts = $adContacts.GetEnumerator() | sort -Property name } return $adContacts } return @() } function cacheO365Objects{ $O365Cache = @{} $O365Cache.FastSearch = @{} $script:WarningActionPreference = "SilentlyContinue" $script:ErrorActionPreference = "SilentlyContinue" Write-Progress -Activity "Retrieving O365 Objects" -PercentComplete 0 -Status "Processing mailboxes" #first look for mailboxes try{ [Array]$O365Cache.Mailboxes = @() get-mailbox -ResultSize Unlimited -erroraction SilentlyContinue -warningAction SilentlyContinue | % { if($_){ $O365Cache.Mailboxes += $_ $O365Cache.FastSearch."$($_.primarySmtpAddress)" = "mbx_$($O365Cache.Mailboxes.Count-1)" } } }catch{$Null} Write-Progress -Activity "Retrieving O365 Objects" -PercentComplete 25 -Status "Processing contacts" #now look for contacts try{ [Array]$O365Cache.MailContacts = @() get-mailcontact -ResultSize Unlimited -erroraction SilentlyContinue -warningAction SilentlyContinue | % { if($_){ $O365Cache.MailContacts += $_ $O365Cache.FastSearch."$($_.primarySmtpAddress)" = "con_$($O365Cache.MailContacts.Count-1)" } } }catch{$Null} Write-Progress -Activity "Retrieving O365 Objects" -PercentComplete 50 -Status "Processing mail users" #now look for mailusers try{ [Array]$O365Cache.MailUsers = @() get-mailuser -ResultSize Unlimited -erroraction SilentlyContinue -warningAction SilentlyContinue | % { if($_){ $O365Cache.MailUsers += $_ $O365Cache.FastSearch."$($_.primarySmtpAddress)" = "usr_$($O365Cache.MailUsers.Count-1)" } } }catch{$Null} Write-Progress -Activity "Retrieving O365 Objects" -PercentComplete 75 -Status "Processing groups" #now look for groups try{ [Array]$O365Cache.Groups = @() Get-distributiongroup -ResultSize Unlimited -erroraction SilentlyContinue -warningAction SilentlyContinue | % { if($_){ $O365Cache.Groups += $_ $O365Cache.FastSearch."$($_.primarySmtpAddress)" = "grp_$($O365Cache.Groups.Count-1)" } } }catch{$Null} Write-Progress -Activity "Retrieving O365 Objects" -PercentComplete 100 -Status "O365 Objects cached!" $script:WarningActionPreference = "Stop" $script:ErrorActionPreference = "Stop" return $O365Cache } function searchExOForUserOrGroupV2{#returns Exchange Online object, when searching based on an email address, also finds if alias, but much slower Param( [Parameter(Mandatory=$true)]$O365ObjectCache, [Parameter(Mandatory=$true)]$searchQuery, #defaults to email address, but switches can modify this [Switch]$byAlias, [Switch]$byDisplayName ) if($searchQuery.Length -lt 2){Throw "Invalid query: $searchQuery"} if(!$byAlias -and !$byDisplayName) { if($searchQuery.IndexOf("smtp:",[System.StringComparison]::CurrentCultureIgnoreCase) -eq 0){ $searchQuery = $searchQuery.SubString(5) } try{ $res = $O365ObjectCache.FastSearch.$searchQuery switch($res.Split("_")[0]){ "mbx"{ $retVal = $O365ObjectCache.Mailboxes[$res.Split("_")[1]] if($retVal){return $retVal} } "con"{ $retVal = $O365ObjectCache.MailContacts[$res.Split("_")[1]] if($retVal){return $retVal} } "usr"{ $retVal = $O365ObjectCache.MailUsers[$res.Split("_")[1]] if($retVal){return $retVal} } "grp"{ $retVal = $O365ObjectCache.Groups[$res.Split("_")[1]] if($retVal){return $retVal} } default{ Throw } } }catch{$Null} $searchQuery = "smtp:$searchQuery" #default to searching by email, prepend smtp: as this is how O365 knows them } #first look for a mailbox try{ if($byAlias){[Array]$res = @($O365ObjectCache.Mailboxes | where {$_.alias -eq $searchQuery -and $_})} elseif($byDisplayName){[Array]$res = @($O365ObjectCache.Mailboxes | where {$_.DisplayName -eq $searchQuery -and $_})} else{[Array]$res = @($O365ObjectCache.Mailboxes | where {$_.EmailAddresses -Contains $searchQuery -and $_})} if($res.Count -eq 0){Throw "No Mailbox Found"} }catch{$res = $Null} #if none is found, look for a contact if($res -eq $Null){ try{ if($byAlias){[Array]$res = @($O365ObjectCache.MailContacts | where {$_.alias -eq $searchQuery -and $_})} elseif($byDisplayName){[Array]$res = @($O365ObjectCache.MailContacts | where {$_.DisplayName -eq $searchQuery -and $_})} else{[Array]$res = @($O365ObjectCache.MailContacts | where {$_.EmailAddresses -Contains $searchQuery -and $_})} if($res.Count -eq 0){Throw "No Contact Found"} }catch{$res = $Null} } #if none is found, look for a mailuser if($res -eq $Null){ try{ if($byAlias){[Array]$res = @($O365ObjectCache.MailUsers | where {$_.alias -eq $searchQuery -and $_})} elseif($byDisplayName){[Array]$res = @($O365ObjectCache.MailUsers | where {$_.DisplayName -eq $searchQuery -and $_})} else{[Array]$res = @($O365ObjectCache.MailUsers | where {$_.EmailAddresses -Contains $searchQuery -and $_})} if($res.Count -eq 0){Throw "No MailUser Found"} }catch{$res = $Null} } #if none is found, look for a Group if($res -eq $Null){ try{ if($byAlias){[Array]$res = @($O365ObjectCache.Groups | where {$_.alias -eq $searchQuery -and $_})} elseif($byDisplayName){[Array]$res = @($O365ObjectCache.Groups | where {$_.DisplayName -eq $searchQuery -and $_})} else{[Array]$res = @($O365ObjectCache.Groups | where {$_.EmailAddresses -Contains $searchQuery -and $_})} if($res.Count -eq 0){Throw "No DistributionGroup Found"} }catch{$res = $Null} } #return false if nothing found, otherwise return object $script:WarningActionPreference = "Stop" $script:ErrorActionPreference = "Stop" if($res){ return $res[0] }else{ Throw "Could not find $searchQuery in Exchange Online" } } $logFileStream = New-Object IO.FileStream([System.IO.Path]::Combine($logPath), [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [IO.FileShare]::Read); $logFileStreamWriter = New-Object System.IO.StreamWriter($logFileStream); log -text "O365 Contact Importer v$version - $($env:USERNAME) on $($env:COMPUTERNAME) starting-----" -color "Green" log -text "Log path: $logPath" -color "Green" if($readOnly){log -text "Running in READ-ONLY mode, no actual changes will occur" -color "Yellow" -warning} log -text "---------------------------------------------" -color "Green" #region connect to Exchange Online try{ log -text "Connecting to Exchange Online...." -color "Green" $secpasswd = ConvertTo-SecureString $o365password -AsPlainText -Force $Credentials = New-Object System.Management.Automation.PSCredential ($o365login, $secpasswd) $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credentials -Authentication Basic -AllowRedirection $res = Import-PSSession $Session -AllowClobber -DisableNameChecking -WarningAction SilentlyContinue log -text "Exchange Online connection: OK" -color "Green" }catch{ log -text "Failed to connect to Exchange Online" -color "Red" -append -error abort_CI -failed }#endregion #region cache O365 objects try{ log -text "Caching current O365 objects..." -color "Green" $O365ObjectCache = cacheO365Objects log -text "Cached $($O365ObjectCache.MailContacts.Count) contacts in O365" -color "Green" }catch{ log -text "Failed to cache O365 objects" -color "Red" -append -error abort_CI -failed }#endregion #region load local contactss try{ log -text "Loading contacts from AD" -color "Green" [Array]$adContacts = @(retrieveADContacts -sourceOU $sourceOU | where {$_}) log -text "Loaded $($adContacts.Count) contacts from AD" -color "Green" }catch{ log -text "There was a problem loading contacts" -color "Red" -append -error abort_CI -failed }#endregion #now start creating contacts log -text "Starting contact creation now..." -color "Green" $script:WarningActionPreference = "Stop" $script:ErrorActionPreference = "Stop" foreach($adContact in $adContacts){ $res = $Null #check if already exists try{ $res = searchExOForUserOrGroupV2 -searchQuery $adContact.targetAddress -O365ObjectCache $O365ObjectCache }catch{$Null} #Format new proxy addresses [Array]$proxyAddressesTemp = @($adContact.ProxyAddresses | where {$_}) if($adContact.LegacyExchangeDN.Length -gt 0){ $proxyAddressesTemp+="X500:$($adContact.LegacyExchangeDN)" } $newProxies = @() $proxyAddressesTemp | % {$newProxies += $_.ToString()} if($res.DisplayName.Length -gt 0){ if($res.RecipientType -ne "MailContact" -and $res.RecipientType -ne "MailUser"){ log -text "$($adContact.DisplayName) already exists but is not a contact or mail user! Thus we cannot update it." -error -color "Red" } if($res.IsDirSynced -eq $True){ log -text "$($adContact.DisplayName) already exists and is being synced by DirSynced from another source, cannot update object!" -error -color "Red" }else{ log -text "$($adContact.DisplayName) already exists but is read-write, will update object if necessary" -color "Green" foreach($proxy in $newProxies){ if($res.EmailAddresses -contains $proxy){continue} try{ if(!$readOnly){$res2 = $res | Set-MailContact -EmailAddresses @{Add=$proxy}} log -text "$($adContact.DisplayName) updated object with $proxy" -color "Green" }catch{ log -text "$($adContact.DisplayName) failed to update object with $proxy" -color "Red" -error -append } } log -text "$($adContact.DisplayName) finished" -color "Green" } continue } log -text "$($adContact.DisplayName) does not exist yet, attempting to create..." -color "Green" try{ if(!$readOnly){$res = New-MailContact -Name $adContact.displayName -externalemailaddress $adContact.targetAddress} log -text "$($res.DisplayName) created" -color "Green" if(!$readOnly){$res2 = $res | Set-MailContact -EmailAddresses $newProxies -Confirm:$False} log -text "$($adContact.DisplayName) aliases configured properly" -color "Green" }catch{ log -text "$($adContact.DisplayName) something went wrong!" -color "Red" -append -error } } abort_CI