Skip to content

Instantly share code, notes, and snippets.

@darrenjrobinson
Last active May 23, 2017 00:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save darrenjrobinson/fdec4ef8721161574d866ef9941fdbf5 to your computer and use it in GitHub Desktop.
Save darrenjrobinson/fdec4ef8721161574d866ef9941fdbf5 to your computer and use it in GitHub Desktop.
Exchange Online User Profile Photos PSMA Import
param (
$Username,
$Password,
$Credentials,
$OperationType,
[bool] $usepagedimport,
$pagesize
)
$DebugFilePath = "C:\PROGRA~1\MICROS~4\2010\SYNCHR~1\EXTENS~2\EXOPho~1\Debug\EXOImport.txt"
if(!(Test-Path $DebugFilePath))
{
$DebugFile = New-Item -Path $DebugFilePath -ItemType File
}
else
{
$DebugFile = Get-Item -Path $DebugFilePath
}
"Starting Import as: " + $OperationType + " " + (Get-Date) | Out-File $DebugFile -Append
$DebugPhotoDir = "C:\PROGRA~1\MICROS~4\2010\SYNCHR~1\EXTENS~2\EXOPho~1\Debug\Photos"
if(!(Test-Path $DebugPhotoDir))
{
$DebugPhotoPath = New-Item -Path $DebugPhotoDir -ItemType Directory
}
else
{
$DebugPhotoPath = Get-Item -Path $DebugPhotoDir
}
# Powershell Module required for Get-Hash (to calculate Image checksum)
Import-Module Pscx
# Created PS Creds
$UserCredential = New-Object System.Management.Automation.PSCredential $Username,$Password
$tempImagePhotoPath = "C:\Program Files\Microsoft Forefront Identity Manager\2010\Synchronization Service\Extensions\EXOPhotos\profileImage.jpg"
$oAuthRefreshTokenPath = "C:\Program Files\Microsoft Forefront Identity Manager\2010\Synchronization Service\Extensions\EXOPhotos\refresh.token"
# Path for oAuth Refresh Token
"oAuth Token Refresh Path $oAuthRefreshTokenPath" | Out-File $DebugFile -Append
# Get a list of mailboxes with profile photos
# ONLY the first time as we are using Paged Imports
if (!$Global:mbxwithpictures) {
# Create Remote PowerShell Session to Exchange Online
try {
if ($global:EXOSession){Remove-PSSession $global:EXOSession}
"Opening a new EXO RPS Session." | out-file $DebugFile -Append
$Global:EXOSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credentials -Authentication Basic -AllowRedirection -ErrorVariable $EXOError
Import-PSSession $Global:EXOSession
"Opened a new RPS Session" | out-file $DebugFile -Append
"EXO Session $($EXOSession.Id) $($EXOSession.ConfigurationName) $($EXOSession.State)" | Out-File $DebugFile -Append
}
catch {
"Failed creating a new RPS Session" | out-file $DebugFile -Append
$EXOError | out-file $DebugFile -Append
}
$Global:mbxwithpictures = Get-Mailbox -ResultSize Unlimited | where-object {$_.haspicture -eq $true}
"Found $($mbxwithpictures.count) mailboxes with photos" | Out-File $DebugFile -Append
[int]$Global:intMBXwithpictures = $mbxwithpictures.count
# Counter to know where we are up to processing the Import
# Starting at minus 1 as our first object is 0 and I'm incrementing at the start of the loop.
[int]$global:objectsImported = -1
}
# Now we have a list of mailboxes with pictures
# Use GraphAPI to go in and get them.
# The resource URI
$resource = "https://graph.microsoft.com"
# The Client ID and Client Secret obainted when registering the WebApp
$clientid = "b7ea7880-5d17-4e3e-ab76-034567890"
$clientSecret = "jhZn8GsMear3VXO3XxMgUngPeeItusaljdsfljIEOOW="
$redirectUri = 'https://localhost/'
# Library required for System.Web.HttpUtility
Add-Type -AssemblyName System.Web
# UrlEncode the ClientID and ClientSecret and URL's for special characters
$clientIDEncoded = [System.Web.HttpUtility]::UrlEncode($clientid)
$clientSecretEncoded = [System.Web.HttpUtility]::UrlEncode($clientSecret)
$resourceEncoded = [System.Web.HttpUtility]::UrlEncode($resource)
$scopeEncoded = [System.Web.HttpUtility]::UrlEncode("https://graph.microsoft.com/User.Read.All")
# Function to popup Auth Dialog Windows Form for getting an AuthCode
# So this will never work as part of the MA. Left here for manual run if Refresh.Token is lost and needs to be regenerated
Function Get-AuthCode {
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object -TypeName System.Windows.Forms.Form -Property @{Width=440;Height=640}
$web = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{Width=420;Height=600;Url=($url -f ($Scope -join "%20")) }
$DocComp = {
$Global:uri = $web.Url.AbsoluteUri
if ($Global:uri -match "error=[^&]*|code=[^&]*") {$form.Close() }
}
$web.ScriptErrorsSuppressed = $true
$web.Add_DocumentCompleted($DocComp)
$form.Controls.Add($web)
$form.Add_Shown({$form.Activate()})
$form.ShowDialog() | Out-Null
$queryOutput = [System.Web.HttpUtility]::ParseQueryString($web.Url.Query)
$output = @{}
foreach($key in $queryOutput.Keys){
$output["$key"] = $queryOutput[$key]
}
$output
}
if (Test-Path $oAuthRefreshTokenPath ){
# We have a previous refresh token.
# use it to get a new token
$refreshtoken = Get-Content $oAuthRefreshTokenPath
"Previous Refresh Token Loaded $refreshtoken"| Out-File $DebugFile -Append
# Refresh the token
$body = "grant_type=refresh_token&refresh_token=$refreshtoken&redirect_uri=$redirectUri&client_id=$clientId&client_secret=$clientSecretEncoded&resource=$resource"
$Authorization = Invoke-RestMethod https://login.microsoftonline.com/common/oauth2/token `
-Method Post -ContentType "application/x-www-form-urlencoded" `
-Body $body `
-ErrorAction STOP
$accesstoken = $Authorization.access_token
$refreshtoken = $Authorization.refresh_token | Out-File $oAuthRefreshTokenPath
"New Refresh Token Loaded $refreshtoken"| Out-File $DebugFile -Append
$refreshtoken | Out-File $DebugFile -Append
}
else
{
# Get Permissions (if the first time), get an AuthCode and Get a Bearer and Refresh Token
# Get AuthCode
$url = "https://login.microsoftonline.com/common/oauth2/authorize?response_type=code&redirect_uri=$redirectUri&client_id=$clientID&resource=$resourceEncoded&scope=$scopeEncoded"
Get-AuthCode
# Extract Access token from the returned URI
$regex = '(?<=code=)(.*)(?=&)'
$authCode = ($uri | Select-string -pattern $regex).Matches[0].Value
Write-output "Received an authCode, $authCode"
#get Access Token
$body = "grant_type=authorization_code&redirect_uri=$redirectUri&client_id=$clientId&client_secret=$clientSecretEncoded&code=$authCode&resource=$resource"
$Authorization = Invoke-RestMethod https://login.microsoftonline.com/common/oauth2/token `
-Method Post -ContentType "application/x-www-form-urlencoded" `
-Body $body `
-ErrorAction STOP
Write-output $Authorization.access_token
$accesstoken = $Authorization.access_token
$refreshtoken = $Authorization.refresh_token | out-file $oAuthRefreshTokenPath
}
# ********************* Process objects with profile photos into the MA *******************
# Know where we are at in relation to the pagesize from the Run Profile
[int]$objectpagecount = 0
foreach ($Global:mbxphoto in $Global:mbxwithpictures){
# continue from where we go to from the previous page of objects processed
$Global:mbxphoto = $Global:mbxwithpictures[$global:objectsImported + 1]
# if we are at the end then set MoreToImport to False and quit
if (!$Global:mbxphoto -or ($global:objectsImported +1 -eq $Global:intMBXwithpictures)) {
# nothing left to process
$global:MoreToImport = $false
break
}
# Double check they have a photo
# Get all photos sizes stored for the user
try {
$images = Invoke-RestMethod -Method Get -Headers @{Authorization = "Bearer $accesstoken"
'Content-Type' = 'application/json'} `
-Uri "https://graph.microsoft.com/v1.0/users/$($mbxphoto.UserPrincipalName)/photos"
}
catch {
"User not located in EXO" | Out-File $DebugFile -Append
$images = $null
}
if ($images.value.Count -gt 0){
# Get the Largest Image Size
[int]$numberofimages = $images.value.Count
$largestsize = $images.value[$numberofimages - 1].id
# Get the profile photo using the WebClient over Invoke-RestMethod so it is returned as Byte and not String
$url = "https://graph.microsoft.com/v1.0/users/$($mbxphoto.UserPrincipalName)/photos/$($largestsize)/`$value"
$wc = New-Object System.Net.WebClient
$wc.Headers["Authorization"] = "Bearer " + $accesstoken;
$byteImg = $wc.DownloadData($url)
# Get a checksum for the profile image
[string]$imgchecksum = $byteImg | Get-Hash -Algorithm SHA256
"$($mbxphoto.name) has a $($largestsize) photo with checksum: $($imgchecksum)" | Out-File $DebugFile -Append
# Create Object in the MA CS
$obj = @{}
$obj.Add("ID", $mbxphoto.ExternalDirectoryObjectId)
$obj.Add("objectID", $mbxphoto.ExternalDirectoryObjectId)
$obj.Add("objectClass", "EXOUser")
$obj.Add("EXOUserPrincipalName",$mbxphoto.userPrincipalName)
$obj.Add("EXODisplayName",$mbxphoto.displayName)
$obj.Add("EXOAlias",$mbxphoto.Alias)
$obj.Add("EXOonPremiseSID",$mbxphoto.ExternalDirectoryObjectId.tobyte())
$obj.Add("EXOMail",$mbxphoto.PrimarySmtpAddress)
$obj.Add("EXOPhoto",$byteImg)
$obj.Add("EXOPhotoChecksum",$imgchecksum)
# Pass the User Object to the MA
$obj
# Increase the object count
$objectpagecount++
# for logging how many we've processed
$global:objectsImported++
# Debug Logging where we are up to
"Page count: " + $objectpagecount | Out-File $DebugFile -Append
"Objects Imported count: " +$global:objectsImported | Out-File $DebugFile -Append
"Objects Remaining count: "+($Global:intMBXwithpictures - $global:objectsImported -1) | Out-File $DebugFile -Append
# if number of processed objects equals the pagesize set MoreToImport as true and break
if ($objectpagecount -eq $pagesize)
{
$global:MoreToImport = $true
"More to Import: " + $objectpagecount | Out-File $DebugFile -Append
break
}
}
else {
"$($mbxphoto.DisplayName) hasn't uploaded a photo to EXO" | Out-File $DebugFile -Append
# Increase the object count
$objectpagecount++
# for logging how many we've processed
$global:objectsImported++
}
}
# ***********************************************************
"Finished Import as: " + $OperationType + " " + (Get-Date) | Out-File $DebugFile -Append
#endregion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment