<# Orchestration script for Public Folder migration (on-prem, WIA) Refactor: 2026-01 Requires: PSmig-Functions.Refactored.ps1 in same directory Notes: - PS 5.1 compatible, supports -WhatIf / -Confirm - OGV for PF/OU selection retained - TLS validation bypass enabled (like original) #> [CmdletBinding(SupportsShouldProcess=$true)] param( [string]$PublicFolderPath, [string]$OUforMBX, [string]$TargetMailboxName, [switch]$DoNotCopyItems, [switch]$CopyOnly, [switch]$CutoverMailflow, [ValidateSet('FullMailbox','FolderACL')][string]$PermissionMode = 'FullMailbox', [string]$Url, [string]$AutodiscoverUrl, [string]$AdditionalAdminSam ) # Load functions . "$PSScriptRoot\PFmig-Functions.Refactored.ps1" cd $PSScriptRoot $start = Get-Date $log = Join-Path $PSScriptRoot ("PFMig_{0:yyyyMMdd_HHmmss}.log" -f $start) Start-Transcript -Path $log -Append | Out-Null try { Write-Host "`nStarting at $start...`n" -ForegroundColor Green # Prepare Exchange cmdlets (snapin if needed) $snapin = Get-PSSnapin -Registered Microsoft.Exchange.Management.PowerShell.E* -ErrorAction SilentlyContinue if ($snapin -eq $null) { Write-Warning "Snapin not available" } else { $cmd = Get-Command "Get-Mailbox" -ErrorAction SilentlyContinue if ($cmd -eq $null) { Write-Host "Loading Exchange Commands" Add-PSSnapin $snapin -ErrorAction SilentlyContinue } else { Write-Host "Exchange Snapin is already loaded" } } Set-ADServerSettings -ViewEntireForest:$true $adminSam = "$env:USERDOMAIN\$env:USERNAME" $adminUser = Get-User -Identity $adminSam $adminUPN = $adminUser.UserPrincipalName "Running as [$adminSam]" # informational # Get PF path (OGV retained, robust) if (-not $PublicFolderPath) { # Liste vollständig materialisieren (verhindert Timing-/Lazy-Probleme) $pfList = @( Get-PublicFolder -Recurse | Where-Object { $_.ParentPath } | Select-Object Name, ParentPath ) # Auswahl EXPLIZIT übernehmen $selection = $pfList | Out-GridView -PassThru -Title "Select the source PF structure" if (-not $selection) { Write-Host "Abgebrochen."; return } # Wenn mehrere gewählt wurden → ersten verwenden if ($selection -is [System.Array]) { $selection = $selection | Select-Object -First 1 } # Pfad bestimmen if ($selection.Name -eq "IPM_SUBTREE") { $PublicFolderPath = "\" } else { if ($selection.ParentPath -ne "\") { $PublicFolderPath = ($selection.ParentPath + "\" + $selection.Name) } else { $PublicFolderPath = "\" + $selection.Name } } Write-Host $PublicFolderPath -ForegroundColor Cyan } # Derive/create target mailbox if (-not $TargetMailboxName) { $last = ($PublicFolderPath.Split("\") | Select-Object -Last 1) if ([string]::IsNullOrWhiteSpace($last)) { $last = "ROOT" } $TargetMailboxName = ("PF_{0}" -f $last) ` -ireplace 'ä','ae' -ireplace 'ö','oe' -ireplace 'ü','ue' -ireplace 'ß','ss' ` -ireplace ' ', '' -ireplace '\\','_' } Write-Host "### Creating/Checking the target mailbox ###" -ForegroundColor Yellow $mbx = Get-Mailbox -Identity $TargetMailboxName -ErrorAction SilentlyContinue if (-not $mbx) { if(-not ($OUforMBX)){ $ou = Get-ADOrganizationalUnit -Filter * | Select-Object DistinguishedName,Name | Out-GridView -PassThru -Title "Mailbox creation: Select OU for disabled user account" if (-not $ou) { Write-Host "Abgebrochen."; return } $ouDn = $ou.DistinguishedName } else{ $ouDn = $OUforMBX } if ($PSCmdlet.ShouldProcess(("Mailbox '{0}'" -f $TargetMailboxName),("Create in OU '{0}'" -f $ouDn))) { #New-Mailbox -Name $TargetMailboxName -OrganizationalUnit $ouDn -Shared -Database "Postfachspeicher01_MGH" | Out-Null New-Mailbox -Name $TargetMailboxName -OrganizationalUnit $ouDn -Shared | Out-Null } # wait until resolvable do { Write-Host "Mailbox creation started, checking progress in 10 seconds..." Start-Sleep 10 $mbx = Get-Mailbox -Identity $TargetMailboxName -ErrorAction SilentlyContinue } while (-not $mbx) # admin FullAccess (no automapping) if ($PSCmdlet.ShouldProcess(("Mailbox '{0}'" -f $TargetMailboxName),"Grant FullAccess to current admin")) { Add-MailboxPermission -Identity $TargetMailboxName -User $adminSam -AccessRights FullAccess -AutoMapping:$false | Out-Null } } else { Write-Host "Using existing mailbox [$TargetMailboxName]" -ForegroundColor DarkGray } $targetAlias = $mbx.Alias $targetSmtp = $mbx.WindowsEmailAddress.Address $adminMbxSmtp = (Get-Mailbox -Identity $adminSam).WindowsEmailAddress.Address # PF folders to process $folders = Get-PublicFolder $PublicFolderPath -Recurse | Where-Object { $_.ParentPath } $folders | Format-Table # EWS connect $service = Connect-Exchange -MailboxName $adminMbxSmtp -Url $Url # Collect pf addresses during loop [System.Collections.ArrayList]$pfAddresses = @() $primarySmtp = $null foreach ($f in $folders) { $fullPath = (($f.ParentPath + "\" + $f.Name).Replace("\\","\")) # normalized $class = $f.FolderClass Write-Host "### Copying items ###" -ForegroundColor Yellow Write-Host ("Processing [{0}] - FolderClass [{1}]" -f $fullPath,$class) # Ensure parent chain exists (and traversal permission for Default) $parentPath = $f.ParentPath if ($parentPath -and $parentPath -ne '\') { # Segmente sicher erzeugen UND als Array materialisieren # Beispiel: '\Folder1\Folder1.2' -> @('Folder1','Folder1.2') $segments = @($parentPath.Trim('\').Split('\') | Where-Object { $_ -ne '' }) $parentAgg = $null # kumulierter Pfad: '\Folder1', dann '\Folder1\Folder1.2' $parentForCreate = '\' # Start-Eltern ist der Root foreach ($seg in $segments) { # 1) Elternordner anlegen (existenzsicher) Create-Folder -MailboxName $targetSmtp -Service $service -NewFolderName $seg -ParentFolder $parentForCreate # 2) Traversal (Default = Reviewer) auf dem aktuellen Parent sicherstellen $pathForCmd = ('{0}:{1}' -f $targetAlias, $parentForCreate) if ($PSCmdlet.ShouldProcess(("Folder '{0}'" -f $pathForCmd), "Ensure Default Reviewer (traversal)")) { try { Set-MailboxFolderPermission -Identity $pathForCmd -User "Default" -AccessRights Reviewer -ErrorAction Stop | Out-Null } catch { # bereits gesetzt / nicht anwendbar -> ignorieren } } # 3) Aggregierten Parentpfad fortschreiben: neuer Parent ist der eben erstellte Ordner if ([string]::IsNullOrEmpty($parentAgg)) { $parentAgg = "\" + $seg } else { $parentAgg = $parentAgg + "\" + $seg } $parentForCreate = $parentAgg } } else { # Top-Level-Fall: keine Elternkette, nur Traversal auf Root (harmlos wenn schon gesetzt) $pathForCmd = ('{0}:{1}' -f $targetAlias, '\') if ($PSCmdlet.ShouldProcess(("Folder '{0}'" -f $pathForCmd), "Ensure Default Reviewer (root traversal)")) { try { Set-MailboxFolderPermission -Identity $pathForCmd -User "Default" -AccessRights Reviewer -ErrorAction Stop | Out-Null } catch { } } } # Copy content (or just ensure folder exists) Get-PublicFolderItems -Service $service ` -AdminMailboxSmtp $adminMbxSmtp ` -PublicFolderPath $fullPath ` -TargetMailboxSmtp $targetSmtp ` -ParentPath $f.ParentPath ` -FolderName $f.Name ` -FolderClass $class ` -AutodiscoverUrl $AutodiscoverUrl ` -DoNotCopyItems:$DoNotCopyItems # Mail-enabled PF? Write-Host "### Collecting mail addresses ###" -ForegroundColor Yellow try { $mailpf = Get-MailPublicFolder -Identity $fullPath -ErrorAction Stop } catch { $mailpf = $null Write-Host "`t$($_.Exception.Message)" -ForegroundColor DarkGray } if ($mailpf) { $asStrings = [string]::Join(';', ($mailpf.EmailAddresses.AddressString)) Write-Host ("`tFolder [{0}] has these mail addresses: [{1}]" -f $fullPath,$asStrings) -ForegroundColor Cyan foreach ($smtp in $mailpf.EmailAddresses) { [void]$pfAddresses.Add($smtp.AddressString) } $primarySmtp = $mailpf.WindowsEmailAddress.Address # Send-As perms Write-Host "### Reading and copying SendAs permissions ###" -ForegroundColor Yellow $perms = $mailpf | Get-ADPermission $sendAs = $perms | Where-Object { $_.ExtendedRights -like "Send-As" } foreach ($p in $sendAs) { $user = $p.User Write-Host ("Copying Send-As permission for [{0}] to [{1}]" -f $user, $targetAlias) $mbxDn = (Get-Mailbox -Identity $TargetMailboxName).DistinguishedName if ($PSCmdlet.ShouldProcess(("Mailbox '{0}'" -f $TargetMailboxName),("Grant Send-As to '{0}'" -f $user))) { Add-ADPermission -Identity $mbxDn -ExtendedRights 'Send-As' -User $user | Out-Null } } } # Folder permissions Write-Host "### Migrating / Copying folder permissions ###" -ForegroundColor Yellow $perms = Get-PublicFolderClientPermission -Identity $fullPath foreach ($perm in $perms) { $user = $perm.User $rights = $perm.AccessRights # Default/Anonymous werden auf Ordner gesetzt (nicht auf Mailbox) if ($user.DisplayName -in @('None','Default','Standard','Anonymous','Anonym')) { $pathForCmd = ('{0}:{1}' -f $targetAlias, $fullPath) if ($PSCmdlet.ShouldProcess(("Folder '{0}'" -f $pathForCmd),("Set permission for '{0}'" -f $user))) { try { Set-MailboxFolderPermission -Identity $pathForCmd -User $user -AccessRights $rights -ErrorAction Stop | Out-Null } catch { Write-Warning $_.Exception.Message } } } else { if ($PermissionMode -eq 'FullMailbox') { # Einfach: FullAccess aufs Postfach $who = $null try { $who = $user.ADRecipient.WindowsEmailAddress.Address } catch { $who = $user } if ($who) { if ($PSCmdlet.ShouldProcess(("Mailbox '{0}'" -f $TargetMailboxName),("Grant FullAccess to '{0}'" -f $who))) { if ($CopyOnly) { Add-MailboxPermission -Identity $TargetMailboxName -AccessRights FullAccess -User $who -WhatIf } else { Add-MailboxPermission -Identity $TargetMailboxName -AccessRights FullAccess -User $who | Out-Null } } } } else { # Präzise: ACL auf Zielordner $pathForCmd = ('{0}:{1}' -f $targetAlias, $fullPath) if ($PSCmdlet.ShouldProcess(("Folder '{0}'" -f $pathForCmd),("Add folder permission for '{0}'" -f $user))) { try { Add-MailboxFolderPermission -Identity $pathForCmd -User $user -AccessRights $rights -ErrorAction Stop | Out-Null } catch { Write-Warning $_.Exception.Message } } } } } # Additional admin as Owner (optional) if ($AdditionalAdminSam) { $pathForCmd = ('{0}:{1}' -f $targetAlias, $fullPath) if ($PSCmdlet.ShouldProcess(("Folder '{0}'" -f $pathForCmd),("Grant Owner to '{0}'" -f $AdditionalAdminSam))) { try { Add-MailboxFolderPermission -Identity $pathForCmd -User $AdditionalAdminSam -AccessRights Owner -ErrorAction Stop | Out-Null } catch { Write-Warning $_.Exception.Message } } } # Mailflow Cutover (disable mail-enabled PF) if ($mailpf) { Write-Host "### Disabling mail-enabled PF (if requested) ###" -ForegroundColor Yellow if ($CutoverMailflow) { if ($PSCmdlet.ShouldProcess(("Mail Public Folder '{0}'" -f $fullPath),"Disable")) { Disable-MailPublicFolder -Identity $mailpf -Confirm:$false } Start-Sleep 10 } else { Write-Host "WHATIF: would disable mail PF [$fullPath]" -ForegroundColor Cyan } } } # foreach folder # Assign PF addresses to target mailbox after cutover if ($CutoverMailflow) { Write-Host "### Assigning mail addresses of the disabled PFs to the target mailbox ###" -ForegroundColor Yellow if ($pfAddresses.Count -gt 0) { $uniq = $pfAddresses | Sort-Object -Unique $normalized = foreach ($a in $uniq) { if ($a -match '^(?i)smtp:(.*)$') { $addr = $Matches[1] if ($primarySmtp -and ($addr -ieq $primarySmtp)) { "SMTP:$addr" } else { "smtp:$addr" } } else { $a } # X500:, SIP:, etc. } if ($PSCmdlet.ShouldProcess(("Mailbox '{0}'" -f $TargetMailboxName),"Set proxy addresses / primary SMTP")) { Set-Mailbox -Identity $TargetMailboxName -EmailAddresses $normalized -EmailAddressPolicyEnabled:$false if ($primarySmtp) { Set-Mailbox -Identity $TargetMailboxName -WindowsEmailAddress $primarySmtp } } } } else { if ($pfAddresses.Count -gt 0) { $sim = $pfAddresses -join '; ' Write-Host ("WHATIF: would set EmailAddresses on [{0}] to: {1}" -f $TargetMailboxName,$sim) -ForegroundColor Cyan } } $end = Get-Date $dur = $end - $start Write-Host ("Finished. This took {0} hours, {1} minutes and {2} seconds." -f $dur.Hours,$dur.Minutes,$dur.Seconds) -ForegroundColor Green } finally { Stop-Transcript | Out-Null }