Convert mailbox folder identifiers non-interactively

A while back, we released a PowerShell script to convert mailbox folder identifiers across the various formats used by various clients and services across Microsoft 365. The script is intended to be used by Exchange Online administrator or anyone with access to the Get-MailboxFolderStatistics cmdlet. It takes the output of said cmdlet, which returns the folderId (aka storeId) value, and transforms it to the format used by eDiscovery targeted collection, the MAPI PR_ENTRYID identifier, the OWAId and the RESTId used by the Graph API.

As the Get-MailboxFolderStatistics cmdlet allows you to enumerate folders in any mailbox, including system mailboxes, and the translateExchangeIds Graph API method needs only the User.ReadBasic.All permission for the conversion, the script has a very low footprint, permission-wise. One downside is that it runs in the context of a user, via delegate permissions, and thus cannot be used in automation scenarios (non-interactively).

Since all the relevant bits and pieces are supported also in the application permissions model, we can address said downside. In fact, we can go a step further and get rid of the PowerShell module dependencies and leverage raw HTTPS requests for the Graph calls, as well as the InvokeCommand method for any Exchange Online bits. Authentication can also be handled direct, albeit we wouldn’t recommend using a client secret for any production tasks. Without further ado, here is the non-interactive version of the script.

Converting folder identifiers non-interactively

First things first, make sure to download the script from GitHub. As the script is using the client credentials flow, specifically a client secret, you need to fill in the corresponding authentication-related variables (line 273-275). Or better yet, replace the auth block and the Renew-Token function with your preferred method to non-interactive scenarios.

You will also need to make sure the service principal used has sufficient permissions. On Exchange Online’s side of things, it needs the View-Only Recipients role or equivalent. If assigning an Entra ID role the Global Reader one should do, though it is a bit more privileged than needed. For the Graph calls, User.ReadBasic.All is sufficient.

The script has two parameters, as follows:

  • Mailbox – the identity of the mailbox for which we want to generate the report of folder identifiers. Mandatory. Aliased as –Identity. Accepts any valid Exchange Online identifier.
  • IncludeNonIPM – switch parameter, used to include the entire Non-IPM subtree. Default value is $false.

The script logic is fairly basic. It will first try to obtain access tokens for both Exchange Online and the Graph API. Next step is to parse the input value and ensure we have a valid identifier that works with both the ExO’s REST API and the Graph API. This is followed by a call to the Get-MailboxFolderStatistics cmdlet in order to obtain a list of folders within the mailbox. We translate the folderId/storeId value to eDiscoveryId and EntryId, and then call the Graph API for the final transformation to RestId. The output is then saved to a CSV file and an HTML one.

To run the script, use one of the examples below:

#Run the script against the user@domain.com mailbox
.\Mailbox_Folder_IDs_API.ps1' -Mailbox user@domain.com

#Alternatively, you can specify ExternalDirectoryObjectId value as input
.\Mailbox_Folder_IDs_API.ps1' -Mailbox 4ebd5057-4d61-4ca0-beb7-df3f1ebd1aa7

#Generate IDs for folders in the non-IMP tree
.\Mailbox_Folder_IDs_API.ps1' -Mailbox username -IncludeNonIPM

translateIDs APIIf no issues are found during execution, the script will generate output to a CSV file within the working directory, containing the following columns:

  • Name – the name of the folder.
  • FolderType – the folder type.
  • Identity – the folder “path”. Keep in mind the mailbox will be designated by the identifier you provided as input, thus you can expect to see GUID’s here, if you provided such.
  • FolderId – the storeId value of the folder.
  • eDiscoveryId – the value you can use for eDiscovery targeted collection.
  • EntryId – the MAPI id.
  • RestId – the id used by the Graph API.

In addition. basic HTML file is generated with the same data packaged in a sortable table. One small addition is added to the HTML: a button that opens the Graph explorer tool with a query to get the folder in question. Keep in mind that the delegate permissions used by the Graph explorer might not be sufficient to get you the folder details.

translateIDs API1

Using only the Graph API

One argument we used to justify the need for non-interactive version of the script, is that it allows us to get rid of PowerShell module dependencies. Yet said version still relies on Exchange Online to validate the input value and most importantly, fetch a list of all folders within the mailbox. So the natural question here is, can we simply use the Graph API for all of this?

