Januar 6, 2025

migrate-PublicFolders_onprem-only.ps1

# Author: Alexander Zarenko, https://blog.zarenko.net
#
# Provided as-is, feel free to change and redistribute
# If you do, please include a short notice mentioning me such as this.
#
# Use at you own risk!

[CmdletBinding()] 
Param(
    [string]$PublicFolderPath,
    [string]$Target,
    [switch]$DoNotcopyItems,
    [switch]$CopyOnly,
    [switch]$cutoverMailflow
) 
### load functions
. $PSScriptRoot\PFmig-functions_onprem-only.ps1

$startdate=get-date
Write-Host "`nStarting at $startdate...`n" -ForegroundColor Green
$adminSAM="$env:USERDOMAIN\$env:USERNAME"
"Running as [$adminSAM]"

### load ex shell
$snapin = Get-PSSnapin -Registered Microsoft.Exchange.Management.PowerShell.E* -ErrorAction 'SilentlyContinue'
if ($snapin -eq $null){Write-Warning "Snapin not available"}
else
{
	$command = Get-Command "Get-Mailbox" -ErrorAction SilentlyContinue
	if ($command -eq $null)
	{
		Write-Host "Loading Exchange Commands"
		Add-PSSnapin $snapin -ErrorAction 'SilentlyContinue'
		
	}
	else {Write-Host "Exchange Snapin is already loaded"}
}

$adminUPN=(Get-User $adminSAM).UserPrincipalName
Set-ADServerSettings -ViewEntireForest:$true

### get hierarchy
# Examples:
#$folderStruct = "\Public Folder 5\5.1\5.1.1\5.1.1.1"
#$folderStruct = "\Public Folder 3\Public Folder 3.2\Public Folder 3.2.1"

# show dialog, let user select the migration source folder
if(-not $PublicFolderPath){
    [pscustomobject]$PublicFolderPath = (Get-PublicFolder -Recurse | select Name,'Parentpath'| ogv -PassThru -Title "Select the source PF structure")
    if($PublicFolderPath.Name -eq "IPM_SUBTREE"){
        $PublicFolderFullPath = "\"
        Write-Host $PublicFolderFullPath -ForegroundColor Magenta
    }
    else{
        if($PublicFolderPath.ParentPath -ne "\"){
            $PublicFolderFullPath=$PublicFolderPath.ParentPath + "\" + $PublicFolderPath.Name
            Write-Host $PublicFolderFullPath -ForegroundColor Cyan
        }
        else{
            $PublicFolderFullPath="\" + $PublicFolderPath.Name
            Write-Host $PublicFolderFullPath -ForegroundColor Green
        }
    }
}
$folderStruct = $PublicFolderFullPath

# hard coded urls in case of problems, uncomment and change if needed
#$url="https://mail.domain.tld/EWS/Exchange.asmx"
#$autodiscourl="https://mail.domain.tld/autodiscover/autodiscover.svc"

