At this point we are able to build our basic function that can obtain the MsolUser and Mailbox from a single Identity parameter.
First, let’s put our code from parts 2 and 3 into functions, add some Debug output, and have them output custom objects with all of the information we need. When we package this as a module, these functions will be internal only – i.e. we will ensure not to export them with the module. There is a suggestion not to use the standard Verb-Dash-Noun syntax for internal functions that won’t be exported, which we do here. Beyond that though I’m not sure what the best naming convention is in PowerShell, so forgive me for the lack of imagination:
Function GetMsolUser2 { [cmdletbinding()] Param( [string] $Identity ) try { # Try converting $Identity to a Guid. # Note that Get-MsolUser attempts this exact process automatically # when called with the -ObjectId parameter. $Guid = $Identity -as [System.Guid] # If it can convert to a Guid, there's no way it's a UserPrincipalName, # if nothing else for the lack of @ sign. So we only need to try one command. if ($Guid) { Write-Debug '[GetMsolUser2] Trying as GUID.' $MsolUser = Get-MsolUser -ObjectId $Guid -ErrorAction Stop } else { Write-Debug '[GetMsolUser2] Trying as UPN.' $MsolUser = Get-MsolUser -UserPrincipalName $Identity -ErrorAction Stop } #end else # Double check that we have actually obtained an MsolUser. if ($MsolUser) { Write-Debug '[GetMsolUser2] MsolUser found.' $MsolUserFound = $true } else { Write-Debug '[GetMsolUser2] MsolUser not found.' $MsolUserFound = $false } #end else } catch [Microsoft.Online.Administration.Automation.MicrosoftOnlineException] { Write-Debug '[GetMsolUser2] Get-MsolUser Error.' # We may need this message for later. $MsolUserError = $_.Exception.Message if ($MsolUserError -match "^User Not Found.") { # We can handle a user not found, so no need to throw. $MsolUserFound = $false } else { # We don't (yet) know how to handle this error, so rethrow for the user to see. throw } #end else } #end catch [Microsoft.Online.Administration.Automation.MicrosoftOnlineException] [pscustomobject]@{ MsolUser=$MsolUser MsolUserFound=$MsolUserFound MsolUserError=$MsolUserError } } #end Function GetMsolUser2 Function GetMailbox2 { [cmdletbinding()] Param( [string] $Identity ) try { # Exchange cmdlets do not pay attention to the -ErrorAction parameter, # but they do pay attention to $ErrorActionPreference. # Capture the old $ErrorActionPreference first as we should set it back once done. $ErrorActionPreferenceOld = $ErrorActionPreference $ErrorActionPreference = 'Stop' $Mailbox = Get-Mailbox -Identity $Identity # Double check that we have actually obtained an Mailbox. if ($Mailbox) { Write-Debug '[GetMailbox2] Mailbox found.' $MailboxFound = $true } else { Write-Debug '[GetMailbox2] Mailbox not found.' $MailboxFound = $false } #end else } catch [System.Management.Automation.RemoteException] { Write-Debug '[GetMailbox2] Get-Mailbox error.' # We may need this message for later. $MailboxError = $_.Exception.Message $MailboxErrorRegex = "^The operation couldn't be performed because object '.*' couldn't be found on '.*'\.$" if ($MailboxError -match $MailboxErrorRegex) { # We can handle a mailbox not found, so no need to throw. $MailboxFound = $false } else { # We don't (yet) know how to handle this error, so rethrow for the user to see. throw } #end else } finally { # Set $ErrorActionPreference back to its previous value. $ErrorActionPreference = $ErrorActionPreferenceOld } #end finally [pscustomobject]@{ Mailbox=$Mailbox MailboxFound=$MailboxFound MailboxError=$MailboxError } } #end Function GetMailbox2
Let’s confirm these functions still work as expected:
PS> GetMsolUser2 -Identity "john@example.com"
MsolUser MsolUserFound MsolUserError
-------- ------------- -------------
Microsoft.Online.Administration.User True
PS> GetMailbox2 -Identity "john@example.com"
Mailbox MailboxFound MailboxError
------- ------------ ------------
John Smith True
PS> GetMsolUser2 -Identity "DoesntExist@example.com"
MsolUser MsolUserFound MsolUserError
-------- ------------- -------------
False User Not Found. User: DoesntExist@example.com.
PS> GetMailbox2 -Identity "DoesntExist@example.com"
The operation couldn't be performed because object 'DoesntExist@example.com' couldn't be found on 'DB3PR01A005DC04.EURPR01A005.prod.outlook.com'.
+ CategoryInfo : NotSpecified: (:) [Get-Mailbox], ManagementObjectNotFoundException
+ FullyQualifiedErrorId : [Server=AM2PR01MB0994,RequestId=06bddba8-10c2-42d8-86f9-43067b07e9fc,TimeStamp=12/02/2017 11:10:52] [FailureCategory=Cmdlet-Man
agementObjectNotFoundException] 2E624E0E,Microsoft.Exchange.Management.RecipientTasks.GetMailbox
+ PSComputerName : outlook.office365.com
Mailbox MailboxFound MailboxError
------- ------------ ------------
False
GetMsolUser2 does exactly what we want, whether the user exists or not. When the user doesn’t exist, our custom object makes it clear that the user was not found, and rather than writing the error to the console, includes it in our custom object for further use.
GetMailbox2 works fine when the user exists. But when it doesn’t, the error isn’t caught at all and is written directly to the host – not good. This worked last time when we ran this interactively, but not now that we have put it in a function. I previously said that I believed the Exchange cmdlets ignored $ErrorActionPreference
, this the behaviour I was referring to.
There is a fix. It’s against best practice, but is much easier and arguably better than the alternative using redirection operators. We need to replace $ErrorActionPreference
with $global:ErrorActionPreference
, which modifies the ErrorActionPreference in the global scope rather than the function’s local scope. Modifying global scope variables like this is poor form, as it’s really for the user to decide what they want $ErrorActionPreference
to be. However we mitigate this practice by ensuring that it is set back to exactly how it was in the finally block.
(I tested modifying in the script scope with $script:ErrorActionPreference
– this worked if the function had been dot sourced into the current session, but not otherwise. So it has to be modified globally.)
Function GetMailbox2 { [cmdletbinding()] Param( [string] $Identity ) try { # Exchange cmdlets do not pay attention to the -ErrorAction parameter, # but they do pay attention to $ErrorActionPreference. # Capture the old $ErrorActionPreference first as we should set it back once done. $ErrorActionPreferenceOld = $ErrorActionPreference $global:ErrorActionPreference = 'Stop' Write-Debug "[GetMailbox2] `$global:ErrorActionPreference set to $global:ErrorActionPreference" $Mailbox = Get-Mailbox -Identity $Identity # Double check that we have actually obtained an Mailbox. if ($Mailbox) { Write-Debug '[GetMailbox2] Mailbox found.' $MailboxFound = $true } else { Write-Debug '[GetMailbox2] Mailbox not found.' $MailboxFound = $false } #end else } catch [System.Management.Automation.RemoteException] { Write-Debug '[GetMailbox2] Get-Mailbox error.' # We may need this message for later. $MailboxError = $_.Exception.Message $MailboxErrorRegex = "^The operation couldn't be performed because object '.*' couldn't be found on '.*'\.$" if ($MailboxError -match $MailboxErrorRegex) { # We can handle a mailbox not found, so no need to throw. $MailboxFound = $false } else { # We don't (yet) know how to handle this error, so rethrow for the user to see. throw } #end else } finally { # Set $ErrorActionPreference back to its previous value. $global:ErrorActionPreference = $ErrorActionPreferenceOld Write-Debug "[GetMailbox2] `$global:ErrorActionPreference set to $global:ErrorActionPreference" } #end finally [pscustomobject]@{ Mailbox=$Mailbox MailboxFound=$MailboxFound MailboxError=$MailboxError } } #end Function GetMailbox2
PS> GetMsolUser2 -Identity "DoesntExist@example.com" Mailbox MailboxFound MailboxError ------- ------------ ------------ False The operation couldn't be performed because object 'DoesntExist@example.com' couldn't be found on 'DB3PR01A005DC04.EURPR01A005.prod.ou...
We’re now ready to build our Get-MITUser function.
First, we try to get the MsolUser from the supplied Identity. If we succeed, we use that to get the Mailbox.
$MsolObj = GetMsolUser2 -Identity $Identity if ($MsolObj.MsolUserFound) { # Get the Mailbox from the MsolUser's objectId, which is unique in Exchange. Write-Debug ('[Get-MITUser] MsolUser found. Obtaining Mailbox using {0}.' -F $MsolObj.MsolUser.ObjectId) $MailboxObj = GetMailbox2 -Identity $MsolObj.MsolUser.ObjectId switch ($MailboxObj.MailboxFound) { $true { Write-Debug '[Get-MITUser] Mailbox found from MsolUser.'} $false { Write-Debug '[Get-MITUser] Mailbox not found from MsolUser.' } } #end switch } else {
If we don’t get the MsolUser, we try to get the Mailbox from the supplied Identity. If we succeed, we use that to get the MsolUser.
} else { Write-Debug '[Get-MITUser] MsolUser not found.' $MailboxObj = GetMailbox2 -Identity $Identity if ($MailboxObj.MailboxFound) { # Get the MsolUser from the Mailbox's ExternalDirectoryObjectId, which is unique in Office 365. Write-Debug ('[Get-MITUser] Mailbox found. Obtaining MsolUser using {0}.' -F $MailboxObj.Mailbox.ExternalDirectoryObjectId) $MsolObj = GetMsolUser2 -Identity $MailboxObj.Mailbox.ExternalDirectoryObjectId switch ($MsolObj.MsolUserFound) { $true { Write-Debug '[Get-MITUser] MsolUser found from Mailbox.'} $false { Write-Debug '[Get-MITUser] MsolUser not found from Mailbox.' } } #end switch } else { Write-Debug '[Get-MITUser] Mailbox not found' } #end else } #end else
We now have $MsolUserObj
and $MailboxObj
. Not to say that we have actually found the MsolUser and Mailbox though. So to begin with, if we don’t have either of them, we want to give up and throw both errors to the user.
if ( (-not $MsolObj.MsolUserFound) -and (-not $MailboxObj.MailboxFound) ) { Write-Debug '[Get-MITUser] Neither MsolUser nor Mailbox found.' $ErrorText = "Unable to find user $Identity.`nMsolUserError: {0}`nMailboxError: {1}" -F $MsolObj.MsolUserError,$MailboxObj.MailboxError throw $ErrorText }
If we have either of them, we want to collect some key properties for the object we output. Some of them we prefer from the MsolUser where possible over the Mailbox:
if ($MsolObj.MsolUserFound) { $DisplayName = $MsolObj.MsolUser.DisplayName $UserPrincipalName = $MsolObj.MsolUser.UserPrincipalName $ObjectId = $MsolObj.MsolUser.ObjectId } elseif ($MailboxObj.MailboxFound) { $DisplayName = $MailboxObj.Mailbox.DisplayName $UserPrincipalName = $MailboxObj.Mailbox.UserPrincipalName $ObjectId = $MailboxObj.Mailbox.ExternalDirectoryObjectId } #end elseif
The Email Address however we much prefer from Exchange, since it’s the authoritative source for email. It’s also a lot simpler to obtain than from the MsolUser, which only has the ProxyAddresses attribute for us to work with – an array of all email addresses, with the primary address beginning with “SMTP:”, which we want to trim.
# Obtain key properties where we prefer the Mailbox if ($MailboxObj.MailboxFound) { $PrimarySmtpAddress = $MailboxObj.Mailbox.PrimarySmtpAddress } elseif ($MsolObj.MsolUserFound) { # Return the email address that begins with SMTP (case sensitive) $PrimarySmtpAddress = $MsolObj.MsolUser.ProxyAddresses | ForEach-Object { if ($_ -cmatch '^SMTP:(.*)$') { $Matches[1] } #end if } #end Foreach-Object } #end elseif
Finally, we output all of this as a custom object.
[pscustomobject]@{ DisplayName = $DisplayName UserPrincipalName = $UserPrincipalName PrimarySmtpAddress = $PrimarySmtpAddress ObjectId = $ObjectId MsolUser = $MsolObj.MsolUser MsolUserFound = $MsolObj.MsolUserFound MsolUserError = $MsolObj.MsolUserError Mailbox = $MailboxObj.Mailbox MailboxFound = $MailboxObj.MailboxFound MailboxError = $MailboxObj.MailboxError } #end [pscustomobject]
Our full function is now complete, and we can begin to try it out. We can obtain both the MsolUser and Mailbox from its UPN, ObjectID, Display Name, Email Address, and many more. We can store this in a variable and access both of the original MsolUser and Mailbox objects. If it doesn’t find anything, we get both errors.
Function Get-MITUser { [cmdletbinding()] Param( [string] $Identity ) $MsolObj = GetMsolUser2 -Identity $Identity if ($MsolObj.MsolUserFound) { # Get the Mailbox from the MsolUser's objectId, which is unique in Exchange. Write-Debug ('[Get-MITUser] MsolUser found. Obtaining Mailbox using {0}.' -F $MsolObj.MsolUser.ObjectId) $MailboxObj = GetMailbox2 -Identity $MsolObj.MsolUser.ObjectId switch ($MailboxObj.MailboxFound) { $true { Write-Debug '[Get-MITUser] Mailbox found from MsolUser.'} $false { Write-Debug '[Get-MITUser] Mailbox not found from MsolUser.' } } #end switch } else { Write-Debug '[Get-MITUser] MsolUser not found.' $MailboxObj = GetMailbox2 -Identity $Identity if ($MailboxObj.MailboxFound) { # Get the MsolUser from the Mailbox's ExternalDirectoryObjectId, which is unique in Office 365. Write-Debug ('[Get-MITUser] Mailbox found. Obtaining MsolUser using {0}.' -F $MailboxObj.Mailbox.ExternalDirectoryObjectId) $MsolObj = GetMsolUser2 -Identity $MailboxObj.Mailbox.ExternalDirectoryObjectId switch ($MsolObj.MsolUserFound) { $true { Write-Debug '[Get-MITUser] MsolUser found from Mailbox.'} $false { Write-Debug '[Get-MITUser] MsolUser not found from Mailbox.' } } #end switch } else { Write-Debug '[Get-MITUser] Mailbox not found' } #end else } #end else # Regardless of whether we have found the MsolUser/Mailbox or not, # at this point we will have two objects $MsolObj and $MailboxObj # If we still haven't found either of them, throw, outputting both errors. if ( (-not $MsolObj.MsolUserFound) -and (-not $MailboxObj.MailboxFound) ) { Write-Debug '[Get-MITUser] Neither MsolUser nor Mailbox found.' $ErrorText = "Unable to find user $Identity.`nMsolUserError: {0}`nMailboxError: {1}" -F $MsolObj.MsolUserError,$MailboxObj.MailboxError throw $ErrorText } # Obtain key properties where we prefer the MsolUser if ($MsolObj.MsolUserFound) { $DisplayName = $MsolObj.MsolUser.DisplayName $UserPrincipalName = $MsolObj.MsolUser.UserPrincipalName $ObjectId = $MsolObj.MsolUser.ObjectId } elseif ($MailboxObj.MailboxFound) { $DisplayName = $MailboxObj.Mailbox.DisplayName $UserPrincipalName = $MailboxObj.Mailbox.UserPrincipalName $ObjectId = $MailboxObj.Mailbox.ExternalDirectoryObjectId } #end elseif # Obtain key properties where we prefer the Mailbox if ($MailboxObj.MailboxFound) { $PrimarySmtpAddress = $MailboxObj.Mailbox.PrimarySmtpAddress } elseif ($MsolObj.MsolUserFound) { # Return the email address that begins with SMTP (case sensitive) $PrimarySmtpAddress = $MsolObj.MsolUser.ProxyAddresses | ForEach-Object { if ($_ -cmatch '^SMTP:(.*)$') { $Matches[1] } #end if } #end Foreach-Object } #end elseif [pscustomobject]@{ DisplayName = $DisplayName UserPrincipalName = $UserPrincipalName PrimarySmtpAddress = $PrimarySmtpAddress ObjectId = $ObjectId MsolUser = $MsolObj.MsolUser MsolUserFound = $MsolObj.MsolUserFound MsolUserError = $MsolObj.MsolUserError Mailbox = $MailboxObj.Mailbox MailboxFound = $MailboxObj.MailboxFound MailboxError = $MailboxObj.MailboxError } #end [pscustomobject] } #end Function Get-MITUser
PS> Get-MITUser "john@example.com"
DisplayName : John Smith
UserPrincipalName : john@example.com
PrimarySmtpAddress : john@example.com
ObjectId : 17ff08b1-13bf-4f9d-85da-eece3971216e
MsolUser : Microsoft.Online.Administration.User
MsolUserFound : True
MsolUserError :
Mailbox : John Smith
MailboxFound : True
MailboxError :
PS> Get-MITUser "Does not exist"
Unable to find user Does not exist.
MsolUserError: User Not Found. User: Does not exist.
MailboxError: The operation couldn't be performed because object 'Does not exist' couldn't be found on 'DB3PR01A005DC04.EURPR01A005.prod.outlook.com'.
+ throw $ErrorText
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Unable to find ...d.outlook.com'.:String) [], RuntimeException
+ FullyQualifiedErrorId : Unable to find user Does not exist.
MsolUserError: User Not Found. User: Does not exist.
MailboxError: The operation couldn't be performed because object 'Does not exist' couldn't be found on 'DB3PR01A005DC04.EURPR01A005.prod.outlook.com'.
PS> $DebugPreference = 'Continue'
PS> Get-MITUser -Identity "John Smith"
DEBUG: [GetMsolUser2] Trying as UPN.
DEBUG: [GetMsolUser2] Get-MsolUser Error.
DEBUG: [Get-MITUser] MsolUser not found.
DEBUG: [GetMailbox2] $global:ErrorActionPreference set to Stop
DEBUG: [GetMailbox2] Mailbox found.
DEBUG: [GetMailbox2] $global:ErrorActionPreference set to Continue
DEBUG: [Get-MITUser] Mailbox found. Obtaining MsolUser using 17ff08b1-13bf-4f9d-85da-eece3971216e.
DEBUG: [GetMsolUser2] Trying as GUID.
DEBUG: [GetMsolUser2] MsolUser found.
DEBUG: [Get-MITUser] MsolUser found from Mailbox.
DisplayName : John Smith
UserPrincipalName : john@example.com
PrimarySmtpAddress : john@example.com
ObjectId : 17ff08b1-13bf-4f9d-85da-eece3971216e
MsolUser : Microsoft.Online.Administration.User
MsolUserFound : True
MsolUserError :
Mailbox : John Smith
MailboxFound : True
MailboxError :
PS> $John = Get-MITUser -Identity "John Smith"
PS> $John.MsolUser
UserPrincipalName DisplayName isLicensed
----------------- ----------- ----------
john@example.com John Smith True
In its current form, this function is fairly useful, and will work great as a utility function in scripts in a relatively “clean” Office 365 environment. But there are many improvements to be made, including:
- We haven’t taken into account for various edge cases, which may be common in some environments.
In particular, Get-Mailbox can return multiple results as some attributes do not need to be unique within Exchange (e.g. Name, Display Name) and other attributes do not have to be unique compared to other attributes (e.g. one Mailbox’s Alias can be another Mailbox’s Display Name). How do we want to handle these? Should we error? Output multiple results? Or perhaps have a parameter to decide between both behaviours? - Right now if we obtain the MsolUser first, we don’t try obtaining the Mailbox using the supplied Identity. Should we? If we do, how should we handle retrieving MsolUsers and Mailboxes that do not correspond to each other?
- We should perform some error handling and checking for being connected to Office 365 PowerShell and Exchange Online PowerShell before trying to run their cmdlets.
- It would be nice to obtain some MailboxStatistics as well, at least optionally.
- The list display of the output is not very useful for interactive use. We would much rather have it in a table (similar to the default display of
Get-MsolUser
andGet-Mailbox
), and we would much rather only display certain attributes (DisplayName, UserPrincipalName, the Founds) and hide others (ObjectId, MsolUser, Mailbox, the Errors).
These will be addressed in upcoming parts.