Tuesday, 17 February 2015

Retrieving Hi-Res Photos from an Exchange Mailbox

As anyone running Lync 2013, Exchange 2013, and SharePoint 2013 may already know, Exchange provides a mechanism to store hi-res user profile photos in the Exchange mailbox, replacing the photos stored in the thumbnailPhoto AD attribute in many use cases.

Typically the Exchange Management Shell cmdlets Set-UserPhoto and Get-UserPhoto are used to store and retrieve the photos, and they can also be retrieved from Exchange using Exchange Web Services by visiting a URL like http://ex2013.example.com/EWS/Exchange.asmx/s/GetUserPhoto?email=ben.lye@example.com&size=HR648x648.

The user photo sizes are 48x48, 64x64, 96x96x 120x120, 240x240, 360x360, 504x504, and 648x648. When a photo is uploaded Exchange creates resized images up to the maximum the source image will support. For example, a 1024x1024 image will be uploaded in all resolutions, but a 420x420 image will be uploaded in resolutions up to 360x360. If a resolution higher than those stored is requested, the highest available will be returned.

I was recently troubleshooting an issue where the photos for a particular user were misbehaving, and one particular resolution photo was not updating correctly. I wanted to know exactly what data was stored in the Exchange mailbox for the user. I've created a PowerShell script which will use the Exchange Web Services API to retrieve the user photos directly from a mailbox in all resolutions actually stored, along with the date the photo was last updated.

The advantage of using EWS API and accessing the mailbox over using the EWS URL is that the script will render all the images which are actually stored in the mailbox at once, and it can tell you the date and time that the photo was uploaded.

# Get-ExchangeUserPhoto.ps1
# PowerShell script to return the UserPhoto items from an Exchange Mailbox using EWS API
# EWS API call is made to the mailbox to retrieve the IPM.UserPhoto item in the mailbox (if it exists)
# The photos can be retrieved and stored as JPG files
# Ben Lye - www.onesimplescript.com

# Parameters for the script
[CmdletBinding(DefaultParametersetName="Common")]
param(
 [Parameter(Mandatory=$True)][string] $User,
 [Parameter(Mandatory=$False)][switch] $ReturnPhotos,
 [Parameter(Mandatory=$False)][switch] $SavePhotos
 )

# Get the Exchange mailbox
If ( !($Mailbox = Get-Mailbox $User -ErrorAction SilentlyContinue) ) {
 Throw "Mailbox could not be found for user '$($User)'."
} Else {
 If ($Mailbox.Count -gt 1) {
  Throw "Multiple mailbxes found for '$($User)'.  Specify a unique username."
 }
 Write-Verbose $Mailbox.DisplayName
}

# Get the SMTP address from the mailbox
$SmtpAddress = $Mailbox.PrimarySmtpAddress.ToString()
Write-Verbose $SmtpAddress

# Path to the Exchange Web Services DLL
$EWSManagedApiPath = "C:\Program Files (x86)\Microsoft\Exchange\Web Services\2.1\Microsoft.Exchange.WebServices.dll"
if ( !(Get-Item -Path $EWSManagedApiPath -ErrorAction SilentlyContinue) ) {
 throw "EWS Managed API could not be found at $($EWSManagedApiPath)."
}
 
# Load EWS Managed API
Add-Type -Path $EWSManagedApiPath
 
# Create EWS Object
$Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013SP1)

# Get the autodiscover URL
$Service.AutodiscoverUrl($SmtpAddress)
Write-Verbose $Service.Url.AbsoluteUri

# Use EWS Impersonation to locate the root folder in the mailbox
$Service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $SmtpAddress)

# Bind to the root folder
$FolderId = [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root
If (!($Folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$FolderId))) {
 Throw "Unable to bind to mailbox root folder"
}
 
# Create the search filter to find the UserPhoto item
$SearchFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass,"IPM.UserPhoto")

# Define the EWS search view
$ItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)
$ItemView.PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)

# Define a property set containing the extended properties where the photos are stored - each different resolution is in a unique extended property
$UserPhotoPropset = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$UserPhotoPropset.Add((New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,"UserPhotoHR48x48",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)))
$UserPhotoPropset.Add((New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,"UserPhotoHR64x64",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)))
$UserPhotoPropset.Add((New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,"UserPhotoHR96x96",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)))
$UserPhotoPropset.Add((New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,"UserPhotoHR120x120",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)))
$UserPhotoPropset.Add((New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,"UserPhotoHR240x240",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)))
$UserPhotoPropset.Add((New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,"UserPhotoHR360x360",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)))
$UserPhotoPropset.Add((New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,"UserPhotoHR432x432",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)))
$UserPhotoPropset.Add((New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,"UserPhotoHR504x504",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)))
$UserPhotoPropset.Add((New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common,"UserPhotoHR648x648",[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)))

# Do the search and return the results
If ($Results = $Service.FindItems( $FolderId, $SearchFilter, $ItemView )) {
 Write-Verbose "IPM.UserPhoto Item Found"
 
 # Create an object to store the output
 $Output = New-Object System.Object
 $Output | Add-Member -type NoteProperty -name "DisplayName" -value $Mailbox.DisplayName
 $Output | Add-Member -type NoteProperty -name "PrimarySmtpAddress" -value $SmtpAddress
 $Output | Add-Member -type NoteProperty -name "DateTimePhotoUploaded" -value $Results.Items[0].DateTimeCreated

 # Only load the extended properties if we actually want to return the binary data
 If ($ReturnPhotos) {
  # Load the extended properties
  [Void]$service.LoadPropertiesForItems($Results,$UserPhotoPropset)

  # Loop through each extended property  
  If ($UserPhotos = $Results.Items[0].ExtendedProperties) {
   ForEach ($UserPhoto in $Results.Items[0].ExtendedProperties) {
    Write-Verbose $UserPhoto.PropertyDefinition.Name
    $Output | Add-Member -type NoteProperty -name $UserPhoto.PropertyDefinition.Name -value $UserPhoto.Value
      # Write the photos out to the temp path if the SavePhotos switch was specified  
    If ($SavePhotos) {
     $Filename = $env:Temp + "\" + $Mailbox.Name + "_" + $UserPhoto.PropertyDefinition.Name + ".jpg"
     [IO.File]::WriteAllBytes("$Filename",$UserPhoto.Value)
     $Output | Add-Member -type NoteProperty -name "$($UserPhoto.PropertyDefinition.Name)Filename" -value $Filename
     Write-verbose $Filename
    }
   }
  }
 }
}

# Return the output
If ($Output) {
 Return $Output
}
 
Download Script

In case you were wondering how I worked out where the user photos were stored, it was a case of using MFCMAPI to open a mailbox which had a photo uploaded and exploring it. I quickly found the item with class 'IPM.UserPhoto' in the Root folder, and I could see the extended properties with names like 'UserPhotoHR240x240'.


From there it was a case of working out how to return the binary value of these properties using the Exchange Web Services API.

- Ben

No comments:

Post a Comment