The answer is not that simple. Due to design oversight, the standard mailFolder Graph API methods only cover mail folders, as in no Calendar, Contact and other folder types. For the set of folders it does support, any subfolders can only be retrieved on the first level (i.e. “Inbox\aaa”, but not “Inbox\aaa\bbb”), requiring additional queries. Peeking into the non-IPM subtree is another point where the Graph API is no up to the task, and so is covering the Online archive. In a nutshell, when it comes to the scenario of enumerating all folders within a mailbox, the Graph API vastly inferior to the Get-MailboxFolderStatistics cmdlet.

Things do get a bit better with the mailbox import and export API, and if you are willing to put the extra effort, you can use it to create an “acceptable” solution, albeit still inferior to Get-MailboxFolderStatistics. Which is exactly what we will cover for the remainder of the article. And while we are at it, we’ll also outline some of the shortcomings of this approach, just to have it all in one place.

As before, we want an automated, non-interactive script, so application permissions are the natural choice. In particular, we will need User.Read.All to fetch the mailbox identifier (more about this in a second), which is also sufficient for calling the translateExchangeIds method. To enumerate all the folders within a given mailbox, MailboxFolder.Read.All is needed.

Do remember that application permissions are tenant-wide when assigned via the Entra portal or the Graph methods. If you want to restrict access to specific mailboxes, consider using Exchange Online’s RBAC for applications functionality. The old application access policies still work, too.

The first thing the script does is to obtain an Access token via the Client credentials flow, which requires you to configure the the corresponding auth variables on line 370-372 (after downloading the script, of course). As always, consider replacing the built-in Renew-Token function with your preferred method to handle authentication, and please do NOT hardcode any secrets as part of the script!

Next, we need an identifier for the mailbox. Unlike the Exchange-based scripts, we cannot use values such as Name, Alias or any SMTP addresses (well, apart any matching the UPN). The Graph methods will work with either the user’s ID/GUID, the user’s UserPrincipalName and the mailbox identifier as obtained via the List Exchange settings method. As an example:

  • ID: cb38f772-a77e-4ad2-a45c-efb7bd0a175e
  • UPN: shared@domain.com
  • primaryMailboxId: MBX:ec691c50-a2d4-4ebd-b2a6-a365121df0bc@923712ba-3a21-2bda-bece-09d0684d0cfb

For the sake of completeness the script includes a simple helper function (GetMailbox) to “resolve” the provided identifier to the primaryMailboxId value, although this is technically not needed. Still, if you ever want to cover folders within Online archive mailboxes, this is the way to go.

Obtaining the list of folders within the mailbox is what the main part of the code is dedicated to, and where most differences with the Exchange-based script(s) stem from. Not only the Graph methods require additional calls to cover subfolders (and a recursive function to traverse the full hierarchy), but they also handle system folders differently (if at all). Direct comparison with the output of Get-MailboxFolderStatistics will always result in disappointment.

As an example, folders within the RecoverableItems subtree are not returned by default, though they can still be retrieved by adjusting the root selection. Folders such as Teamchat, Quick Step Settings or Conversation Action Settings are missing from the output, regardless of the query used. Things get even more unreliable when working with the non-IPM subtree, with not only the includeHiddenFolders=true flag making no difference, but also the output happily misleading you. Sadly, this is not limited to the mailbox export and import endpoints either:

translateIDs API2

At the end of the day, it is what it is and we only cover the set of folders returned. To generate their corresponding identifiers we take the “opposite” approach to the Exchange-based scripts, starting from the RestId, as returned by the Graph methods. We use the translateExchangeIds method to obtain the matching EntryId, then work our way back to restoring the FolderId. Another problem arises at this point, as the last char of the folderId value is calculated based on the folder type/class. As you can probably guess by now, the Graph methods are not reliable on this point either, and sometimes do not return the proper value, or return no value at all.

The last bit of code handles the eDiscoveryId value and once we generate it, output is written to both CSV and HTML files as with the Exchange version of the script. As the output should match between the versions (sans the aforementioned missing folders and potentially incorrect last char of the FolderId value), there is no point adding more screenshots here. Refer to the previous section should you need such.

This version of the script supports the same parameters as the Exchange -based one, namely -Mailbox to designate mailbox for which to generate folder identifier values and optionally, -IncludeNonIPM to cover all folders in the non-IPM subtree. Here are some examples on how to run the script:

