Mobile device inventory and statistics report (2023 updated version)

Inventorying mobile devices remains one of the more challenging tasks in Microsoft 365, even with the latest improvements in the service. In this article, we will examine an improved version of the old mobile devices inventory and statistics script, which I last updated 5 years ago.

As before, we want to avoid iterating over each mailbox, as there are likely to be at least few mailboxes with no mobile device configured. Instead, we can use the Get-MobileDevice cmdlet to return the full set of mobile devices within the organizations. As the cmdlet exposes a limited set of properties though, we then loop over each device in order to “enrich” the output with details, such as the UserPrincipalName and PrimarySmtpAddress of the device owner. Herein, we can reuse an already obtained mailbox inventory report file, generated via your preferred script or exported directly from the Exchange Admin Center UI.

In the current version of the script this is handled via the Load-MailboxMatchInputFile helper function. The function will check for the existence of any CSV files in the script directory that have names ending with “*MailboxReport”. If any such CSV file is found, and its last modified date is less than 30 days ago, the data from said file will be used. Otherwise, the Get-ExOMailbox cmdlet will be used to generate a new report.

Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox | Select-Object DisplayName,Alias,UserPrincipalName,DistinguishedName,PrimarySmtpAddress

Note that we are only including User mailboxes in the output, in order to minimize the time it takes to run the script. In some organizations, you can find mobile devices configured for other mailbox types, such as shared mailbox. If this is a scenario you want to cover, consider updating the filter statement (line 36 in the script). Similarly, we only gather a handful of properties – if more are needed, make sure to amend the list.

After we get the mailbox data, another helper function is used to “store” it a hash table, using the DistinguishedName value as key. Getting the DistinguishedName value for each mobile device allows us to get the DistinguishedName value for the corresponding mailbox as well, which in turn makes obtaining the mailbox properties very fast, thanks to the hash table lookup. This is all enabled via the arrayToHash and Get-MailboxMatch helper functions.

Once we have all the mobile devices and mailboxes data, we simply prepare the output by iterating against each mobile device and inserting the additional properties. Apart from information about the device owner, in most cases you will likely want to include some of the properties exposed via the Get-EXOMobileDeviceStatistics cmdlet, such as when did the device last sync with the server. This unfortunately means the script execution will slow down considerably, as the cmdlet will be run against each device – there is no way to obtain this data in bulk for all devices.

Compared to the previous version of the script, the Get-EXOMobileDeviceStatistics should bring some improvements in terms of reliability and speed. At least that’s the theory. In practice, accommodating the script to use the RESTful cmdlet turned to be a bit more complicated than expected, due to some changes in the input/output. Without boring you with too many details, using the device’s Guid property seems to be a bad option, as it slows the cmdlet execution considerably. In addition, the DistinguishedName property cannot be used as input, and throws an error. And, the Identity property uses a different format/value, compared to the output of the Get-MobileDevice cmdlet.

At the end, all these annoyances can be worked around. To craft a “suitable” value for the –Identity parameter, we use the OrganizationId and Identity values as reported by Get-MobileDevice, and perform some basic string manipulation operations. Once a suitable value is crafted, we use the Get-EXOMobileDeviceStatistics cmdlet to include four additional properties in the output: LastSuccessSync, Status, DevicePolicyApplied and DevicePolicyApplicationStatus. If you don’t need those properties, make sure to comment out the corresponding lines (92-103) – the script will run much, much faster.

Speaking of additional properties, the addition of mailbox properties doesn’t bring a penalty because of the method we use, so you can include as many of those in the output as needed. Make sure to have the corresponding properties in the CSV file used as input for the hash table, or add the properties to the Get-ExOMailbox cmdlet (line 36), as needed. However, if you want to include properties from other cmdlets, perhaps the licensing data as obtained via the Graph SDK for PowerShell or the good old MSOnline module, the script runtime will greatly increase. An example on how to do that is included in line 114.

That about covers the script’s logic. Go and downloaded it from my GitHub repo. There are no parameters you need to pass, though it might be a good idea to add one to handle other mailbox types. If you want to speed up the execution a bit, make sure to place your mailbox report CSV file within the script directory. Oh, and do make sure you use a recent version of the Exchange Online module, as the script leverages some of the REST cmdlets. Here’s an example on how to run the script:

.\Mobile_devices_inventoryV2.ps1

