O365 and Exchange 2016/Exchange 2013: Understanding the UserPhoto API

NOTE: This post – drafted, composed, written, and published by me – originally appeared on https://blogs.technet.microsoft.com/johnbai and is potentially (c) Microsoft.

We recently had an issue for an Enterprise Cloud customer, in which the photo was not rendering for the user – which was uploaded to AD (and synced over via MMSSPP to the managed environment). It was sussed that the issue was customer-caused, as the customer was modifying the photo via the PowerShell commandlets and had deleted the photo.

Despite the fact that the customer had disabled the OWA functionality to change the user photo in OWA, the customer was using the PowerShell commandlets to modify this object, which is the same interface/commands that OWA utilises behind the scenes.

To explain this behaviour in further detail: When you delete a photo from a mailbox in Exchange (or Exchange Online), it changes the UserPhotoCacheId MAPI property value to ‘0’ and this signifies to the UserPhoto API that the photo has been deleted. When this occurs, we will not fall-back to the ADPhotoHandler to call the photo from AD because the user explicitly deleted the photo. This scenario is by-design of the UserPhoto API.

To rectify this behaviour, clear the MAPI property by running ‘Remove-UserPhoto -ClearMailboxPhotoRecord’.

Keep in mind, as well, that Exchange uses a CachingPhotoHandler and that the photos stored on disk have a TTL of 7 days.

I have written a script to help ascertain the MAPI properties on a given mailbox, obtain the location of the cached photos on disk, and obtain the IPM.UserPhoto item from the user’s mailbox (on-premises):