#region TargetMailboxCreation
### target and Admin Mailboxes
# of a given folder structure, use the last piece 
# e.g. in case of "\Public Folder 1\Public Folder 1.4" use "Public Folder 1.4"
Write-Host "### Creating the target mailbox ###" -ForegroundColor Yellow
$targetMBX = $folderStruct.Split("\") | select -Last 1
# if there is no meaningful last piece, we assume that the PF Root was selected
if($targetMBX.Length -eq 0){$targetMBX="ROOT"}
# take care special characters to create a nice target mailbox name
$targetMBX = "PF_" + $targetMBX -ireplace 'ä', 'ae' -ireplace 'ö', 'oe' -ireplace 'ü', 'ue' -ireplace 'ß', 'ss' -ireplace '  ', ' ' -ireplace ' ', '' -ireplace '\\', '_'
# show a dialog and let the user select the destination OU for the target mailbox
$OU=Get-ADOrganizationalUnit -Filter * | select DistinguishedName,Name | ogv -PassThru -Title "Mailbox creation: Select OU for disabled user account"
$OU=$OU.DistinguishedName

# create the target mailbox if it does not yet exist
if (-not (get-mailbox $targetMBX -ErrorAction SilentlyContinue)){
    New-Mailbox $targetMBX -OrganizationalUnit $OU -Shared #-DisplayName $targetMBXdisplayname
    Start-Sleep 10
    do{
        Write-host "Mailbox creation started, checking progress in 10 seconds..."
        Start-Sleep 10
    }until($targetMBX=Get-Mailbox $targetMBX -ErrorAction SilentlyContinue)
    $targetMBX=Get-Mailbox $targetMBX -ErrorAction SilentlyContinue
    # Add full access permission to the current admin user
    $addFApermResult=Add-MailboxPermission $targetMBX -User $adminSAM -AccessRights fullaccess -AutoMapping:$false
    Write-Host( $addFApermResult | Out-String) -ForegroundColor Gray
}
Write-host "using [$targetMBX] as target mailbox"
#endregion TargetMailboxCreation

#region Preparation
$targetalias=(Get-Mailbox $targetMBX).Alias
$targetMBX=(Get-Mailbox $targetMBX).WindowsEmailAddress.Address
$adminMBX = (Get-Mailbox $adminSAM).WindowsEmailAddress.Address

### get a list of PFs beginning at the path of $folderStruct
$folders2process=Get-PublicFolder $folderStruct -Recurse | ?{$_.Parentpath}
### print the list to the screen
$folders2process | ft

### variable to contain all mail addresses of mail-enabled PFs in the $folderStruct
[System.Collections.ArrayList]$pfaddresses = @()

if(-not $credentials){$credentials = Get-Credential -Message "Enter onprem Admin credentials for EWS connection" -UserName "$env:USERDOMAIN\$env:USERNAME"}
$global:service = Connect-Exchange -MailboxName $adminMBX -Credentials $Credentials -url $url

### starting to process the relevant PFs
foreach($folder in $folders2process){
    $mailpf=$null
    ### concat the parent folder name with the child folder name. on the top level this results in a double \\ which is replaced with a single \ so these paths work correctly 
    $fullpath=(($folder.ParentPath + "\" + $folder.Name).Replace("\\","\"))
    ### take note of the folder class of the source PF to create an appropriate target folder 
    $class=$folder.FolderClass
    Write-Host "### Copying items ###" -ForegroundColor Yellow
    Write-Host "Processing [$fullpath] - FolderClass [$class]"
#endregion Preparation

#region FolderCreation
    $newfoldernamearray=($folder.ParentPath).Split("\")
    for($i=1;$i -lt $newfoldernamearray.Length;$i++){
        $newfolder=$newfoldernamearray[$i]
        $parentfolder=$newfoldernamearray[$i - 1]
        if($i -eq 1){
            #Create level 1 folder. 
            $parentpath=$null
            if($folder.ParentPath -eq "\"){$newfolder=$folder.Name}
            Write-Host "Creating folder [$newfolder]"
            Create-Folder -MailboxName $targetMBX -Credentials $credentials -NewFolderName $newfolder # without parent
            ### adding default.read for traversal
            # If the folder contains no data and is used only for traversal, everyone should have READ. Otherwise the permissions here are overwritten later by migrated folder permissions
            Set-MailboxFolderPermission "$targetalias`:\" -User "Default" -AccessRights Reviewer #-WhatIf            
        }
        else{
            #Create level 2 folder and onwards. The same as above applies in terms of traversal and permissions
            Write-Host "Creating folder [$newfolder] as a subfolder of [$parentfolder]";
            $parentpath=$parentpath+="\$parentfolder"
            Write-Verbose "This is the complete parent path used for folder creation: [$parentpath]";
            Create-Folder -MailboxName $targetMBX -Credentials $credentials -NewFolderName $newfolder -ParentFolder $parentpath #with parent [$i-1]
            ### adding default.read for traversal
            Set-MailboxFolderPermission "$targetalias`:$parentpath" -User "Default" -AccessRights Reviewer #-WhatIf            
        }
    }
#endregion FolderCreation

#region ContentMigration
    Write-Verbose "Read the contents of the public folder [$($folder.Name)]"
    Write-Verbose "Create [$($folder.Name)] in [$targetMBX] as a subfolder of [$($folder.ParentPath)]"
    Write-Verbose "Copy all contents to [$targetMBX`:$($folder.ParentPath)\$($folder.Name)]."

    # Get-PublicFolderItems is not only getting the items - it also copies them to the target Mailbox 
    # Debug output if parameter is given
    if($PSCmdlet.MyInvocation.BoundParameters["Debug"].IsPresent){
        Get-PublicFolderItems -MailboxName $adminMBX -Credentials $credentials -PublicFolderPath $fullpath -targetMBX $targetMBX -folderName $folder.Name -parentpath $folder.ParentPath -folderClass $class -url $url -autodiscourl $autodiscourl -Debug
    }
    # If not: just normal output
    else{
        Get-PublicFolderItems -MailboxName $adminMBX -Credentials $credentials -PublicFolderPath $fullpath -targetMBX $targetMBX -folderName $folder.Name -parentpath $folder.ParentPath -folderClass $class -url $url -autodiscourl $autodiscourl
    }
    Write-Host "Finished copying, moving on...`n"
#endregion ContentMigration

#region SMTPAddresses
    ### collecting mail addresses of mail-enabled PFs
    Write-Host "### Collecting mail addresses ###" -ForegroundColor Yellow
    Write-Host "Check if [$fullpath] is mail-enabled. If yes, get its mail address"
    try{
        $mailpf=Get-MailPublicFolder $fullpath -ErrorAction Stop
    }
    catch{
        Write-Host "`t$($_.Exception.Message)" -ForegroundColor DarkGray
    }
    if($mailpf){
        $strPFMailAddresses=[string]::join(";",($mailpf.EmailAddresses.AddressString))
        Write-Host -ForegroundColor Cyan "`tFolder [$fullpath] has these mail addresses: [$strPFMailAddresses]"
        foreach($smtp in $mailpf.EmailAddresses){
            $pfaddresses.Add("$($smtp.AddressString)") | Out-Null
            }
        $global:primarySMTP=$mailpf.WindowsEmailAddress.Address
    }
#endregion SMTPAddresses

#region SendAsPermissions
    ### Migrating SendAs permissions
    if($mailpf){
        Write-Host "### Reading and copying SendAs permissions ###" -ForegroundColor Yellow
        $perms=$mailpf | Get-ADPermission
        $sendAsPerms=$perms | ?{$_.ExtendedRights -like "Send-As"}        
        foreach($sendAsPerm in $sendAsPerms){
            $user=$sendAsPerm.User
            "Copying sendAs permission for [$user] to [$($targetalias)]"
            $mbx = get-Mailbox $targetMBX
            $mbx | Add-ADPermission -ExtendedRights Send-As -User $user | Out-Null #-WhatIf
            $user=$null
        }
        $perms,$sendAsPerms,$user=$null
    }
#endregion SendAsPermissions

#region FolderPermissions
    ### Migrating folder permissions
    Write-Host "### Migrating / Copying folder permissions ###" -ForegroundColor Yellow
    $folderperms=Get-PublicFolderClientPermission $fullPath
    foreach($folderperm in $folderperms){
        $user=$folderperm.User
        $accessrights=$folderperm.AccessRights
        # ToDo: add routine, for remotemailboxes : get-AdUser -Identity $_.Guid | Set-ADObject -Replace @{msExchRecipientDisplayType=-1073741818}
        # WHY again?
        if($user.DisplayName -eq "None" -or $user.DisplayName -eq "Default" -or $user.DisplayName -eq "Standard" -or $user.DisplayName -eq "Anonymous" -or $user.DisplayName -eq "Anonym" ){
            #"Setting folder permission for [$user] on [$fullpath] to migrated folder in [$targetMBX]"
            $ret=Set-MailboxFolderPermission "$targetalias`:$fullpath" -User $user -AccessRights $accessrights #-WhatIf
            $ret | Out-String -Verbose
            # everything commented out, do nothing
        }
        else{
            write-host "Migrating folder permission for [$user] on [$fullpath] to AccessRight [FullAccess] on [$targetMBX]"
            try{
                #Add-MailboxFolderPermission "$targetalias`:$fullpath" -User $user -AccessRights $accessrights -ErrorAction Stop #-WhatIf
                if($copyonly){
                    $res=Add-MailboxPermission $targetalias -AccessRights FullAccess -User $user.ADRecipient.WindowsEmailAddress.Address -WhatIf
                }
                else{
                    $res=Add-MailboxPermission $targetalias -AccessRights FullAccess -User $user.ADRecipient.WindowsEmailAddress.Address
                }
                $res | Out-String -Verbose
            }
            catch{
                Write-Warning $Error[0]            
            }            
        }
    }
    if($additionaladminSAM){
        ### add exchange admin with owner rights to every migrated folder as well
        "Adding folder permission for [$additionaladminSAM] on [$fullpath] to migrated folder in [$targetMBX]"
        try{
            Add-MailboxFolderPermission "$targetalias`:$fullpath" -User $additionaladminSAM -AccessRights $accessrights -ErrorAction Stop #-WhatIf
        }
        catch{
            Write-Warning $Error[0] 
        }
        $user,$accessrights=$null
    }
    $folderperms=$null
#endregion FolderPermissions

#region MailFlowCutover
    # make sure that the target mbx is there and ready
    $mbx=get-mailbox $targetMBX
    if($mailpf -and $mbx){
        ### Disabling mail-enabled PFs
        Write-Host "### Disabling mail-enabled PFs ###" -ForegroundColor Yellow
        if($cutoverMailflow){            
            Write-Host -ForegroundColor Cyan "Disabling mail public folder [$fullpath] now..."
            Disable-MailPublicFolder $mailpf -Confirm:$false
            Write-host "checking progress in 10 seconds..."
            Start-Sleep 10
            try{
                $mailpf=Get-MailPublicFolder $fullpath -ErrorAction stop
            }
            catch{
                Write-Warning $Error[0]    
            }
            if(-not $mailpf){Write-Host -ForegroundColor Cyan "[$fullpath] has been disabled successfully."}
        }
        else{
            Write-Host -ForegroundColor Cyan "WHATIF: Disabling mail public folder [$fullpath] now..."
            Disable-MailPublicFolder $mailpf -WhatIf
        }
    }    
}

### migrating mail addresses of the disabled PFs to the target mailbox
if($cutoverMailflow){ 
    Write-Host "### Assigning mail addresses of the disabled PFs to the target mailbox ###" -ForegroundColor Yellow
    Write-Host "Waiting 1 minute before proceeding..."
    if($pfaddresses){
        Write-Host "`tSetting Emailaddresses of [$targetMBX] to Emailaddresses of the mail-enabled PFs [[string]$pfaddresses]..."
        $mbx | Set-Mailbox -EmailAddresses $pfaddresses -EmailAddressPolicyEnabled $false
        Write-Host "`tSetting WindowsEmailAddress of [$targetMBX] to that of the previously mail-enabled PF [-> $global:primarySMTP]..."
        $mbx | Set-Mailbox -WindowsEmailAddress $global:primarySMTP
    }
}
else{
    if($pfaddresses){
        Write-Host "WHAT IF: `tSetting Emailaddresses of [$targetMBX] to Emailaddresses of the mail-enabled PFs [[string]$pfaddresses]..." -ForegroundColor Cyan
        Set-Mailbox $targetMBX -EmailAddresses $pfaddresses -EmailAddressPolicyEnabled $false -WhatIf
    }
}
#endregion MailFlowCutover

$enddate=get-date
$timediff=$enddate-$startdate
Write-Host "Finished. This took $($timediff.Hours) hours, $($timediff.Minutes) minutes and $($timediff.Seconds) seconds." -ForegroundColor Green