All-in-one Get-User cmdlet: Part 4 – Basic Function Put Together

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:

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.)

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
$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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
} 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.

1
2
3
4
5
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:

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
8
9
10
11
# 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.

1
2
3
4
5
6
7
8
9
10
11
12
[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.

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:

  1. 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?
  2. 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?
  3. 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.
  4. It would be nice to obtain some MailboxStatistics as well, at least optionally.
  5. 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 and Get-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.

Leave a Comment

Your email address will not be published. Required fields are marked *