Created
October 15, 2025 04:01
-
-
Save bill-long/8ad649d7c1f5de8b8250522bc6356ab3 to your computer and use it in GitHub Desktop.
For exporting and importing PF permissions. See notes in the comment at the bottom.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| MIT License | |
| Copyright (c) Microsoft Corporation. | |
| Permission is hereby granted, free of charge, to any person obtaining a copy | |
| of this software and associated documentation files (the "Software"), to deal | |
| in the Software without restriction, including without limitation the rights | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| copies of the Software, and to permit persons to whom the Software is | |
| furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in all | |
| copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| SOFTWARE | |
| #> | |
| # Version 25.10.15.0349 | |
| #Requires -Version 5.1 | |
| #Requires -Modules ExchangeOnlineManagement | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory = $true, ParameterSetName = 'Export')] | |
| [switch] | |
| $Export, | |
| [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = 'Export')] | |
| [object[]] | |
| $Folder, | |
| [Parameter(Mandatory = $false, ParameterSetName = 'Export')] | |
| [string] | |
| $Mailbox, | |
| [Parameter(Mandatory = $true, ParameterSetName = 'Import')] | |
| [switch] | |
| $Import, | |
| [Parameter(Mandatory = $false, ParameterSetName = 'Import')] | |
| [switch] | |
| $ReplaceExisting, | |
| [Parameter(Mandatory = $false, ParameterSetName = 'Export')] | |
| [Parameter(Mandatory = $true, ParameterSetName = 'Import')] | |
| [string] | |
| $File | |
| ) | |
| begin { | |
| $foldersToProcess = [System.Collections.ArrayList]::new() | |
| function GetUserStringFromUser($User) { | |
| if ($user.UserType -eq 'Default') { | |
| return 'Default' | |
| } elseif ($user.UserType -eq 'Anonymous') { | |
| return 'Anonymous' | |
| } else { | |
| return $user.RecipientPrincipal | |
| } | |
| } | |
| function GetMatchingPermission($CurrentPermissions, $ImportedPermission) { | |
| if ($importedPermission.UserType -eq 'Default' -or $importedPermission.UserType -eq 'Anonymous') { | |
| return $currentPermissions | Where-Object { $_.User.UserType.Value -eq $importedPermission.UserType } | |
| } else { | |
| return $currentPermissions | Where-Object { $_.User.RecipientPrincipal.Guid -eq $importedPermission.RecipientPrincipal } | |
| } | |
| } | |
| } | |
| process { | |
| if ($PSCmdlet.ParameterSetName -eq 'Export' -and $null -ne $Folder) { | |
| $foldersToProcess.AddRange($Folder) | Out-Null | |
| } | |
| } | |
| end { | |
| if ($Export) { | |
| if (-not $File) { | |
| $timestamp = Get-Date -Format "yyMMdd-HHmm" | |
| $File = "PublicFolderPermissions_$timestamp.csv" | |
| } | |
| if (Test-Path -Path $File) { | |
| Write-Error "The specified file '$File' already exists. Please specify a different file name or delete the existing file." | |
| exit 1 | |
| } | |
| if (-not $Mailbox) { | |
| Write-Host "No mailbox specified. Using the root public folder mailbox." | |
| $Mailbox = Get-Mailbox -PublicFolder (Get-OrganizationConfig -ErrorAction Stop).RootPublicFolderMailbox -ErrorAction Stop | |
| } else { | |
| $Mailbox = Get-Mailbox -PublicFolder -Identity $Mailbox -ErrorAction Stop | |
| } | |
| if ($foldersToProcess.Count -lt 1) { | |
| Write-Host "Retrieving all public folders..." | |
| $foldersToProcess = Get-PublicFolder -Recurse -ResultSize Unlimited | Select-Object -Skip 1 | |
| Write-Host "Retrieved $($foldersToProcess.Count) public folders." | |
| } else { | |
| Write-Host "Exporting permissions for $($foldersToProcess.Count) specified public folders." | |
| } | |
| Write-Host "Exporting public folder permissions to $File." | |
| Write-Host "Retrieving permissions..." | |
| $permissionsList = @() | |
| $progressCount = 0 | |
| foreach ($folder in $foldersToProcess) { | |
| $progressCount++ | |
| Write-Progress -Activity "Processing public folders" -Status "Folder $progressCount of $($foldersToProcess.Count)" -PercentComplete (($progressCount / $foldersToProcess.Count) * 100) | |
| if ($folder.Identity -eq "\" -or $folder.Identity -eq "\non_ipm_subtree") { | |
| continue | |
| } | |
| $permissions = Get-PublicFolderClientPermission -Identity $folder.Identity -Mailbox $mailbox | |
| foreach ($perm in $permissions) { | |
| $permissionsList += [PSCustomObject]@{ | |
| FolderPath = $folder.Identity | |
| ContentMailboxName = $folder.ContentMailboxName | |
| ExportedFrom = $Mailbox.ToString() | |
| DisplayName = $perm.User.DisplayName | |
| RecipientPrincipal = $perm.User.RecipientPrincipal.Guid | |
| UserType = $perm.User.UserType | |
| AccessRights = ($perm.AccessRights -join ';') | |
| } | |
| } | |
| } | |
| Write-Host "Writing CSV file..." | |
| $permissionsList | Export-Csv -Path $File -NoTypeInformation -Encoding UTF8 | |
| Write-Host "Export completed." | |
| } | |
| if ($Import) { | |
| if (-not (Test-Path -Path $File)) { | |
| Write-Error "The specified file '$File' does not exist." | |
| exit 1 | |
| } | |
| Write-Host "Importing public folder permissions from $File." | |
| $permissionsList = Import-Csv -Path $File | |
| $totalPermissions = $permissionsList.Count | |
| $progressCount = 0 | |
| $currentFolderIdentity = $null | |
| $currentFolderPermissions = @() | |
| foreach ($perm in $permissionsList) { | |
| $progressCount++ | |
| Write-Progress -Activity "Applying public folder permissions" -Status "Permission $progressCount of $totalPermissions" -PercentComplete (($progressCount / $totalPermissions) * 100) | |
| try { | |
| if ($currentFolderIdentity -ne $perm.FolderPath) { | |
| try { | |
| $currentFolderPermissions = Get-PublicFolderClientPermission -Identity $perm.FolderPath | |
| $currentFolderIdentity = $perm.FolderPath | |
| } catch { | |
| Write-Warning "Could not retrieve permissions for folder '$($perm.FolderPath)'. Error:`n$_" | |
| } | |
| } | |
| if ($null -eq $currentFolderPermissions -or $currentFolderPermissions.Count -lt 1) { | |
| Write-Warning "Public folder '$($perm.FolderPath)' permissions could not be retrieved. Skipping." | |
| continue | |
| } | |
| $existingPermission = GetMatchingPermission -CurrentPermissions $currentFolderPermissions -ImportedPermission $perm | |
| if ($null -ne $existingPermission) { | |
| if (-not $ReplaceExisting) { | |
| Write-Host "Permission for user '$($existingPermission.User.DisplayName)' on folder '$($perm.FolderPath)' already exists. Skipping." | |
| continue | |
| } | |
| if ($existingPermission.AccessRights -join ';' -eq $perm.AccessRights) { | |
| Write-Host "Permission for user '$($existingPermission.User.DisplayName)' on folder '$($perm.FolderPath)' is already correct. Skipping." | |
| continue | |
| } else { | |
| Write-Host "Permission for user '$($existingPermission.User.DisplayName)' on folder '$($perm.FolderPath)' exists but differs. Removing." | |
| Remove-PublicFolderClientPermission -Identity $perm.FolderPath -User (GetUserStringFromUser $perm) -Confirm:$false | |
| } | |
| } | |
| Write-Host "Adding permission for user '$($perm.DisplayName)' on folder '$($perm.FolderPath)'." | |
| Add-PublicFolderClientPermission -Identity $perm.FolderPath -User (GetUserStringFromUser $perm) -AccessRights ($perm.AccessRights -split ';') -Confirm:$false | |
| } catch { | |
| Write-Warning "Failed to import permissions for user '$($perm.DisplayName)' on folder '$($perm.FolderPath)': $_" | |
| } | |
| } | |
| Write-Host "Import completed." | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Scenario: Content mailboxes show client permissions but the root PF mailbox is somehow missing them, despite the mailboxes thinking that the hierarchy is in sync.
Mitigation steps:
This creates a separate export file for each content mailbox. The output should look something like this:
You should then see the resulting CSV files in the folder:
Then you can turn around and import these files to add these permissions to the root mailbox. The syntax is:
Note this is an additive operation only by default. The script will not remove any existing permissions, and will only add permissions for users not already present. For example, if I simply turn around and import what I just exported in my test tenant where there is no problem, it simply skips everything:
Optionally, you can add the -ReplaceExisting switch. In this case, if the user is present but has different permissions than the import, the script will remove the permissions for that one user, and then add what is in the import. For example, if UserOne currently shows as Author, but the import file says Owner, the script will not make any changes unless you add -ReplaceExisting. That switch will cause it to evaluate if the permissions match:
This is the only scenario where the script will remove a permission. It will only do so when -ReplaceExisting is specified so it can change existing permissions to match the import file.