<# Functions for Public Folder migration (on-prem, WIA) Authors (original/adapted): Glen Scales, Alexander Zarenko Refactor: 2026-01 Notes: - PowerShell 5.1 compatible - Uses EWS Managed API (WIA/Default Credentials) - TLS validation bypass is active (same as original) #> function Load-EWSManagedAPI { [CmdletBinding()] param() # Wenn bereits eine EWS-Assembly im AppDomain ist, NICHT erneut laden $loaded = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Microsoft.Exchange.WebServices' } if ($loaded) { Write-Verbose ("EWS bereits geladen: {0}" -f ($loaded | Select-Object -First 1 -Expand Location)) return } # Sonst: höchste Version aus Registry laden (wie bisher) $key = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Exchange\Web Services' $ewsKey = Get-ChildItem -Path $key -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name if ($ewsKey) { $installDir = (Get-ItemProperty -Path "Registry::$ewsKey" -ErrorAction SilentlyContinue).'Install Directory' $dll = Join-Path $installDir 'Microsoft.Exchange.WebServices.dll' if (Test-Path $dll) { Import-Module $dll -ErrorAction Stop return } } Write-Error "EWS Managed API (>=1.2) nicht gefunden. Bitte installieren." throw } function Handle-SSL { [CmdletBinding()] param() # Keep original: trust-all TLS (for lab/compat) – security risk in prod $Provider = New-Object Microsoft.CSharp.CSharpCodeProvider $Params = New-Object System.CodeDom.Compiler.CompilerParameters $Params.GenerateExecutable = $False $Params.GenerateInMemory = $True $Params.IncludeDebugInformation = $False [void]$Params.ReferencedAssemblies.Add("System.DLL") $TASource=@' namespace Local.ToolkitExtensions.Net.CertificatePolicy{ public class TrustAll : System.Net.ICertificatePolicy { public TrustAll() { } public bool CheckValidationResult(System.Net.ServicePoint sp, System.Security.Cryptography.X509Certificates.X509Certificate cert, System.Net.WebRequest req, int problem) { return true; } } } '@ $TAResults = $Provider.CompileAssemblyFromSource($Params,$TASource) $TAAssembly = $TAResults.CompiledAssembly $TrustAll = $TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll") [System.Net.ServicePointManager]::CertificatePolicy = $TrustAll } function Connect-Exchange { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$MailboxName, [Parameter()][string]$Url ) Load-EWSManagedAPI Handle-SSL $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2 $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion) $service.UseDefaultCredentials = $true if ($Url) { $uri = [System.Uri]$Url $service.Url = $uri } else { # Autodiscover with WIA $service.AutodiscoverUrl($MailboxName, { $true }) } if (-not $service.Url) { throw "Error connecting to EWS" } return $service } function Get-PublicFolderRoutingHeader { [CmdletBinding()] param( [Parameter(Mandatory=$true)][Microsoft.Exchange.WebServices.Data.ExchangeService]$Service, [Parameter(Mandatory=$true)][string]$MailboxName, [Parameter(Mandatory=$true)][string]$Header, [Parameter()][string]$AutodiscoverUrl ) $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_SP1 $ads = New-Object Microsoft.Exchange.WebServices.Autodiscover.AutodiscoverService($ExchangeVersion) $ads.UseDefaultCredentials = $true $ads.EnableScpLookup = $false $ads.RedirectionUrlValidationCallback = { $true } $ads.PreAuthenticate = $true $ads.KeepAlive = $false if ($AutodiscoverUrl) { $ads.Url = $AutodiscoverUrl } if ($Header -eq 'X-AnchorMailbox') { $gsp = $ads.GetUserSettings($MailboxName, [Microsoft.Exchange.WebServices.Autodiscover.UserSettingName]::PublicFolderInformation) $pfi = $null if ($gsp.Settings.TryGetValue([Microsoft.Exchange.WebServices.Autodiscover.UserSettingName]::PublicFolderInformation, [ref]$pfi)) { if (-not $Service.HttpHeaders.$Header) { $Service.HttpHeaders.Add($Header, $pfi) } } } } function Get-PublicFolderContentRoutingHeader { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Microsoft.Exchange.WebServices.Data.ExchangeService]$Service, [Parameter(Mandatory=$true)] [string]$MailboxName, [Parameter(Mandatory=$true)] [string]$PfAddress, [Parameter()] [string]$AutodiscoverUrl ) process { $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_SP1 $ads = New-Object Microsoft.Exchange.WebServices.Autodiscover.AutodiscoverService($ExchangeVersion) $ads.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials $ads.UseDefaultCredentials = $true $ads.EnableScpLookup = $false $ads.RedirectionUrlValidationCallback = { $true } $ads.PreAuthenticate = $true $ads.KeepAlive = $false if ($AutodiscoverUrl) { # Explizit vorgegeben $ads.Url = $AutodiscoverUrl } else { # Dieser Call füllt $ads.Url zuverlässig $null = $ads.GetUserSettings($MailboxName, [Microsoft.Exchange.WebServices.Autodiscover.UserSettingName]::AutoDiscoverSMTPAddress) } if (-not $ads.Url) { throw "Autodiscover URL konnte nicht ermittelt werden. Übergib -AutodiscoverUrl (z. B. https://mail.domain.tld/autodiscover/autodiscover.svc)." } $xml = '' + ("{0}" -f $PfAddress) + 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a' + '' $req = [System.Net.HttpWebRequest]::Create($ads.Url.ToString().Replace(".svc",".xml")) $bytes = [System.Text.Encoding]::UTF8.GetBytes($xml) $req.ContentLength = $bytes.Length $req.ContentType = "text/xml" $req.UserAgent = "Microsoft Office/16.0 (Windows NT 6.3; Microsoft Outlook 16.0.6001; Pro)" $req.Headers.Add("Translate","F") $req.Method = "POST" $req.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials $stream = $req.GetRequestStream() $stream.Write($bytes,0,$bytes.Length) $stream.Close() $req.AllowAutoRedirect = $true # Bugfix (dein Original hatte $truee) $resp = $req.GetResponse().GetResponseStream() $sr = New-Object System.IO.StreamReader($resp) [xml]$xmlResp = $sr.ReadToEnd() if ($xmlResp.Autodiscover.Response.User.AutoDiscoverSMTPAddress) { $anchor = $xmlResp.Autodiscover.Response.User.AutoDiscoverSMTPAddress Write-Verbose ("Public Folder Content Routing Information Header : {0}" -f $anchor) $Service.HttpHeaders["X-AnchorMailbox"] = $anchor $Service.HttpHeaders["X-PublicFolderMailbox"] = $anchor } } } function Get-FolderFromPath { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$FolderPath, [Parameter(Mandatory=$true)][string]$MailboxName, [Parameter(Mandatory=$true)][Microsoft.Exchange.WebServices.Data.ExchangeService]$Service, [Parameter()][Microsoft.Exchange.WebServices.Data.PropertySet]$PropertySet ) # Bind to MsgFolderRoot of target mailbox $rootId = New-Object Microsoft.Exchange.WebServices.Data.FolderId( [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $MailboxName ) # EARLY RETURN for root if ([string]::IsNullOrEmpty($FolderPath) -or $FolderPath -eq '\') { return [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$rootId) } $tf = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$rootId) # Split path safely: remove leading/trailing "\" and empty segments $segments = $FolderPath.Trim('\').Split('\') | Where-Object { $_ -ne '' } foreach ($seg in $segments) { $fv = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1) if ($PropertySet) { $fv.PropertySet = $PropertySet } $sf = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo( [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, $seg ) $res = $Service.FindFolders($tf.Id, $sf, $fv) if ($res.TotalCount -gt 0) { foreach ($f in $res.Folders) { $tf = $f } } else { Write-Host "Error Folder not found check path and try again" return $null } } if ($tf -ne $null) { return [Microsoft.Exchange.WebServices.Data.Folder]$tf } throw "Folder Not found" } function PublicFolderIdFromPath { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Microsoft.Exchange.WebServices.Data.ExchangeService]$Service, [Parameter(Mandatory=$true)] [string]$FolderPath, [Parameter(Mandatory=$true)] [string]$SmtpAddress, [Parameter()] [string]$AutodiscoverUrl ) process { # 1) Sicherstellen, dass der X-AnchorMailbox-Header gesetzt ist if (-not $Service.HttpHeaders['X-AnchorMailbox']) { Get-PublicFolderRoutingHeader -Service $Service -MailboxName $SmtpAddress -Header 'X-AnchorMailbox' -AutodiscoverUrl $AutodiscoverUrl } # 2) PublicFoldersRoot binden + PropertySet vorbereiten $folderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId( [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::PublicFoldersRoot ) $ps = New-Object Microsoft.Exchange.WebServices.Data.PropertySet( [Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties ) # PR_REPLICA_LIST optional hinzufügen (kann bei EWS-DLL-Mischbetrieb fehlschlagen) $PR_REPLICA_LIST = $null try { $PR_REPLICA_LIST = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition( 0x6698, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary ) # Expliziter Cast – bei homogener Assembly problemlos, bei Mismatch fangen wir ab [void]$ps.Add([Microsoft.Exchange.WebServices.Data.PropertyDefinitionBase]$PR_REPLICA_LIST) } catch { Write-Verbose "Konnte PR_REPLICA_LIST nicht zum PropertySet hinzufügen (möglicher EWS-Typkonflikt). Fahre ohne fort." $PR_REPLICA_LIST = $null } $tf = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service, $folderId, $ps) # 3) Root-Fall: "\" sofort zurückgeben if ([string]::IsNullOrEmpty($FolderPath) -or $FolderPath -eq '\') { # Optional: Routing-Header aus REPLICA_LIST ableiten (falls verfügbar) if ($PR_REPLICA_LIST) { $val = $null if ($tf.TryGetProperty($PR_REPLICA_LIST, [ref]$val)) { $guid = [System.Text.Encoding]::ASCII.GetString($val, 0, 36) $addr = New-Object System.Net.Mail.MailAddress($Service.HttpHeaders['X-AnchorMailbox']) $pfHeader = $guid + '@' + $addr.Host Write-Verbose ("Root Public Folder Routing Information Header : {0}" -f $pfHeader) if (-not $Service.HttpHeaders.'X-PublicFolderMailbox') { $Service.HttpHeaders.Add('X-PublicFolderMailbox', $pfHeader) } } else { Write-Verbose "PR_REPLICA_LIST am Root nicht verfügbar – verwende nur X-AnchorMailbox." } } return $tf.Id.UniqueId.ToString() } # 4) Teilbaum durchsuchen – Pfad sicher splitten $segments = $FolderPath.Trim('\').Split('\') | Where-Object { $_ -ne '' } foreach ($seg in $segments) { $fv = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1) $fv.PropertySet = $ps $sf = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo( [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, $seg ) $res = $Service.FindFolders($tf.Id, $sf, $fv) if ($res.TotalCount -gt 0) { foreach ($f in $res.Folders) { $tf = $f } } else { Write-Error "Error Folder Not Found (Segment: '$seg' im Pfad '$FolderPath')" return $null } } # 5) Nach erfolgreicher Auflösung: Content-Routing-Header optional ergänzen if ($PR_REPLICA_LIST) { $val = $null if ($tf.TryGetProperty($PR_REPLICA_LIST, [ref]$val)) { $guid = [System.Text.Encoding]::ASCII.GetString($val, 0, 36) $addr = New-Object System.Net.Mail.MailAddress($Service.HttpHeaders['X-AnchorMailbox']) $pfHeader = $guid + '@' + $addr.Host Write-Verbose ("Target Public Folder Routing Information Header : {0}" -f $pfHeader) # Setzt X-AnchorMailbox/X-PublicFolderMailbox für Content-Routing (Autodiscover-XML) Get-PublicFolderContentRoutingHeader -Service $Service -MailboxName $SmtpAddress -PfAddress $pfHeader -AutodiscoverUrl $AutodiscoverUrl } else { Write-Verbose "PR_REPLICA_LIST am Zielordner nicht verfügbar – verwende nur X-AnchorMailbox." } } else { Write-Verbose "PR_REPLICA_LIST nicht gesetzt – verwende nur X-AnchorMailbox." } # 6) UniqueId als String zurückgeben return $tf.Id.UniqueId.ToString() } } function Create-Folder { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$MailboxName, [Parameter(Mandatory=$true)][Microsoft.Exchange.WebServices.Data.ExchangeService]$Service, [Parameter(Mandatory=$true)][string]$NewFolderName, [Parameter()][string]$ParentFolder, [Parameter()][string]$FolderClass ) $newFolder = New-Object Microsoft.Exchange.WebServices.Data.Folder($Service) $newFolder.DisplayName = $NewFolderName $newFolder.FolderClass = if ([string]::IsNullOrEmpty($FolderClass)) { 'IPF.Note' } else { $FolderClass } # Root direkt binden, wenn Parent leer oder "\" ist if ([string]::IsNullOrEmpty($ParentFolder) -or $ParentFolder -eq '\') { $rootId = New-Object Microsoft.Exchange.WebServices.Data.FolderId( [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $MailboxName ) $EWSParentFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$rootId) } else { # Elternpfad sauber auflösen $EWSParentFolder = Get-FolderFromPath -MailboxName $MailboxName -Service $Service -FolderPath $ParentFolder if (-not $EWSParentFolder) { throw "Parent folder '$ParentFolder' not found in target mailbox '$MailboxName'." } } # Existenz prüfen $fv = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1) $sf = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo( [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, $NewFolderName ) $res = $Service.FindFolders($EWSParentFolder.Id, $sf, $fv) if ($res.TotalCount -eq 0) { $newFolder.Save($EWSParentFolder.Id) Write-Host ("Folder [{0}] Created" -f $NewFolderName) -ForegroundColor Green } else { Write-Host ("Folder [{0}] already exists." -f $NewFolderName) -ForegroundColor DarkGray } } function Get-PublicFolderItems { <# Refactored version: Copies items from PF path to target mailbox folder. - No global variables required - Supports ShouldProcess - CSV de-dup uses Item.Id.UniqueId #> [CmdletBinding(SupportsShouldProcess=$true)] param( [Parameter(Mandatory=$true)][Microsoft.Exchange.WebServices.Data.ExchangeService]$Service, [Parameter(Mandatory=$true)][string]$AdminMailboxSmtp, [Parameter(Mandatory=$true)][string]$PublicFolderPath, [Parameter(Mandatory=$true)][string]$TargetMailboxSmtp, [Parameter(Mandatory=$true)][string]$ParentPath, [Parameter(Mandatory=$true)][string]$FolderName, [Parameter()][string]$FolderClass, [Parameter()][string]$AutodiscoverUrl, [Parameter()][switch]$DoNotCopyItems ) # Anchor Headers Get-PublicFolderRoutingHeader -Service $Service -MailboxName $AdminMailboxSmtp -Header "X-AnchorMailbox" -AutodiscoverUrl $AutodiscoverUrl $fldId = PublicFolderIdFromPath -Service $Service -FolderPath $PublicFolderPath -SmtpAddress $AdminMailboxSmtp -AutodiscoverUrl $AutodiscoverUrl $subFolderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId($fldId) # Ensure destination folder exists Create-Folder -MailboxName $TargetMailboxSmtp -Service $Service -NewFolderName $FolderName -ParentFolder $ParentPath -FolderClass $FolderClass $targetFolder = Get-FolderFromPath -FolderPath $PublicFolderPath -MailboxName $TargetMailboxSmtp -Service $Service if ($DoNotCopyItems) { return } # Paging $iv = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000) $iv.PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties) # Copy log $copyLog = ".\{0}.csv" -f ($TargetMailboxSmtp -replace '[^a-zA-Z0-9._-]','_') $already = @() if (Test-Path $copyLog) { $already = Import-Csv $copyLog -Encoding UTF8 | Select-Object -ExpandProperty UniqueId Write-Host "Previous copy results imported" -ForegroundColor Cyan } else { Write-Host "No file with previous copy results found" -ForegroundColor Yellow } $totalProcessed = 0 do { $fi = $Service.FindItems($subFolderId,$iv) $itemTotal = $fi.Items.Count $idx = 0 foreach ($item in $fi.Items) { $subject = if ($item.Subject) { $item.Subject } else { '#no subject#' } $idx++ $percent = if ($itemTotal -gt 0) { [int](($idx / $itemTotal) * 100) } else { 100 } Write-Progress -Activity ("{0}/{1} - Copying items to MBX [{2}]" -f $idx,$itemTotal,$TargetMailboxSmtp) -Status $subject -PercentComplete $percent $uid = $item.Id.UniqueId if ($already -contains $uid) { Write-Host "`tItem already copied, skipping..." -ForegroundColor DarkGray } else { if ($PSCmdlet.ShouldProcess(("Item '{0}'" -f $subject), ("Copy to {0}" -f $targetFolder.DisplayName))) { $item.Copy($targetFolder.Id) [pscustomobject]@{ UniqueId = $uid } | Export-Csv $copyLog -NoTypeInformation -Encoding UTF8 -Append } } $totalProcessed++ } $iv.Offset += $fi.Items.Count } while ($fi.MoreAvailable -eq $true) Write-Progress -Activity ("{0}/{1} - Copying items to MBX [{2}]" -f $idx,$itemTotal,$TargetMailboxSmtp) -Completed }