The script will output the report to a CSV file in the current directory. Here’s how the output will look like (with some columns hidden):

Output of the Mailbox devices inventory scriptAs usual, hit me up with any comments or suggestions about the script, or make the changes yourself 🙂

17 thoughts on “Mobile device inventory and statistics report (2023 updated version)

  1. Stanvy says:

    I crafted a few test cases to compare the overall performance. Each test had been run a few times to ensure there are no significant deviations in duration due to whatever might happening in a live environment, so the value ratios are representative. All the tests have been run in cloud only, where number of mobile devices is not a huge, but sufficient to get estimations.

    The set of tests:
    1. Classic command via “Identity”.
    2. Classic command via “Mailbox”. Unlike previous one, all the device statistics returned by one request, therefore we need to process each user only once.
    3. EXO command via “Mailbox”.
    4. EXO command via “Mailbox” with pre-cached recipients. Time difference with the previous one shows how pricy “Get-Recipient” is, which triggered that comment in the first place.
    5. EXO command via “Identity”.

    To measure timing I surrounded code with:

    $objElapsedTimeStopWatch = [System.Diagnostics.Stopwatch]::StartNew()
    ...
    [System.TimeSpan]::FromSeconds([System.Int64] $objElapsedTimeStopWatch.Elapsed.TotalSeconds).ToString()

    1. Classic command via “Identity”:

    Get-MobileDevice -ResultSize "Unlimited" |
        ForEach-Object -Process {
            $strIdentity = $_.OrganizationId.Split("-")[0].TrimEnd() + "/"+ $_.Identity.Replace("\","/")
            Get-MobileDeviceStatistics -Identity $strIdentity
        } | Export-Csv -Path "MobileDeviceStatistics.csv" -NoTypeInformation

    Execution time: 11:40

    2. Classic command via “Mailbox”:

    $setProcessedDNs = New-Object -TypeName System.Collections.Generic.HashSet[System.String]
    Get-MobileDevice -ResultSize "Unlimited" |
        ForEach-Object -Process {
            $strUserDN = $_.DistinguishedName -replace '^(?:.+?(?<!\\),){2}(.+)$', '$1'
            if (!$setProcessedDNs.Add($strUserDN))
            {
                return
            }
    
            $objRecipient = Get-Recipient -Identity $strUserDN
            Get-MobileDeviceStatistics -Mailbox $objRecipient.PrimarySmtpAddress
        } | Export-Csv -Path "MobileDeviceStatistics.csv" -NoTypeInformation

    Execution time: 10:59

    3. EXO command via “Mailbox”:

    $setProcessedDNs = New-Object -TypeName System.Collections.Generic.HashSet[System.String]
    Get-MobileDevice -ResultSize "Unlimited" |
        ForEach-Object -Process {
            $strUserDN = $_.DistinguishedName -replace '^(?:.+?(?<!\\),){2}(.+)$', '$1'
            if (!$setProcessedDNs.Add($strUserDN))
            {
                return
            }
    
            $objRecipient = Get-Recipient -Identity $strUserDN
            Get-EXOMobileDeviceStatistics -Mailbox $objRecipient.PrimarySmtpAddress
        } | Export-Csv -Path "MobileDeviceStatistics.csv" -NoTypeInformation

    Execution time: 3:31

    4. EXO command via “Mailbox” with pre-cached recipients:
    Recipients pre-cache – run separately, so the duration is not included in the measurement:

    $hshRecipients = @{}
    Get-Recipient -ResultSize "Unlimited" | ForEach-Object -Process {$hshRecipients.Add($_.DistinguishedName, $_)}
    $setProcessedDNs = New-Object -TypeName System.Collections.Generic.HashSet[System.String]
    Get-MobileDevice -ResultSize "Unlimited" |
        ForEach-Object -Process {
            $strUserDN = $_.DistinguishedName -replace '^(?:.+?(?<!\\),){2}(.+)$', '$1'
            if (!$setProcessedDNs.Add($strUserDN))
            {
                return
            }
    
            $objRecipient = $hshRecipients[$strUserDN]
            Get-EXOMobileDeviceStatistics -Mailbox $objRecipient.PrimarySmtpAddress
        } | Export-Csv -Path "MobileDeviceStatistics.csv" -NoTypeInformation

    Execution time: 2:41

    5. EXO command via “Identity”:

    Get-MobileDevice -ResultSize "Unlimited" |
        ForEach-Object -Process {
            $strIdentity = $_.OrganizationId.Split("-")[0].TrimEnd() + "/"+ $_.Identity.Replace("\","/")
            Get-EXOMobileDeviceStatistics -Identity $strIdentity
        } | Export-Csv -Path "MobileDeviceStatistics.csv" -NoTypeInformation

    Execution time: 2:38

    As results show, in case of EXO command, using “Identity” and “Mailbox” parameter can be considered the same from performance perspective – the minor deviations can be ignored as I’ve observed fluctuations going both ways.
    The difference between 3 and 4 is 50 seconds, which shows the cost of “Get-Recipient” call. From the ratio perspective it can be considered as quite high, but for our inventory scenario is not a big deal.
    At the same time, in case of using Classic command (tests 1 and 2) that price is overcompensated by slowness of “Get-MobileDeviceStatistics” required for each device.

    After the tests, I went through your code one more time with more attention to the details and noticed few other areas that could be improved.

    In this exact case, your implementation of progress bar is not a limiting factor, but when objects processing is fast, it becomes a bottleneck due to perfroming display update for an each item. To overcome that I’d suggest to do it by intervals:

    $objElapsedTimeStopWatch = [System.Diagnostics.Stopwatch]::StartNew()
    $objProgressBarUpdateStopWatch = [System.Diagnostics.Stopwatch]::StartNew()
    $iProgressBarUpdateIntervalInMilliseconds = 100
    
    $iPercentsCompleted = -1
    $iItemsProcessed = 0
    $strProgressBarActivity = "Processing objects:"
    ...
    if ($objProgressBarUpdateStopWatch.ElapsedMilliseconds -ge $iProgressBarUpdateIntervalInMilliseconds)
    {
        $strProgressBarStatus = $iItemsProcessed.ToString("N0") + " processed so far..."
        $strProgressBarOperation = "Time elapsed: " + [System.TimeSpan]::FromSeconds([System.Int64] $objElapsedTimeStopWatch.Elapsed.TotalSeconds).ToString()
    
        Write-Progress -Activity $strProgressBarActivity -Status $strProgressBarStatus -PercentComplete $iPercentsCompleted -CurrentOperation $strProgressBarOperation
    
        $objProgressBarUpdateStopWatch.Reset()
        $objProgressBarUpdateStopWatch.Start()
    }
    $iItemsProcessed++

    The downside of this implementation though, is being “jerky” when single object processing takes longer than interval. To address that and have a “perfect display”, it needs to be run in a separate thread, which is not that complicated as it might sound.

    In my experience I’ve never faced any issues looping over a big set of objects, so, from my point of view, artificial delay seems to be redundant. Did you put it as a precaution or had the real cases where it was necessary?

    As I’ve indirectly mentioned before, current implementation lacks “universality” due to hard dependency on REST API. In order to accommodate both EXO and Classic commands withing single solution, we can do something like this:

    $OriginalModuleAutoloading = $PSModuleAutoloadingPreference
    $PSModuleAutoloadingPreference = "None"
    
    $sbGetMobileDeviceStatistics = {Get-MobileDeviceStatistics -Identity $strIdentity}
    if ($null -ne (Get-Command -Name "Get-EXOMobileDeviceStatistics" -CommandType ([System.Management.Automation.CommandTypes]::Cmdlet) -ErrorAction ([System.Management.Automation.ActionPreference]::SilentlyContinue)))
    {
        $sbGetMobileDeviceStatistics = {Get-EXOMobileDeviceStatistics -Identity $strIdentity}
    }
    
    $PSModuleAutoloadingPreference = $OriginalModuleAutoloading
    ...
    Invoke-Command -ScriptBlock $sbGetMobileDeviceStatistics

    Unfortunately, even thought your current approach of generating “Identity” value works for the cloud, it doesn’t for on-premises.
    The reliable way to construct correct value should be based on “DistinguishedName” of mobile device instead:

    $colMatches = [System.Text.RegularExpressions.Regex]::Matches($strMobileDeviceDN, '^(?:.+?=(?<Names>.+?)(?<!\\),)+?DC=(?<Domain>.+)$')
    
    $strIdentity = $colMatches[0].Groups["Domain"].Value.Replace(",DC=", ".")
    
    $colNames = $colMatches[0].Groups["Names"].Captures
    $iIndex = $colNames.Count
    while ($iIndex--)
    {
        $strIdentity = [System.String]::Concat($strIdentity, "/", $colNames[$iIndex].Value)
    }

    And one last thing – combining all the pieces together, the simplified concept that shows progress, so we don’t get bored staring at static console and can visually estimate execution speed, covers any environment and allows to leverage EXO benefits:

    $OriginalModuleAutoloading = $PSModuleAutoloadingPreference
    $PSModuleAutoloadingPreference = "None"
    
    $sbGetMobileDeviceStatistics = {Get-MobileDeviceStatistics -Identity $strIdentity}
    if ($null -ne (Get-Command -Name "Get-EXOMobileDeviceStatistics" -CommandType ([System.Management.Automation.CommandTypes]::Cmdlet) -ErrorAction ([System.Management.Automation.ActionPreference]::SilentlyContinue)))
    {
        $sbGetMobileDeviceStatistics = {Get-EXOMobileDeviceStatistics -Identity $strIdentity}
    }
    
    $PSModuleAutoloadingPreference = $OriginalModuleAutoloading
    
    
    $objElapsedTimeStopWatch = [System.Diagnostics.Stopwatch]::StartNew()
    $objProgressBarUpdateStopWatch = [System.Diagnostics.Stopwatch]::StartNew()
    $iProgressBarUpdateIntervalInMilliseconds = 100
    
    Get-MobileDevice -ResultSize "Unlimited" |
        ForEach-Object -Begin {
            $iPercentsCompleted = -1
            $iItemsProcessed = 0
            $strProgressBarActivity = "Processing objects:"
        } -Process {
            if ($objProgressBarUpdateStopWatch.ElapsedMilliseconds -ge $iProgressBarUpdateIntervalInMilliseconds)
            {
                $strProgressBarStatus = $iItemsProcessed.ToString("N0") + " processed so far..."
                $strProgressBarOperation = "Time elapsed: " + [System.TimeSpan]::FromSeconds([System.Int64] $objElapsedTimeStopWatch.Elapsed.TotalSeconds).ToString()
    
                Write-Progress -Activity $strProgressBarActivity -Status $strProgressBarStatus -PercentComplete $iPercentsCompleted -CurrentOperation $strProgressBarOperation
    
                $objProgressBarUpdateStopWatch.Reset()
                $objProgressBarUpdateStopWatch.Start()
            }
            $iItemsProcessed++
    
    
            $colMatches = [System.Text.RegularExpressions.Regex]::Matches($_.DistinguishedName, '^(?:.+?=(?<Names>.+?)(?<!\\),)+?DC=(?<Domain>.+)$')
            if (!(($colMatches.Count -eq 1) -and $colMatches[0].Success))
            {
                return
            }
    
            $strIdentity = $colMatches[0].Groups["Domain"].Value.Replace(",DC=", ".")
    
            $colNames = $colMatches[0].Groups["Names"].Captures
            $iIndex = $colNames.Count
            while ($iIndex--)
            {
                $strIdentity = [System.String]::Concat($strIdentity, "/", $colNames[$iIndex].Value)
            }
    
            Invoke-Command -ScriptBlock $sbGetMobileDeviceStatistics |
                Write-Output
        } |
            Export-Csv -Path "MobileDeviceStatistics.csv" -NoTypeInformation

    In my use-cases though, I prefer to have primary email address of device owner for further pivoting, so with described above performance penalties, would stick to:

    $OriginalModuleAutoloading = $PSModuleAutoloadingPreference
    $PSModuleAutoloadingPreference = "None"
    
    $sbGetMobileDeviceStatistics = {Get-MobileDeviceStatistics -Mailbox $objRecipient.PrimarySmtpAddress}
    if ($null -ne (Get-Command -Name "Get-EXOMobileDeviceStatistics" -CommandType ([System.Management.Automation.CommandTypes]::Cmdlet) -ErrorAction ([System.Management.Automation.ActionPreference]::SilentlyContinue)))
    {
        $sbGetMobileDeviceStatistics = {Get-EXOMobileDeviceStatistics -Mailbox $objRecipient.PrimarySmtpAddress}
    }
    
    $PSModuleAutoloadingPreference = $OriginalModuleAutoloading
    
    
    $objElapsedTimeStopWatch = [System.Diagnostics.Stopwatch]::StartNew()
    $objProgressBarUpdateStopWatch = [System.Diagnostics.Stopwatch]::StartNew()
    $iProgressBarUpdateIntervalInMilliseconds = 100
    
    Get-MobileDevice -ResultSize "Unlimited" |
        ForEach-Object -Begin {
            $iPercentsCompleted = -1
            $iItemsProcessed = 0
            $strProgressBarActivity = "Processing objects:"
    
            $setProcessedDNs = New-Object -TypeName System.Collections.Generic.HashSet[System.String]
        } -Process {
            if ($objProgressBarUpdateStopWatch.ElapsedMilliseconds -ge $iProgressBarUpdateIntervalInMilliseconds)
            {
                $strProgressBarStatus = $iItemsProcessed.ToString("N0") + " processed so far..."
                $strProgressBarOperation = "Time elapsed: " + [System.TimeSpan]::FromSeconds([System.Int64] $objElapsedTimeStopWatch.Elapsed.TotalSeconds).ToString()
    
                Write-Progress -Activity $strProgressBarActivity -Status $strProgressBarStatus -PercentComplete $iPercentsCompleted -CurrentOperation $strProgressBarOperation
    
                $objProgressBarUpdateStopWatch.Reset()
                $objProgressBarUpdateStopWatch.Start()
            }
            $iItemsProcessed++
    
    
            $strUserDN = [System.Text.RegularExpressions.Regex]::Replace($_.DistinguishedName, '^(?:.+?(?<!\\),){2}(.+)$', '$1')
            if (!$setProcessedDNs.Add($strUserDN))
            {
                return
            }
    
            $objRecipient = Get-Recipient -Identity $strUserDN
            Invoke-Command -ScriptBlock $sbGetMobileDeviceStatistics |
                Add-Member -NotePropertyName "PrimarySmtpAddress" -NotePropertyValue $objRecipient.PrimarySmtpAddress -PassThru |
                    Write-Output
        } |
            Export-Csv -Path "MobileDeviceStatistics.csv" -NoTypeInformation

    Another reasons I prefer “Mailbox” over “Identity”:
    1. For on-premises environments that value might become unreliable in case of moving user objects around OUs, so result will be incomplete. Thanks to the cloud’s flat structure it’s not an issue.
    2. In case of using Classic command it works faster as it receives all the devices via one request rather than reaching each one individually. Again – important for on-premises.
    3. I’m not a big fan of manual values construction. I know, I know – we have to extract user DN anyway, but it’s more reliable approach.

    As a conclusion – EXO does make a significant difference, but is available for Exchange Online only. However, if we apply a pinch of knowledge and intersect it with a fraction of creativity, we can squeeze as much as possible, depending on circumstances:)…

    Reply
    1. Vasil Michev says:

      Lots to unpack here, and some concepts I’m not familiar with, so allow me some time to play with them. It sure looks like I’ll learn few tricks, so thanks for that 🙂

      The delay is just a precaution, a very crude anti-throttling control. It shouldn’t matter for the REST cmdlets. It was an issue with the old cmdlets, but I was too lazy to implement proper throttling handling (i.e. https://github.com/Canthv0/RobustCloudCommand)

      Reply
      1. Stanvy says:

        For sure – take your time and in case of any questions I would be happy to assist…

        Yeah, that’s exactly my point – with REST I see no need in that artificial delay and have never faced any issues so far.

        Reply
    2. Vasil Michev says:

      Pfft, while comparing the output between yours and mine, I noticed that the value of FirstSyncTime seems to be reported differently between Get-MobileDevice and Get-(ExO)MobileDeviceStatistics. This seems to be related to Outlook devices only, but it’s annoying nevertheless:

      PS C:\> Get-MobileDevice -Identity $dn3 | select FirstSyncTime,WhenCreatedUTC
      
      FirstSyncTime     WhenCreatedUTC   
      -------------     --------------   
      12/09/17 15:25:48 12/09/17 15:25:48
      
      PS C:\> Get-MobileDeviceStatistics -Identity $dn3 | select FirstSyncTime
      
      FirstSyncTime    
      -------------    
      24/07/18 15:21:22

      I’m inclined to trust the Get-MobileDevice, as it matches the device creation date.

      Reply
      1. Stanvy says:

        Actually, the only reason I use “Get-MobileDeviceStatistics” is to get last activity time to find out stale devices and clean them up. Otherwise, I would get rid of it completely, especially considering how incredibly slow it is.
        For my own needs I generate reports for both commands and do further pivoting via Power BI or Excel (which are essentially the same from this perspective).

        Reply
        1. Stanvy says:

          Again, speaking of the concept, that’s how I would address that:

          $hshUserDNs = @{}
          Get-MobileDevice -ResultSize "Unlimited" |
              ForEach-Object -Process {
                  $strUserDN = $_.DistinguishedName -replace '^(?:.+?(?<!\\),){2}(.+)$', '$1'
                  $strPrimarySMTPAddress = $hshUserDNs[$strUserDN]
                  if ($null -eq $strPrimarySMTPAddress)
                  {
                      $objRecipient = Get-Recipient -Identity $strUserDN
                      $strPrimarySMTPAddress = $objRecipient.PrimarySmtpAddress
                      $hshUserDNs.Add($strUserDN, $strPrimarySMTPAddress)
                  }
          
                  $_ |
                      Add-Member -NotePropertyName "PrimarySmtpAddress" -NotePropertyValue $strPrimarySMTPAddress -PassThru
              } | Export-Csv -Path "MobileDevices.csv" -NoTypeInformation
          
          
          $hshUserDNs.GetEnumerator() |
              ForEach-Object -Process {
                  $strPrimarySMTPAddress = $_.Value
                  Get-MobileDeviceStatistics -Mailbox $strPrimarySMTPAddress |
                      Add-Member -NotePropertyName "PrimarySmtpAddress" -NotePropertyValue $strPrimarySMTPAddress -PassThru
              } | Export-Csv -Path "MobileDeviceStatistics.csv" -NoTypeInformation

          On the first pass we collect all the primary addresses of mailboxes that have mobile devices, avoiding “Get-Recipient” call for those we already know, and on the second one we use that collection as an input for “Get-MobileDeviceStatistics”.

    3. Steve Poirier says:

      May I suggest adding something like this,

      [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;

      to force using TLS 1.2?

      Thank you.

      Reply
  2. Stanvy says:

    Just a test – please ignore…

    Get-MobileDevice -ResultSize “Unlimited” |
    ForEach-Object -Process {
    $strIdentity = $_.OrganizationId.Split(“-“)[0].TrimEnd() + “/”+ $_.Identity.Replace(“\”,”/”)
    Get-MobileDeviceStatistics -Identity $strIdentity
    } | Export-Csv -Path “MobileDeviceStatistics.csv” -NoTypeInformation

    Reply
  3. Stanvy says:

    You are totally right on that – one mailbox might have a number of mobile devices. I just purposely omitted that part for simplicity. I’m planning to wright a quite big comment where this specific will be shown…

    Reply
  4. Stanvy says:

    Hi Vasil,

    I did the tests and would like to share my results. However, before I do that, I’d appreciate if you can point me to some guidance on how to properly format PowerShell code in the comments. Also, it would be nice if you can edit my original comment’s sample to make it look nicer.

    Reply
  5. Stanvy says:

    Hi Vasil,

    I briefly looked at your code, so might have missed some key points or misunderstood the idea, however would like to share my feedback.

    Split “DistinguishedName” value by comma is a very bad idea in general due to comma can be a part of object name – for example:
    CN=LastName\, FirstName,OU=Container,DC=domain,DC=com

    Therefore, more reliable way to get user’s DN would be something like

    'CN=Hx§Outlook§1234567890ABCDEF1234567890ABCDEF,CN=ExchangeActiveSyncDevices,CN=LastName\, FirstName,OU=Container,DC=domain,DC=com' -replace '^(?:.+?(?<!\\),){2}(.+)$', '$1'

    Or as simple as:

    'CN=Hx§Outlook§1234567890ABCDEF1234567890ABCDEF,CN=ExchangeActiveSyncDevices,CN=LastName\, FirstName,OU=Container,DC=domain,DC=com' -replace '^.+?,CN=ExchangeActiveSyncDevices,(.+)$', '$1'

    I know that it's very unlikely for device name to contain comma, but sometimes we might face with very unusual things.

    Not sure why you've come up with combining "OrganizationId" and "Identity", but that approach seems to be arguable.

    From performance perspective it might makes sense to do caching, but in this exact case the slowest operation is "MobileDeviceStatistics", therefore I'd say that intermediate CSV file introduces unnecessary complexity and outdated results.

    Also I'd suggest to avoid storing results in a variable and perform classic looping or operations over them. On a big scale it might become very significant memory eater and performance killer.

    Saying all above, here is an option to achieve the result (hope formatting won't break:)):

    Get-MobileDevice -ResultSize 5 |
    ForEach-Object -Process {
    $strUserDN = $_.DistinguishedName -replace '^(?:.+?(?<!\\),){2}(.+)$', '$1'
    $objRecipient = Get-Recipient -Identity $strUserDN
    Get-MobileDeviceStatistics -Mailbox $objRecipient.PrimarySmtpAddress
    } | Export-Csv -Path "MobileDeviceStatistics.csv" -NoTypeInformation
    

    I intentionally ommited error handling, progress bar and other "decorations" just to demonstrate the concept.

    Reply
    1. Vasil Michev says:

      Thanks for the comments. I fully agree on the DistinguishedName remark, it’s a lazy solution. In all fairness I did check across some few thousand samples, none of which had a comma, so it looks acceptable. But that’s no excuse.

      As for “OrganizationId” and “Identity”, this seems to be the format used for the Identity property as returned by Get-(ExO)MobileDeviceStatistics.

      Get-MobileDevice -ResultSize 1 | fl DeviceId,Identity,OrganizationId
      
      DeviceId       : 748e5ccfbef372c110e9cf7cd053bb0a
      Identity       : vasil\ExchangeActiveSyncDevices\REST§Outlook§748e5ccfbef372c110e9cf7cd053bb0a
      OrganizationId : EURPR03A001.prod.outlook.com/Microsoft Exchange Hosted Organizations/michev.onmicrosoft.com - EURPR03A001.prod.outlook.com/ConfigurationUnits/michev.onmicrosoft.com/Configuration
      
      Get-EXOMobileDeviceStatistics -Mailbox vasil | fl Identity
      
      Identity : EURPR03A001.prod.outlook.com/Microsoft Exchange Hosted Organizations/michev.onmicrosoft.com/vasil/ExchangeActiveSyncDevices/REST§Outlook§748e5ccfbef372c110e9cf7cd053bb0a

      This “Identity” is actually the only value accepted by Get-ExOMobileDeviceStatistics, apart from the GUID, but the speed of execution between those is not even comparable. The whole idea was to leverage the Get-ExOMobileDeviceStatistics, which unfortunately doesn’t behave as 1:1 replacement of Get-MobileDeviceStatistics.

      Reply
      1. Stanvy says:

        Hi Vasil,

        Thank you for your response.

        In your example you use “Mailbox” parameter, which accepts primary email address as well as mailbox GUID. From performance perspective it seems to be similar to “Identity”, as far as I can tell. Therefore, the sample I provided above is equal to what can be done via “Get-EXOMobileDeviceStatistics”.

        I totally get the beauty of these cloud commands, but still prefer to use regular ones as it allows to get the result using the same code regardless of on-premises or cloud environment. Also, due to V3 module is REST-based, it eliminates some pain points in case of running against the cloud.

        However, I see that in my brief review of your code I overlooked that you don’t have this extra step of getting primary address and just manipulating “Identity” value received directly from mobile device.
        Not sure how significantly it impacts the overall speed of execution – need to do some testing on that.

        Reply
        1. Vasil Michev says:

          Right, but -Mailbox returns all mobile devices per user/mailbox, whereas -Identity returns a single device, based on the Identity of the device. The approach used in the script tries to avoid looping over each mailbox, as it’s expected that at least few of them will not have any mobile devices added. While I do use some of the properties exposed via Get-Mailbox, they’re all captured in a single call (when not importing the CSV), not per-mailbox, which certainly makes a difference in larger orgs.
          But again, that’s just “my” interpretation on how we can optimize a mobile device inventory script, I don’t claim for it to be the only possible method. And I’m certainly not a programmer, so I fully expect some code optimizations to be needed. Your comments are appreciated, thanks again for the time you’ve taken to analyze the script.

        2. Stanvy says:

          Hi Vasil,

          I did the tests and would like to share my results. However, before I do that, I’d appreciate if you can point me to some guidance on how to properly format the code in the comments. Also, it would be nice if you can edit my original comment’s sample to make it look nicer.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.