It's pretty obvious that our Get-MITUser cmdlet isn't yet as visually usable as Microsoft's cmdlets. You can always modify the formatting of a cmdlet by piping it to a Format-* command. First of all this is inconvenient to have to do each time, but also the output of this isn't your original object but an entirely different formatting object. You can no longer operate on it as an object. For example try obtaining the DisplayName from an object that has been formatted - it won't work.
Object Formatting
It’s pretty obvious that our Get-MITUser cmdlet isn’t yet as visually usable as Microsoft’s cmdlets.
PS> Get-MsolUser -UserPrincipalName Vicki@milneitlab01.onmicrosoft.com UserPrincipalName DisplayName isLicensed ----------------- ----------- ---------- Vicki@milneitlab01.onmicrosoft.com Vicki Marsh True PS> Get-Mailbox -Identity Vicki@milneitlab01.onmicrosoft.com Name Alias ServerName ProhibitSendQuota ---- ----- ---------- ----------------- Vicki Vicki mm1p12301mb1609 99 GB (106,300,440,576 bytes) PS> Get-MITUser -Identity Vicki@milneitlab01.onmicrosoft.com DisplayName : Vicki Marsh UserPrincipalName : Vicki@milneitlab01.onmicrosoft.com PrimarySmtpAddress : Vicki@milneitlab01.onmicrosoft.com ObjectId : fe491646-d83c-4043-a6a5-d40b0a3075eb MsolUser : Microsoft.Online.Administration.User MsolUserFound : True MsolUserError : Mailbox : Vicki Marsh MailboxFound : True MailboxError : PS>
You can always modify the formatting of a cmdlet by piping it to a Format-* command. First of all this is inconvenient to have to do each time, but also the output of this isn’t your original object but an entirely different formatting object. You can no longer operate on it as an object. For example try obtaining the DisplayName from an object that has been formatted – it won’t work.
PS> $UserFormatted = Get-MITUser -Identity Vicki@milneitlab01.onmicrosoft.com | Format-Table DisplayName,UserPrincipalName,PrimarySmtpAddress,MsolUserFound,MailboxFound PS> $UserFormatted DisplayName UserPrincipalName PrimarySmtpAddress MsolUserFound MailboxFound ----------- ----------------- ------------------ ------------- ------------ Vicki Marsh Vicki@milneitlab01.onmicrosoft.com Vicki@milneitlab01.onmicrosoft.com True True PS> $UserFormatted.DisplayName PS> $User = Get-MITUser -Identity Vicki@milneitlab01.onmicrosoft.com PS> $User.DisplayName Vicki Marsh PS>
The proper way to handle this is to set a default format, which purely affects the display of the object on the console and does not affect how we can otherwise use the object. To achieve this we need to do the following:
- Give our objects their own unique type names.
- Create a format.ps1xml file that specifies the formatting for our object’s unique type names.
- Create a PowerShell Module Manifest that specifically loads the formatting XML file.
You have a lot of freedom when choosing typenames, but it’s sensible to follow a hierarchical structure similar to .NET types. I am going with MilneIT.MITUser.User. I will use MilneIT for all of my custom PowerShell objects in all of my modules, MITUser for all objects relating to our custom user functions, and User specifically for the normal output of Get-MITUser.
First, we need to modify our code to have a typename for our custom object. This is easy enough; we just need to change the part where we define and output our custom object, from this:
[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]
Into this:
$obj = [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] $obj.PSObject.TypeNames.Insert(0,'MilneIT.MITUser.User') $obj
Now we need to create our format.ps1xml file. This should be created in the same folder as the .psm1 module.
The best way to understand how to create the XML file – the different XML tags and how they should be layered, etc. – is to look at the existing format.ps1xml files that Windows uses for modules that come with Windows. These can generally be found at C:\Windows\System32\WindowsPowerShell\v1.0\
, including the Modules
subfolder.
It is a bit daunting though, particularly as some formats are more complex than others. For example, the format for the output of Get-Process displays the Working Set as WS(K) in Kilobytes, but if you pipe Get-Process to Get-Member you will see that WS(K) isn’t a property of the Process object. If you look at the View for System.Diagnostics.Process in C:\Windows\System32\WindowsPowerShell\v1.0\DotNetTypes.format.ps1xml
, you will see that WS(K) is calculated from the WS property – it divides it by 1024 to convert bytes into kilobytes as it is formatted.
<View> <Name>process</Name> <ViewSelectedBy> <TypeName>System.Diagnostics.Process</TypeName> </ViewSelectedBy> <TableControl> <TableHeaders> <TableColumnHeader> <Label>Handles</Label> <Width>7</Width> <Alignment>right</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>NPM(K)</Label> <Width>7</Width> <Alignment>right</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>PM(K)</Label> <Width>8</Width> <Alignment>right</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>WS(K)</Label> <Width>10</Width> <Alignment>right</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>VM(M)</Label> <Width>5</Width> <Alignment>right</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>CPU(s)</Label> <Width>8</Width> <Alignment>right</Alignment> </TableColumnHeader> <TableColumnHeader> <Width>6</Width> <Alignment>right</Alignment> </TableColumnHeader> <TableColumnHeader> <Width>3</Width> <Alignment>right</Alignment> </TableColumnHeader> <TableColumnHeader /> </TableHeaders> <TableRowEntries> <TableRowEntry> <TableColumnItems> <TableColumnItem> <PropertyName>HandleCount</PropertyName> </TableColumnItem> <TableColumnItem> <ScriptBlock>[long]($_.NPM / 1024)</ScriptBlock> </TableColumnItem> <TableColumnItem> <ScriptBlock>[long]($_.PM / 1024)</ScriptBlock> </TableColumnItem> <TableColumnItem> <ScriptBlock>[long]($_.WS / 1024)</ScriptBlock> </TableColumnItem> <TableColumnItem> <ScriptBlock>[long]($_.VM / 1048576)</ScriptBlock> </TableColumnItem> <TableColumnItem> <ScriptBlock> if ($_.CPU -ne $()) { $_.CPU.ToString("N") } </ScriptBlock> </TableColumnItem> <TableColumnItem> <PropertyName>Id</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>SI</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>ProcessName</PropertyName> </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> </View>
We don’t need any complex functionality. The following file will format our sole custom object as a simple table:
<?xml version="1.0" encoding="utf-8" ?> <Configuration> <ViewDefinitions> <View> <Name>Default</Name> <ViewSelectedBy> <TypeName>MilneIT.MITUser.User</TypeName> </ViewSelectedBy> <TableControl> <TableRowEntries> <TableRowEntry> <TableColumnItems> <TableColumnItem> <PropertyName>DisplayName</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>UserPrincipalName</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>PrimarySmtpAddress</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>MsolUserFound</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>MailboxFound</PropertyName> </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> </View> </ViewDefinitions> </Configuration>
Now we need to create our Module Manifest, again in the same folder as the .psm1 module and the .formats.ps1xml formats. Rather than create it from scratch, we can use the New-ModuleManifest cmdlet to create one from a template. We can either pass parameters to New-ModuleManifest, or we can edit the file it creates, or a combination of the two. In this case we specify everything on the command line. The important parts are the -RootModule
(otherwise loading the module won’t load any code) and the -FormatsToProcess
(otherwise the formats aren’t loaded and this whole exercise is pointless!)
PS> New-ModuleManifest -Path "C:\Users\Ryan\OneDrive\Scripts\Milne.IT\PowerShell\Modules\MilneIT\MilneIT.psd1" -RootModule "MilneIT.psm1" -FormatsToProcess 'MilneIT.format.ps1xml' -RequiredModules 'MSOnline' -CmdletsToExport '*' -Author 'Ryan Milne' -CompanyName 'Ryan Milne' PS>
Now, unload and reload your module, and try it out. You will see that the output is now formatted as a table, with fewer properties displayed. But we can always work with the now-“hidden” properties exactly as normal. We can access them with Select-Object or $Variable.Property
, we can filter with Where-Object, etc.
PS> Remove-Module MilneIT PS> Import-Module MilneIT PS> Get-MITUser Will@milneitlab01.onmicrosoft.com DisplayName UserPrincipalName PrimarySmtpAddress MsolUserFound MailboxFound ----------- ----------------- ------------------ ------------- ------------ Will Cook Will2@milneitlab01.onmicrosoft.com Will@milneitlab01.onmicrosoft.com True True Will Richard Will@milneitlab01.onmicrosoft.com True False PS> Get-MITUser -Identity Roy@milneitlab01.onmicrosoft.com | Select-Object -ExpandProperty MsolUser UserPrincipalName DisplayName isLicensed ----------------- ----------- ---------- Roy@milneitlab01.onmicrosoft.com Roy Hitchcock True PS> $Vicki = Get-MITUser -Identity Vicki PS> $Vicki.Mailbox Name Alias ServerName ProhibitSendQuota ---- ----- ---------- ----------------- Vicki Vicki mm1p12301mb1609 99 GB (106,300,440,576 bytes) PS>
Mailbox Statistics
I said previously that it would be good to include Mailbox Statistics in the output as well. I mean, the intention of this cmdlet is that it’s an all-in-one utility function to be easy to use and to save time. The only concession we will make is that we will make this optional; controllable by a switch. We will do this because calling Get-MailboxStatistics does increase the time our cmdlet takes to finish.
First, let’s make a function exactly like GetMailbox2, but for MailboxStatistics instead. The only real difference is that a different Regex is used for determining whether we’ve encountered a “not found” error. (We put the regex in a single-quoted here-string, as it’s the best way to handle a string that includes both double and single quotes).
$MailboxStatisticsErrorRegex = @' ^The specified mailbox ".*" doesn't exist. '@ Function GetMailboxStatistics2 { [cmdletbinding()] Param( [string] $Identity ) try { $MailboxStatistics = $null $MailboxStatisticsError = $null # 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 "[GetMailboxStatistics2] `$global:ErrorActionPreference set to $global:ErrorActionPreference" $MailboxStatistics = Get-MailboxStatistics -Identity $Identity # Double check that we have actually obtained an MailboxStatistics. if ($MailboxStatistics) { Write-Debug '[GetMailboxStatistics2] MailboxStatistics found.' $MailboxStatisticsFound = $true } else { Write-Debug '[GetMailboxStatistics2] MailboxStatistics not found.' $MailboxStatisticsFound = $false } #end else } catch [System.Management.Automation.RemoteException] { Write-Debug '[GetMailboxStatistics2] Get-MailboxStatistics error.' # We may need this message for later. $MailboxStatisticsError = $_.Exception.Message if ($MailboxStatisticsError -match $MailboxStatisticsErrorRegex) { # We can handle a MailboxStatistics not found, so no need to throw. $MailboxStatisticsFound = $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 "[GetMailboxStatistics2] `$global:ErrorActionPreference set to $global:ErrorActionPreference" } #end finally [pscustomobject]@{ MailboxStatistics=$MailboxStatistics MailboxStatisticsFound=$MailboxStatisticsFound MailboxStatisticsError=$MailboxStatisticsError } } #end Function GetMailboxStatistics2
Now, let’s modify our NewMITUserObj function to accept the additional object.
Function NewMITUserObj { [cmdletbinding()] Param( [Parameter(mandatory=$true)] $MsolObj, [Parameter(mandatory=$true)] $MailboxObj, $MailboxStatisticsObj ) # ...
And then use Add-Member to add additional properties to our output object, if MailboxStatisticsObj was given.
# ... $obj = [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] if ($MailboxStatisticsObj) { # Additional properties regardless of whether we found the Mailbox Statistics or not. $additional = [ordered]@{ MailboxStatistics = $MailboxStatisticsObj.MailboxStatistics MailboxStatisticsFound = $MailboxStatisticsObj.MailboxStatisticsFound MailboxStatisticsError = $MailboxStatisticsObj.MailboxStatisticsError } #end [ordered] # Additional properties dependent on whether we found the mailbox statistics or not. # The somewhat roundabout way of doing it is so we do not have an error with StrictMode. if ($MailboxStatisticsObj.MailboxStatisticsFound) { $additional2 = [ordered]@{ ItemCount = $MailboxStatisticsObj.MailboxStatistics.ItemCount TotalItemSize = $MailboxStatisticsObj.MailboxStatistics.TotalItemSize.Value } #end [ordered] } else { $additional2 = [ordered]@{ ItemCount = $null TotalItemSize = $null } #end [ordered] } #end else $obj | Add-Member -NotePropertyMembers $additional $obj | Add-Member -NotePropertyMembers $additional2 } #end if ($MailboxStatisticsObj) $obj.PSObject.TypeNames.Insert(0,'MilneIT.MITUser.User') $obj } #end Function NewMITUserObj
Now onto Get-MITUser. We add a switch parameter for whether MailboxStatistics are included in the output. We have it default to false as, again, Get-MailboxStatistics takes time and we only want that if the user has asked for it.
Function Get-MITUser { [cmdletbinding()] Param( [Parameter(mandatory=$true)] [string] $Identity, [switch] $IncludeMailboxStatistics=$false ) # ...
Then we rewrite the foreach loop to include the MailboxStatistics dependent on the switch. Note the following:
- Unlike Get-Mailbox, Get-MailboxStatistics’s -Identity does not accept the ObjectId. (The output of the cmdlet when you try it suggests this is a bug.) At the point we call it we already have the Mailbox, so we will use the Mailbox’s Guid to obtain the MailboxStatistics. This Guid is another globally unique attribute which is not the same as the ObjectId. We could also use the ExchangeGuid among other things.
- At this point, our if statements will get very messy due to the multiple combinations we would have to call
$MITUsers += NewMITUserObj
. So instead, we rewrite this block using the PowerShell technique splatting. This is where instead of passing parameters directly to a cmdlet, we put them in a variable as a hashtable, and then “splat” those parameters onto the cmdlet (by using@variable
instead of$variable
). One valuable advantage of this is that we can create the hashtable with some parameters, and then add other parameters into this hashtable as we go along – which we can do based on conditional statements. This greatly simplifies our code by reducing the need for nested if statements for every combination. We now only need a single line that invokes NewMITUserObj, but the parameters we splat will vary depending on the preceding conditional statements.
foreach ($Mailbox in $MailboxesObj.Mailbox) { Write-Debug ('[Get-MITUser] Mailbox: {0}.' -F $Mailbox.ExternalDirectoryObjectId) $MailboxObj = [pscustomobject]@{ Mailbox=$Mailbox MailboxFound=$true MailboxError=$null } <# If $MsolObj.MsolUser.ObjectId.Guid does not exist, referencing it would cause an error if StrictMode is set in PowerShell. $MsolObj.MsolUser.ObjectId.Guid will definitely be set if $MsolObj.MsolUserFound is true. PowerShell processes boolean operators from left to right. If $MsolObj.MsolUserFound is $false, it does not process past the first -and because it knows the result must be $false. This prevents any StrictMode errors. #> $MsolMailboxMatch = $MsolObj.MsolUserFound -and ($MsolObj.MsolUser.ObjectId.Guid -eq $Mailbox.ExternalDirectoryObjectId) $NewMITUserObjParams = @{ MailboxObj=$MailboxObj } if ($IncludeMailboxStatistics) { $MailboxStatisticsObj = GetMailboxStatistics2 -Identity $Mailbox.Guid.Guid $NewMITUserObjParams.Add('MailboxStatisticsObj',$MailboxStatisticsObj) } #end if ($IncludeMailboxStatistics) if ($MsolMailboxMatch) { # We have matched, so create the MITUser object and add it to our final output. Write-Debug ('[Get-MITUser] Mailbox {0} matched with MsolUser.' -F $Mailbox.ExternalDirectoryObjectId) $NewMITUserObjParams.Add('MsolObj',$MsolObj) $MsolMailboxMatchFound = $true } else { Write-Debug ('[Get-MITUser] Mailbox did not match with MsolUser. Obtaining MsolUser using {0}' -F $Mailbox.ExternalDirectoryObjectId) # We have not matched, so see if we can find an MsolUser for this mailbox. $LoopMsolObj = GetMsolUser2 -Identity $MailboxObj.Mailbox.ExternalDirectoryObjectId $NewMITUserObjParams.Add('MsolObj',$LoopMsolObj) } #end else $MITUsers += NewMITUserObj @NewMITUserObjParams } #end foreach ($MailboxObj in $MailboxesObj)
We do similar for the second phase where we have not matched the MsolUser with a mailbox yet. Unlike above where we had a Mailbox to begin with, here we have the MsolUser and may not actually obtain the Mailbox (i.e. if there isn’t one). So we have to handle the case where there is no Mailbox by creating a largely empty $MailboxStatisticsObj
.
if ($MsolObj.MsolUserFound -and ($MsolMailboxMatchFound -eq $false) ) { Write-Debug ('[Get-MITUser] MsolUser found but did not match any Mailbox. Obtaining Mailbox using {0}.' -F $MsolObj.MsolUser.ObjectId) $MailboxObj = GetMailbox2 -Identity $MsolObj.MsolUser.ObjectId $NewMITUserObjParams = @{ MsolObj = $MsolObj MailboxObj = $MailboxObj } if ($IncludeMailboxStatistics) { if ($MailboxObj.MailboxFound) { $MailboxStatisticsObj = GetMailboxStatistics2 -Identity $MailboxObj.Mailbox.Guid.Guid } else { $MailboxStatisticsObj = [pscustomobject]@{ MailboxStatistics=$null MailboxStatisticsFound=$false MailboxStatisticsError=$null } #end [pscustomobject] } #end else $NewMITUserObjParams.Add('MailboxStatisticsObj',$MailboxStatisticsObj) } #end if ($IncludeMailboxStatistics) $MITUsers += NewMITUserObj @NewMITUserObjParams } $MITUsers } #end Function Get-MITUser
Now, if we re-import our module, we can see that the MailboxStatistics properties are now included.
PS> Get-MITUser -Identity Vicki -IncludeMailboxStatistics | Format-List * DisplayName : Vicki Marsh UserPrincipalName : Vicki@milneitlab01.onmicrosoft.com PrimarySmtpAddress : Vicki@milneitlab01.onmicrosoft.com ObjectId : fe491646-d83c-4043-a6a5-d40b0a3075eb MsolUser : Microsoft.Online.Administration.User MsolUserFound : True MsolUserError : Mailbox : Vicki Marsh MailboxFound : True MailboxError : MailboxStatistics : Microsoft.Exchange.Management.MapiTasks.Presentation.MailboxStatistics MailboxStatisticsFound : True MailboxStatisticsError : ItemCount : 116 TotalItemSize : 646.2 KB (661,673 bytes) PS> Get-MITUser -Identity vicki -IncludeMailboxStatistics | Select -ExpandProperty MailboxStatistics DisplayName ItemCount StorageLimitStatus LastLogonTime ----------- --------- ------------------ ------------- Vicki Marsh 116 01/03/2017 14:05:23
But if the Get-MITUser formatted output doesn’t include any MailboxStatistics, because those properties are not in our format.
PS> Get-MITUser -Identity vicki -IncludeMailboxStatistics DisplayName UserPrincipalName PrimarySmtpAddress MsolUserFound MailboxFound ----------- ----------------- ------------------ ------------- ------------ Vicki Marsh Vicki@milneitlab01.onmicrosoft.com Vicki@milneitlab01.onmicrosoft.com True True
How should we resolve this? If MailboxStatistics are not requested, we don’t want to display empty columns for TotalItemSize and ItemCount, as that would be very misleading.
The way I like to handle this is to introduce a separate type name and format view for when Mailbox Statistics are included. In MilneIT.format.ps1xml, right after we define end the View tag for MilneIT.MITUser.User, we create a new one for MilneIT.MITUser.UserWithMailboxStatistics. It’s largely the same but with two extra fields.
<View> <Name>Default</Name> <ViewSelectedBy> <TypeName>MilneIT.MITUser.UserWithMailboxStatistics</TypeName> </ViewSelectedBy> <TableControl> <TableRowEntries> <TableRowEntry> <TableColumnItems> <TableColumnItem> <PropertyName>DisplayName</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>UserPrincipalName</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>PrimarySmtpAddress</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>MsolUserFound</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>MailboxFound</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>ItemCount</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>TotalItemSize</PropertyName> </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> </View>
Then we just modify our NewMITUserObj function to choose the type name based on whether MailboxStatistics are included or not.
if ($MailboxStatisticsObj) { $TypeName = 'MilneIT.MITUser.UserWithMailboxStatistics' # ... } else { $TypeName = 'MilneIT.MITUser.User' } #end else $obj.PSObject.TypeNames.Insert(0,$TypeName) $obj
If we reimport the module, the result is that we now have a different format based on whether we have included mailbox statistics or not.
PS> Get-MITUser -Identity Vicki DisplayName UserPrincipalName PrimarySmtpAddress MsolUserFound MailboxFound ----------- ----------------- ------------------ ------------- ------------ Vicki Marsh Vicki@milneitlab01.onmicrosoft.com Vicki@milneitlab01.onmicrosoft.com True True PS> Get-MITUser -Identity Vicki -IncludeMailboxStatistics DisplayName UserPrincipalName PrimarySmtpAddress MsolUserFound MailboxFound ItemCount TotalItemSize ----------- ----------------- ------------------ ------------- ------------ --------- ------------- Vicki Marsh Vicki@milneitlab01.onmicrosoft.com Vicki@milneitlab01.onmicrosoft.com True True 116 646.2 KB (661,673 bytes)
We can see that it was definitely worth making the inclusion of MailboxStatistics optional, as it roughly doubles how long it takes the command to execute.
PS> Measure-Command {Get-MITUser Will@milneitlab01.onmicrosoft.com } Days : 0 Hours : 0 Minutes : 0 Seconds : 0 Milliseconds : 737 Ticks : 7374708 TotalDays : 8.53554166666667E-06 TotalHours : 0.000204853 TotalMinutes : 0.01229118 TotalSeconds : 0.7374708 TotalMilliseconds : 737.4708 PS> Measure-Command {Get-MITUser Will@milneitlab01.onmicrosoft.com -IncludeMailboxStatistics} Days : 0 Hours : 0 Minutes : 0 Seconds : 1 Milliseconds : 515 Ticks : 15155650 TotalDays : 1.75412615740741E-05 TotalHours : 0.000420990277777778 TotalMinutes : 0.0252594166666667 TotalSeconds : 1.515565 TotalMilliseconds : 1515.565
The full MilneIT.psm1 module as of this blog post can be viewed on Bitbucket here. You can always view the latest version of the full repository here.