Problem Description
If you are in a hybrid environment where users are synced from on-prem AD to Azure AD using AD Connect, you may find that your user photos were never populated in SharePoint Online. In this case, the user photo may be displayed in Outlook (OWA), and Teams but not SharePoint.
Here is an example:
Outlook:

Teams:

SharePoint

Cause
This issue is occurring because there is currently a 10 KB limit for UserPhotos to be stored in Exchange Online. In many cases, the userPhotos are populated by updating the “thumbnailPhoto” attribute in Active Directory.
The thumbnailPhoto attribute from the on-prem AD is synchronizationed AzureAD, but never pulled into Exchange due to the 10 KB limitation.
Since SharePoint Online only pulls from Exchange Online to populate the user photo, if the photo is missing in Exchange Online, it will never be displayed in SharePoint Online.
You can determine if this issue occurring in your environment by inspecting your Exchange Online mailboxes to see of the user attribute “HasPicture” equals false.
Example:
get-mailbox -ResultSize Unlimited | Where {($_.IsDirSynced -eq $true -and $_.HasPicture -eq $false)}

Solution
To resolve this issue, you can use this script to execute the following actions.
Part 1:
Find all users that are missing photos in Exchange Online and update the photo from AzureAD.
1. Loop through all users, locate the photo in Azure AD
2. Download the photo to a temp location on the PC.
3. Publish the photo in Exchange Online
4. Delete the downloaded photos.
Part 2:
Copy the photos from Exchange Online to SharePoint.
1. Loop through all users that have photos in Exchange Online.
2. Locate their photo in Exchange Online.
3. Publish the photos from Exchange Online to SharePoint Online.
4. Update the SharePoint User Properties to reflect the new user photo.
Please Note: Part 2 is only required if you want the photo to show up right away in SharePoint Online. The photo will eventually make its way to SharePoint Online within 3 days via backend timer jobs.
The Script
|
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 |
#Title: Fix-UserPhotos #Description: Iterates through users and updates missing photos in Exchange and SharePoint. #Date: 2/1/2023 #Updated: 7/30/2026 #Author: Mike Lee #Credit and Ref: https://techcommunity.microsoft.com/t5/sharepoint/profile-photo-sync-spo/m-p/78585 #Disclaimer: This PowerShell script is provided "as-is" with no warranties expressed or implied. Use it at your own risk. #Input parameters #Update Tenant with your Tenant Info $siteUrl = "https://YOURTENANT-my.sharepoint.com/" $SPOAdminPortalUrl = "https://YOURTENANT-admin.sharepoint.com/" $adminacct = 'YOUR GLOBAL ADMIN ACCOUNT' Write-Host "Type the user e-mail address to fix a single user or 'All' to fix all users: " -ForegroundColor Green -NoNewLine $UserEmail = Read-Host # Validate input if ([string]::IsNullOrWhiteSpace($UserEmail)) { Write-Host "No input provided. Exiting." -ForegroundColor Red exit } #Add references to SharePoint client assemblies and authenticate to Office 365 site required for CSOM Add-Type -AssemblyName System.Drawing #Modules Needed #Install-Module PnP.PowerShell -Force -Confirm:$false #Install-Module -Name ExchangeOnlineManagement -Force -Confirm:$false #Install-Module Microsoft.Graph -Force -Confirm:$false #For environments with multiple accounts, use this 'Register-PnPManagementShellAccess' to make sure PNP is using the correct account #Register-PnPManagementShellAccess # --------------------------------------------------------------------------- # Functions # --------------------------------------------------------------------------- # This function will get the actual user photo from Microsoft Graph and publish to Exchange Online. Function Update-ExchangePhoto { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [String]$UserEmail ) # Only find DirSync users that do not have a photo in Exchange Online $users = Get-Mailbox -ResultSize Unlimited | Where-Object { $_.IsDirSynced -eq $true -and $_.HasPicture -eq $false } foreach ($user in $users) { if (($user.PrimarySmtpAddress -eq $UserEmail) -or ($UserEmail -eq "All")) { # Use GUID as temp filename to avoid invalid path characters in DisplayName $tempFile = Join-Path $env:USERPROFILE ($user.ExternalDirectoryObjectId + ".jpg") try { Write-Host "Getting user photo from Microsoft Graph - User: $($user.Alias)" -ForegroundColor Yellow Get-MgUserPhotoContent -UserId $user.ExternalDirectoryObjectId -OutFile $tempFile Write-Host "Uploading user photo to Exchange Online - User: $($user.Alias)" -ForegroundColor Green Set-UserPhoto -Identity $user -PictureData ([System.IO.File]::ReadAllBytes($tempFile)) -Confirm:$false Write-Host "Successfully updated user photo: $($user.Alias)" -ForegroundColor Green } catch { Write-Host "Failed to process $($user.Alias)" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } finally { if (Test-Path $tempFile) { Write-Host "Deleting temp photo $tempFile - User: $($user.Alias)" -ForegroundColor Red Remove-Item -Path $tempFile -Force -Confirm:$false } } # Short-circuit after finding the single requested user if ($UserEmail -ne "All") { break } } } } # This function will copy the user photo from Exchange Online and publish to SharePoint User Photos. Function Update-SharePointPhoto { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [String]$SiteURL, [Parameter(Mandatory = $true)] [String]$SPOAdminPortalUrl, [Parameter(Mandatory = $true)] [String]$UserEmail ) # Default image library and folder values $DocLibName = "User Photos" $foldername = "Profile Pictures" $Extension = ".jpg" $spoimagename = @{ "_SThumb" = "48"; "_MThumb" = "72"; "_LThumb" = "200" } $allUserMailboxes = Get-Mailbox -ResultSize Unlimited | Where-Object { $_.IsDirSynced -eq $true -and $_.HasPicture -eq $true } foreach ($mailbox in $allUserMailboxes) { if (($mailbox.PrimarySmtpAddress -eq $UserEmail) -or ($UserEmail -eq "All")) { Write-Host "Processing $($mailbox.PrimarySmtpAddress)" try { # Download image from Exchange Online $photo = Get-UserPhoto -Identity $mailbox.Identity Write-Host "Exchange Online user photo downloaded - User: $($mailbox.PrimarySmtpAddress)" -ForegroundColor Cyan # Use GUID as filename (matches the correct SPO naming convention) $username = $mailbox.ExternalDirectoryObjectId foreach ($imagename in $spoimagename.GetEnumerator()) { # Convert image to target size $ms = New-Object System.IO.MemoryStream (, $photo.PictureData) $img = [System.Drawing.Image]::FromStream($ms) [int32]$new_width = $imagename.Value [int32]$new_height = $imagename.Value $img2 = New-Object System.Drawing.Bitmap($new_width, $new_height) $graph = [System.Drawing.Graphics]::FromImage($img2) try { $graph.DrawImage($img, 0, 0, $new_width, $new_height) # Convert resized image to memory stream $stream = New-Object System.IO.MemoryStream $img2.Save($stream, [System.Drawing.Imaging.ImageFormat]::Jpeg) $null = $stream.Seek(0, [System.IO.SeekOrigin]::Begin) # Upload image to SharePoint Online $FullFilename = $username + $imagename.Name + $Extension Add-PnPFile -Connection $siteConnection -Folder "User Photos/Profile Pictures" -FileName $FullFilename -Stream $stream } finally { # Dispose GDI/stream objects to release handles and memory if ($null -ne $stream) { $stream.Dispose() } if ($null -ne $graph) { $graph.Dispose() } if ($null -ne $img2) { $img2.Dispose() } if ($null -ne $img) { $img.Dispose() } if ($null -ne $ms) { $ms.Dispose() } } } Write-Host "SharePoint Online user photo uploaded - User: $($mailbox.PrimarySmtpAddress)" -ForegroundColor Yellow # Update user profile properties in SharePoint Online $PictureURL = $SiteURL + $DocLibName + "/" + $foldername + "/" + $username + "_MThumb" + $Extension $timestamp = [int64](Get-Date).ToFileTime() Write-Host "Updating user profile properties with new photo - User: $($mailbox.PrimarySmtpAddress)" -ForegroundColor Magenta Set-PnPUserProfileProperty -Connection $adminConnection -Account $mailbox.PrimarySmtpAddress -PropertyName PictureURL -Value $PictureURL Set-PnPUserProfileProperty -Connection $adminConnection -Account $mailbox.PrimarySmtpAddress -PropertyName SPS-PicturePlaceholderState -Value 0 Set-PnPUserProfileProperty -Connection $adminConnection -Account $mailbox.PrimarySmtpAddress -PropertyName SPS-PictureExchangeSyncState -Value 0 Set-PnPUserProfileProperty -Connection $adminConnection -Account $mailbox.PrimarySmtpAddress -PropertyName SPS-PictureTimestamp -Value $timestamp # Note: if the ODB URL does not match the SMTP address, replace $mailbox.PrimarySmtpAddress # with $mailbox.UserPrincipalName in the Set-PnPUserProfileProperty commands above. Write-Host "Image processed successfully and ready to display - User: $($mailbox.PrimarySmtpAddress)" -ForegroundColor Green } catch { Write-Host "Failed to process $($mailbox.PrimarySmtpAddress)" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } # Short-circuit after finding the single requested user if ($UserEmail -ne "All") { break } } } } # --------------------------------------------------------------------------- # Connections # --------------------------------------------------------------------------- #Connect to SharePoint Online (My Site) $siteConnection = Connect-PnPOnline -Url $SiteURL -Interactive -ForceAuthentication -ReturnConnection #Connect to SharePoint Online (Admin Portal) # -UseWebLogin is needed for 'Set-PnPUserProfileProperty' due to Bug https://github.com/pnp/powershell/issues/277 #$adminConnection = Connect-PnPOnline -Url $SPOAdminPortalUrl -Interactive -ForceAuthentication -ReturnConnection $adminConnection = Connect-PnPOnline -Url $SPOAdminPortalUrl -UseWebLogin -ForceAuthentication -ReturnConnection #Connect to Exchange Online Connect-ExchangeOnline -UserPrincipalName $adminacct #Connect to Microsoft Graph (replaces deprecated AzureAD module) Connect-MgGraph -Scopes "User.Read.All" -NoWelcome # --------------------------------------------------------------------------- # Run # --------------------------------------------------------------------------- $option = Read-Host "Type '1' to update missing photos in Exchange Online`nType '2' to update missing photos in SharePoint Online" if ($option -eq "1") { Update-ExchangePhoto -UserEmail $UserEmail } elseif ($option -eq "2") { Update-SharePointPhoto -SiteURL $siteUrl -SPOAdminPortalUrl $SPOAdminPortalUrl -UserEmail $UserEmail } else { Write-Host "Invalid option '$option'. Please enter 1 or 2." -ForegroundColor Red } |
What the script looks like in action:
Here is an example of running “Part 1” the script for a single user.


Now you can see that “JSMITH” no longer shows up in the query to show “HasPicture = False”

Now the user photo has been updated in Exchange Online, but it still missing in SharePoint, until you wait for the UPA to sync via backend timer jobs.
If you want to update the photo in SharePoint right away, run “Part 2” of the script:




Now when sharing content via SharePoint, the UserPhoto will be displayed.

Conclusion
I hope you found this information useful, and provided a better understanding of why this issue is occurring and how to resolve it. If you have any comments, please let me know.
More Information
User photos aren’t synced to Exchange Online – Exchange | Microsoft Learn
Information about profile picture synchronization in Microsoft 365 – Microsoft Support