function Get-UserPhotoDataOnPremises
{
param([string]$User)
Write-Warning -Message "The source of our powers comes from a Cracker Jack™ box, so this will take a minute. Please be patient..."
if([System.String]::IsNullOrEmpty($User))
{
throw [System.NullReferenceException]::new("The user value cannot be null.")
}
else
{
$mbx = Get-Mailbox $User
}

[string]$scriptPath = $env:ExchangeInstallPath + “Scripts\ManagedStoreDiagnosticFunctions.ps1”
# Dot-load script into function
. $scriptPath

[GUID]$guid = $mbx.ExchangeGuid.Guid
[int]$mbxNumber = Get-StoreQuery -Database $mbx.Database -Query “SELECT MailboxNumber FROM Mailbox WHERE MailboxGuid=’$guid'” | Select -ExpandProperty MailboxNumber
[string]$folderId = Get-StoreQuery -Database $mbx.Database -Query “SELECT FolderId FROM Folder WHERE MailboxNumber=’$mbxNumber’ AND DisplayName=’$([System.String]::Empty)'” | Select -ExpandProperty FolderId
$global:item = Get-StoreQuery -Database $mbx.Database -Query “SELECT * FROM Message WHERE MailboxNumber=’$mbxNumber’ AND FolderId=’$folderId’ AND MessageClass=’IPM.UserPhoto'” -Unlimited
[string]$previewPhotoCachedId = (Get-StoreQuery -Database $mbx.Database -Query “SELECT UserPhotoPreviewCacheId FROM Mailbox WHERE MailboxNumber=’$mbxNumber'” -Unlimited).p7C1B0003
[string]$photoCacheId = (Get-StoreQuery -Database $mbx.Database -Query “SELECT UserPhotoCacheId FROM Mailbox WHERE MailboxNumber=’$mbxNumber'” -Unlimited).p7C1A0003

# Obtain files on disk (if any)
$smtp = $mbx.WindowsEmailAddress.Address
$smtpAtIndex = $smtp.IndexOf(“@”)
$smtpAtIndexPlusOne = $smtpAtIndex + 1
$smtpDotIndex = $smtp.LastIndexOf(“.”)
$smtpNewLength = $smtpDotIndex – $smtpAtIndexPlusOne
$subString = $smtp.Substring($smtpAtIndexPlusOne, $smtpNewLength)
$queryString = “_$subString”
$preSubString = $smtp.Substring(0, $smtpAtIndex)
$srvr = Get-MailboxDatabaseCopyStatus $mbx.Database.Name | Where{$_.Status -contains ‘Mounted’} | Select -ExpandProperty MailboxServer
$folderPathUnc = “\\$($srvr)\” + $env:ExchangeInstallPath.Replace(“:”, “$”) + “\ClientAccess\photos”
$obj = Get-ChildItem -Path $folderPathUnc -Filter “*$($queryString)*”
$folderPath = $obj.FullName
$obj2 = Get-ChildItem -Path $folderPath
$fullPaths = @()
foreach($o in $obj2)
{
$picObj = Get-ChildItem -Path $o.FullName -Filter “*$($preSubString)*”
$fullPaths += New-Object PSobject -Property @{
Name=$picObj.Name
Size=$picObj.Length
Location=$picObj.FullName
}
}

if([System.String]::IsNullOrEmpty($item.MessageId) -eq $FALSE)
{
$string = “IPM.UserPhoto item found in user’s mailbox. The object can be found in ” + “$” + “item”
Write-Host $string
}
if($previewPhotoCachedId)
{
Write-Host “UserPhotoPreviewCacheId found: $previewPhotoCachedId”
}
if($photoCacheId)
{
Write-Host “UserPhotoCacheId found: $photoCacheId”
}
Write-Host -ForegroundColor Green “Photo files found on disk on $($srvr) for $($smtp):”
$fullPaths | FL
}

Here’s an example as run from my lab:


[PS] E:\>Get-UserPhotoDataOnPremises -User Administrator
WARNING: The source of our powers comes from a Cracker Jack™ box, so this will take a minute. Please be patient...
IPM.UserPhoto item found in user's mailbox. The object can be found in $item
UserPhotoPreviewCacheId found: -88512737
UserPhotoCacheId found: -88512737
Photo files found on disk on [REDACTED] for [email protected]:

Name : _Administrator-8EEDD78A2D804372C17E9FABD151BCD5.jpg
Location : \\[REDACTED]\E$\exchsrvr\ClientAccess\photos\_contoso.se-BBED6250E228D4F38F5F19BF4F1A6823\HR648x648\_Administrator-8EEDD78A2D804372C17E9FABD151BCD5.jpg
Size : 79838

Name : _Administrator-8EEDD78A2D804372C17E9FABD151BCD5.jpg
Location : \\[REDACTED]\E$\exchsrvr\ClientAccess\photos\_contoso.se-BBED6250E228D4F38F5F19BF4F1A6823\HR96x96\_Administrator-8EEDD78A2D804372C17E9FABD151BCD5.jpg
Size : 3224

I’ve also written a method in C# to test obtain the photo via the EWS GetUserPhoto REST method:


///
/// Creates an EWS request for a user's photo at each standard size.
///

/// Vanity name of your endpoint.
/// Smtp Address of the user you’re targeting.
private static void GetPhotos(string vanityName, string username)
{
int[] sizeInts = new[] { 48, 64, 96, 120, 240, 360, 432, 504, 648 };
Parallel.ForEach(sizeInts, delegate (int i)
{
Uri targetUri = new Uri($”https://{vanityName}/EWS/Exchange.asmx/s/GetUserPhoto?email={username}&size=HR{i}x{i}”);
Console.WriteLine($”Targeting: {targetUri}”);
try
{
HttpWebRequest newWebRequest = (HttpWebRequest)WebRequest.Create(targetUri);
newWebRequest.UserAgent = “Enterprise Cloud UserPhoto EWS Client”;
newWebRequest.Credentials = new NetworkCredential(“[UserName]”, “[PassWord]”);
using (HttpWebResponse newWebResponse = (HttpWebResponse)newWebRequest.GetResponse())
{
if (newWebResponse.StatusCode == HttpStatusCode.OK)
{
Console.WriteLine($”Size {i} photo found.”);
}
else if(newWebResponse.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine($”Photo API states that a photo cannot be found for the size {i}x{i}”);
}
else
{
Console.WriteLine($”Unexpected http response received: {newWebResponse.StatusCode}”);
}
}

}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

});
}

If you run into any problems with the script or have any questions or concerns around the UserPhoto API, feel free to let know! 🙂

Exchange 2013: Understanding the Room Finder function in OWA

NOTE: This post – drafted, composed, written, and published by me – originally appeared on https://blogs.technet.microsoft.com/johnbai and is potentially (c) Microsoft.

In Exchange 2013, when you use OWA to book a room, there can be two methods that get called for this to occur. Both of these methods start workflows against service.svc and you can see this in the client behavior via the following URLs:

https://<vanityDomain>/owa/service.svc?action=GetRoomLists&ID=-<ID>&AC=1
https://<vanityDomain>/owa/service.svc?action=GetRoomsInternal&ID=-<ID>&AC=1

The first method, GetRoomLists, returns all rooms found in room lists (Get-DistributionGroup -Filter {RecipientTypeDetails -eq “RoomList”}) and this is called when you select ‘Add Room’. Once a user selects a room list, that selection is remembered (read: cached) for subsequent uses in OWA to make the fetch of the list faster.

The second method, GetRoomsInternal, returns the first 100 rooms found in the GAL (Get-Recipient -RecipientPreviewFilter $allRoomsAddrList.RecipientFilter -ResultSize Unlimited)* and is called when you use the Scheduling Assistant to find rooms.

*$allRoomsAddrList = Get-AddressList “All Rooms”

O365: CDN Change Causes OWA Client Error

NOTE: This post – drafted, composed, written, and published by me – originally appeared on https://blogs.technet.microsoft.com/johnbai and is potentially (c) Microsoft.

Recently, we’ve seen a pattern of escalations wherein users are no longer able to access OWA. Specifically, the error will be similar to the following:

In the details, we’ll see the error we’re concerned with:

X-OQA-Error: ClientError;exMsg=’_g’ is undefined; file=https://pod51048.outlook.com/owa/:362

If we use Network Tracing (F12 in Internet Explorer) [or Fiddler] we’ll see a failure to connect to the CDN:

The connection to ‘r1.res.office365.com‘ failed. <br />Error: ConnectionRefused (0x274d). <br />System.Net.Sockets.SocketException No connection could be made because the target machine actively refused it 23.221.8.9:443

Using ARIN, we can check to make sure that the CDN (Content Delivery Network) we’re trying to hit is owned by Akamai.

The return we’re receiving, highlighted, is because the client cannot load ‘boot.worldwide.0.mouse.js’ from the CDN. This is evidenced by the refusal to connect to the RES server, highlighted.

The root cause of this issue is the configuration of the customer-owned firewall. This is evidenced by the client’s inability to connect to specific CDN and other users are able to use OWA sans issue; notably, because they are connecting to another CDN (‘r4.res.office365.com’, for example).

Since the CDN is controlled by a third-party and we have no way to predict which hosts will be up/in use/referred, our recommendation is to use URL-based filtering. You can read the EHLO blog post (written by our very own Tim Heeney) on the recommendation for URL-based filtering, here: http://blogs.technet.com/b/exchange/archive/2013/12/02/office-365-url-based-filtering-is-just-better-and-easier-to-sustain.aspx

O365: Accessing Another Mailbox via OWA URL

NOTE: This post – drafted, composed, written, and published by me – originally appeared on https://blogs.technet.microsoft.com/johnbai and is potentially (c) Microsoft.

It may become necessary for an admin or delegate to access a mailbox (other than their own) in OWA. There’s two ways to do this and most people are familiar with the change in the URL method, which is what I’ll be covering in this post.

In Wave14 (Exchange 2010), you merely had to append the user’s smtp address to the suffix the OWA URL. So, for example, to access Amy Luu‘s mailbox in my test tenant, I would add the following: [email protected]

In Wave15 (Exchange 2013), we merely need to add another character at the end of the URL for this to work: [email protected]/