#Run the script against the user@domain.com mailbox
.\Mailbox_Folder_IDs_GraphAPI.ps1' -Mailbox user@domain.com

#Alternatively, you can specify ExternalDirectoryObjectId value as input
.\Mailbox_Folder_IDs_GraphAPI.ps1' -Mailbox 4ebd5057-4d61-4ca0-beb7-df3f1ebd1aa7 

#Generate IDs for folders in the non-IMP tree
.\Mailbox_Folder_IDs_GraphAPI.ps1' -Mailbox username -IncludeNonIPM

Summary

In summary, we presented two PowerShell scripts that can be used to bulk generate folder identifiers for Exchange Online. A version based on the Graph API, with no dependences on anything on Exchange side, but with some downsides, as well as an Exchange-based version that offers a bit more flexibility. Both versions can be run non-interactively and need only read-only permissions.

As is often the case, the Graph-based version has some limitations and in most scenarios will give you an incomplete picture, your mileage will vary. Apart from missing folders, another issue you might run into with the Graph version is a wrong value for the folderId/storeId identifier, which last bit needs to be calculated based on the folder class/type. Then again, should you need said identifiers, you should be using the Get-MailboxFolderStatistics cmdlet instead of workarounds 🙂

Addendum

As mentioned in the text above, when restoring the FolderId value we need to calculate the last char based on the folder class or type. This is handles as part of the EntryIdToFolderId helper function, here are the relevant bits:

# determine the suffix based on the folder type
    $suffix = switch -Wildcard ($FolderType) {
        'IPF.Note*' { "01" }
        'IPF.Appointment*' { "02" }
        'IPF.Contact*' { "03" }
        'IPF.Task*' { "04" }
        'IPF.StickyNote*' { "05" }
        'IPF.Journal*' { "06" }
        #'IPF.Note' { "07" } #SearchDiscoveryHoldsFolder, SearchDiscoveryHoldsUnindexedItemFolder, AllCategorizedItems, AllContacts, AllItems, AllTodoTasks...
        default { "01" }
    }

As hinted by the commented line, there are a number of folders that claim to be of the IPF.Note type but have corresponding FolderId values ending in “H”. Here’s a list of matching folders within my mailbox, probably an incomplete one:

AllCategorizedItems
AllContacts
AllContactsExtended
AllItems
AllPersonMetadata
AllTaggedItems
AllTodoTasks
MomentsRecordData
Calendar Version Store
Document Centric Conversations
Favorites
NoArchiveTagSearchFolder8534F96D-4183-41fb-8A05-9B7112AE2100
OwaFV15.1AllFocusedAQMkAGU2MWM5NzU0LWY3MmQtNGI3OS1hNDVlLTBkMzI3OWVlADViM2YALgAAA6EpIkCGWdRMge0pKyjfROEBAEg98MzHI/9FiFjwzzGYA9UAAAIBDQAAAA==
OwaFV15.1AllOtherAQMkAGU2MWM5NzU0LWY3MmQtNGI3OS1hNDVlLTBkMzI3OWVlADViM2YALgAAA6EpIkCGWdRMge0pKyjfROEBAEg98MzHI/9FiFjwzzGYA9UAAAIBDQAAAA==
OwaFV15.1UnreadAAMkAGU2MWM5NzU0LWY3MmQtNGI3OS1hNDVlLTBkMzI3OWVlNWIzZgAuAAAAAAChKSJAhlnUTIHtKSso30ThAQBIPfDMxyP/RYhY8M8xmAPVAADKWXNoAAA=
GraphFilesAndWorkingSetSearchFolder
LyncConversationLogs
LyncMissedConversationLogs
My Contacts
MyAnalytics-RequestsFolder
MyAnalytics-UnreadFromIRankerFolder
MyAnalytics-UnreadFromVIPFolder
MyContactsExtended
People I Know
RelevantContacts
Reminders
SharedFilesSearchFolder
Spooler Queue
SpoolsPresentSharedItemsSearchFolder
SpoolsSearchFolder
To-Do Search
Tracked Mail Processing
Unified Inbox
UserCuratedContacts
XrmActivityStreamSearch
XrmCompanySearch
XrmDealSearch
XrmSearch

 

 

Leave a Reply

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

Discover more from Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading