From 74e486e9ab42d77a4ec2c949402af35fb3d0c1bc Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 17 Mar 2025 15:22:16 -0500 Subject: [PATCH 01/10] Initial commit of PF hybrid scripts --- .../Hybrid/Import-PublicFolderMailboxes.ps1 | Bin 0 -> 11986 bytes .../ImportPublicFolderMailboxes.strings.psd1 | Bin 0 -> 2668 bytes .../Sync-MailPublicFoldersCloudToOnprem.ps1 | 617 ++++++++++++++++++ ...ailPublicFoldersCloudToOnprem.strings.psd1 | Bin 0 -> 4594 bytes 4 files changed, 617 insertions(+) create mode 100644 PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 create mode 100644 PublicFolders/Hybrid/ImportPublicFolderMailboxes.strings.psd1 create mode 100644 PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 create mode 100644 PublicFolders/Hybrid/SyncMailPublicFoldersCloudToOnprem.strings.psd1 diff --git a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..3edbc30b9e364a26ac2ebe9b473d09428c7fb345 GIT binary patch literal 11986 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 W+Z@NN#+vu#!Fpuhx8Vq1j(-7%rQpy2 literal 0 HcmV?d00001 diff --git a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 new file mode 100644 index 0000000000..8490ab3bd5 --- /dev/null +++ b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 @@ -0,0 +1,617 @@ +# .SYNOPSIS +# Sync-MailPublicFoldersCloudToOnprem.ps1 +# This script imports the new mail public folders as sync mail public folders from Exchange Online to on-premise. +# And also synchronizes the properites of existing mail-enabled public folders from Exchange Online to on-premises (thereby overriding the mail public folder properties in on-premise). +# +# Example input to the script: +# +# Sync-MailPublicFoldersCloudToOnprem.ps1 -ConnectionUri -CsvSummaryFile +# +# The above example imports new mail public folders objects from Exchange Online as sync mail public folders to on-premise. +# +# .DESCRIPTION +# +# Copyright (c) 2016 Microsoft Corporation. All rights reserved. +# +# THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK +# OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER. +param ( + [Parameter(Mandatory=$false)] + [PSCredential] $Credential, + + [Parameter(Mandatory = $false)] + [ValidateNotNull()] + [string] $ConnectionUri = "https://outlook.office365.com/powerShell-liveID", + + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] $CsvSummaryFile + ) + +# Writes a dated information message to console +function WriteInfoMessage() +{ + param ($message) + Write-Host "[$($(Get-Date).ToString())]" $message; +} + +# Writes an error importing a mail public folder to the CSV summary +function WriteErrorSummary() +{ + param ($folder, $operation, $errorMessage, $commandtext) + + WriteOperationSummary $folder.Guid $operation $errorMessage $commandtext; + $script:errorsEncountered++; +} + +# Writes the operation executed and its result to the output CSV +function WriteOperationSummary() +{ + param ($folder, $operation, $result, $commandtext) + + $columns = @( + (Get-Date).ToString(), + $folder.Guid, + $operation, + (EscapeCsvColumn $result), + (EscapeCsvColumn $commandtext) + ); + + Add-Content $CsvSummaryFile -Value ("{0},{1},{2},{3},{4}" -f $columns); +} + +#Escapes a column value based on RFC 4180 (http://tools.ietf.org/html/rfc4180) +function EscapeCsvColumn() +{ + param ([string]$text) + + if ($text -eq $null) + { + return $text; + } + + $hasSpecial = $false; + for ($i=0; $i -lt $text.Length; $i++) + { + $c = $text[$i]; + if ($c -eq $script:csvEscapeChar -or + $c -eq $script:csvFieldDelimiter -or + $script:csvSpecialChars -contains $c) + { + $hasSpecial = $true; + break; + } + } + + if (-not $hasSpecial) + { + return $text; + } + + $ch = $script:csvEscapeChar.ToString([System.Globalization.CultureInfo]::InvariantCulture); + return $ch + $text.Replace($ch, $ch + $ch) + $ch; +} + +## Create a tenant PSSession against Exchange Online using modern auth. +function InitializeExchangeOnlineRemoteSession() +{ + WriteInfoMessage $LocalizedStrings.CreatingRemoteSession; + + $oldWarningPreference = $WarningPreference; + $oldVerbosePreference = $VerbosePreference; + + try + { + Import-Module ExchangeOnlineManagement -ErrorAction SilentlyContinue; + if (Get-Module ExchangeOnlineManagement) + { + $sessionOption = (New-PSSessionOption -SkipCACheck); + Connect-ExchangeOnline -Credential $Credential -ConnectionURI $ConnectionUri -PSSessionOption $sessionOption -Prefix "Remote" -ErrorAction SilentlyContinue; + $script:isConnectedToExchangeOnline = $true; + } + else + { + Write-Warning $LocalizedStrings.EXOV2ModuleNotInstalled; + Exit; + } + } + catch + { + Write-Error "Error message: $($_.Exception.Message)"; + WriteInfoMessage ($LocalizedStrings.ConnectExchangeOnlineFailure); + Exit; + } + WriteInfoMessage ($LocalizedStrings.RemoteSessionCreatedSuccessfully); +} + +## Formats the command and its parameters to be printed on console or to file +function FormatCommand() +{ + param ([string]$command, [System.Collections.IDictionary]$parameters) + + $commandText = New-Object System.Text.StringBuilder; + [void]$commandText.Append($command); + foreach ($name in $parameters.Keys) + { + [void]$commandText.AppendFormat(" -{0}:",$name); + + $value = $parameters[$name]; + if ($value -isnot [Array]) + { + [void]$commandText.AppendFormat("`"{0}`"", $value); + } + elseif ($value.Length -eq 0) + { + [void]$commandText.Append("@()"); + } + else + { + [void]$commandText.Append("@("); + foreach ($subValue in $value) + { + [void]$commandText.AppendFormat("`"{0}`",",$subValue); + } + + [void]$commandText.Remove($commandText.Length - 1, 1); + [void]$commandText.Append(")"); + } + } + + return $commandText.ToString(); +} + +## Get external email address domains which are not configured on the on-premise connector +function GetMissingDomainsFromConnector() +{ + param($sendConnectorDomains) + + $missingDomains = @(); + + foreach ($domain in $script:ExternalEmailAddressDomains.Keys) + { + if (-not $sendConnectorDomains.ContainsKey($domain)) + { + $missingDomains += $domain; + } + } + + return $missingDomains; +} + +## Check if the centralized transport feature is enabled +function IsCentralizedTransportEnabled() +{ + $hybridConf = Get-HybridConfiguration + + if ($hybridConf -ne $null -and $hybridConf.Features -ne $null -and $hybridConf.Features.Contains("CentralizedTransport")) + { + return $true; + } + + return $false; +} + +## Get send connector (on-premise) domains configured for O365 +function GetSendConnectorDomains() +{ + $connector = Get-SendConnector -Identity $script:SendConnectorToO365 -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue; + $domains = @{}; + + if ($connector -ne $null -and $connector.AddressSpaces -ne $null) + { + foreach ($addressSpace in $connector.AddressSpaces) + { + $domain = $addressSpace.Address.ToString(); + + if(-not $domains.ContainsKey($domain)) + { + $domains.Add($domain, $false); + } + } + } + + return $domains; +} + +## Store distinct domains used in the external email addresses +function StoreExternalEmailAddressDomain( + [string] $emailAddress) +{ + if (-not [string]::IsNullOrEmpty($emailAddress)) + { + $index = $emailAddress.IndexOf('@') + $domain = $emailAddress.Substring($index + 1) + + if (-not $script:ExternalEmailAddressDomains.ContainsKey($domain)) + { + $script:ExternalEmailAddressDomains.Add($domain, $false) + } + } +} + +## Concatenate a list of domains +function ConcatDomains() +{ + param($domainList) + + $domains = $script:separator; + $domains += $domainList -join $script:separator + return $domains; +} + +## Get external email address from an EXO mail public folder +function GetExternalEmailAddress() +{ + param ($remotePublicFolder) + + $primarySmtpAddress = $remotePublicFolder.PrimarySmtpAddress.ToString(); + + if ($primarySmtpAddress.EndsWith($script:OnMicrosoftDomain, [StringComparison]::InvariantCultureIgnoreCase)) + { + return $primarySmtpAddress; + } + + $externalEmailAddress = $primarySmtpAddress; + $primarySmtpAddressParts = $primarySmtpAddress.Split($script:proxyAddressSeparators); # alias@domain + + if ($remotePublicFolder.EmailAddresses -ne $null -and $remotePublicFolder.EmailAddresses.Count -gt 0) + { + foreach ($address in $remotePublicFolder.EmailAddresses) + { + if ($address.StartsWith($script:SmtpPrefix, [StringComparison]::InvariantCultureIgnoreCase)) + { + $addressParts = $address.Split($script:proxyAddressSeparators); # smtp:alias@domain + + if ($addressParts.Count -eq 3 -and + $addressParts[1].Equals($primarySmtpAddressParts[0], [StringComparison]::InvariantCultureIgnoreCase) -and + $addressParts[2].EndsWith($script:OnMicrosoftDomain, [StringComparison]::InvariantCultureIgnoreCase)) + { + return (RemoveSmtpPrefix $address); + } + } + } + } + + return $externalEmailAddress; +} + +## Remove smtp prefix from the email address +function RemoveSmtpPrefix() +{ + param ($emailAddress) + + if ([String]::IsNullOrEmpty($emailAddress)) + { + return [String]::Empty; + } + + if ($emailAddress.StartsWith($script:SmtpPrefix, [StringComparison]::InvariantCultureIgnoreCase)) + { + return $emailAddress.Substring($script:SmtpPrefixLength); + } + + return $emailAddress; +} + +## Retrieve mail enabled public folders from EXO +function GetRemoteMailPublicFolders() +{ + $mailPublicFolders = Get-RemoteMailPublicFolder -ResultSize:Unlimited -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue; + + # Return the results + if ($mailPublicFolders -eq $null -or ([array]($mailPublicFolders)).Count -eq 0) + { + return $null; + } + + return $mailPublicFolders; +} + +#Get list of MEPF's in OnPrem to be deleted +function GetFoldersToMailDisable( + [object[]] $localMailPublicFolders, + [hashtable] $validExternalEmailAddresses) +{ + $foldersToMailDisable = @(); + foreach($syncPublicFolder in $localMailPublicFolders) + { + $localExternalEmailAddress = [String]::Empty; + + if ($syncPublicFolder.ExternalEmailAddress -ne $null) + { + $localExternalEmailAddress = RemoveSmtpPrefix $syncPublicFolder.ExternalEmailAddress.ToString(); + } + + if (-not $validExternalEmailAddresses.ContainsKey($localExternalEmailAddress)) + { + $foldersToMailDisable += $syncPublicFolder.Identity; + } + } + return $foldersToMailDisable +} + +## Sync mail public folders from cloud to on-premise. +function SyncMailPublicFolders( + [object[]] $mailPublicFolders) +{ + $validExternalEmailAddresses = @{}; + + if ($mailPublicFolders -ne $null) + { + foreach ($mailPublicFolder in $mailPublicFolders) + { + # extracting properties + $alias = $mailPublicFolder.Alias.Trim(); + $identity = $mailPublicFolder.PrimarySmtpAddress.ToString(); + $externalEmailAddress = GetExternalEmailAddress $mailPublicFolder; + $entryId = $mailPublicFolder.EntryId.ToString(); + $name = $mailPublicFolder.Name.Trim(); + $displayName = $mailPublicFolder.DisplayName.Trim() + $hiddenFromAddressListsEnabled = $mailPublicFolder.HiddenFromAddressListsEnabled; + + $windowsEmailAddress = $mailPublicFolder.WindowsEmailAddress.ToString(); + if ($windowsEmailAddress -eq "") + { + $windowsEmailAddress = $externalEmailAddress; + } + + # extracting all the EmailAddress + $emailAddress = @(); + foreach($address in $mailPublicFolder.EmailAddresses) + { + $emailAddress += $address.ToString(); + } + + # preserve the ability to reply via Outlook's nickname cache post-migration + $x500Proxy = ("X500:" + $mailPublicFolder.LegacyExchangeDN); + if ($x500Proxy -notin $emailAddress) + { + $emailAddress += $x500Proxy; + } + + WriteInfoMessage ($LocalizedStrings.SyncingMailPublicFolder -f $alias); + + $syncPublicFolder = Get-MailPublicFolder -Identity $identity -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue; + + if ($syncPublicFolder -eq $null) + { + WriteInfoMessage ($LocalizedStrings.CreatingSyncMailPublicFolder -f $alias); + try + { + $newParams = @{}; + $newParams.Add("Name", $name); + $newParams.Add("ExternalEmailAddress", $externalEmailAddress); + $newParams.Add("Alias", $alias); + $newParams.Add("EntryId", $entryId); + $newParams.Add("WindowsEmailAddress", $windowsEmailAddress); + $newParams.Add("WarningAction", "SilentlyContinue"); + $newParams.Add("ErrorAction", "Stop"); + + [string]$createSyncPublicFolder = (FormatCommand $script:NewSyncMailPublicFolderCommand $newParams); + + # Creating new sync mail public folder + $newMailPublicFolder = &$script:NewSyncMailPublicFolderCommand @newParams; + + WriteOperationSummary $mailPublicFolder $LocalizedStrings.CreateOperationName $LocalizedStrings.CsvSuccessResult $createSyncPublicFolder; + + $setParams = @{}; + $setParams.Add("Identity", $name); + $setParams.Add("EmailAddresses", $emailAddress); + $setParams.Add("DisplayName", $displayName); + $setParams.Add("HiddenFromAddressListsEnabled", $hiddenFromAddressListsEnabled); + $setParams.Add("WarningAction", "SilentlyContinue"); + $setParams.Add("ErrorAction", "Stop"); + $setParams.Add("EmailAddressPolicyEnabled", $false); + + [string]$setOtherProperties = (FormatCommand $script:SetMailPublicFolderCommand $setParams); + + # Setting other properties to the newly created sync mail public folder + &$script:SetMailPublicFolderCommand @setParams; + + WriteOperationSummary $mailPublicFolder $LocalizedStrings.SetOperationName $LocalizedStrings.CsvSuccessResult $setOtherProperties; + + $validExternalEmailAddresses.Add($externalEmailAddress, $false); + $script:CreatedPublicFoldersCount++; + } + + catch + { + WriteErrorSummary $mailPublicFolder $LocalizedStrings.CreateOperationName $_.Exception.Message ""; + Write-Error $_; + } + } + + else + { + WriteInfoMessage ($LocalizedStrings.UpdatingSyncMailPublicFolder -f $syncPublicFolder); + try + { + $updateParams = @{}; + $updateParams.Add("Identity", $syncPublicFolder); + $updateParams.Add("EmailAddresses", $emailAddress); + $updateParams.Add("HiddenFromAddressListsEnabled", $hiddenFromAddressListsEnabled); + $updateParams.Add("DisplayName", $displayName); + $updateParams.Add("Name", $name); + $updateParams.Add("ExternalEmailAddress", $externalEmailAddress); + $updateParams.Add("Alias", $alias); + $updateParams.Add("WindowsEmailAddress", $windowsEmailAddress); + $updateParams.Add("WarningAction", "SilentlyContinue"); + $updateParams.Add("ErrorAction", "Stop"); + + [string]$updateProperties = (FormatCommand $script:SetMailPublicFolderCommand $updateParams); + + # Setting properties to the existing sync mail public folder + &$script:SetMailPublicFolderCommand @updateParams; + + WriteOperationSummary $mailPublicFolder $LocalizedStrings.UpdateOperationName $LocalizedStrings.CsvSuccessResult $updateProperties; + + $validExternalEmailAddresses.Add($externalEmailAddress, $false); + $script:UpdatedPublicFoldersCount++; + } + + catch + { + WriteErrorSummary $mailPublicFolder $LocalizedStrings.UpdateOperationName $_.Exception.Message $updateProperties; + Write-Error $_; + } + + } + + StoreExternalEmailAddressDomain $externalEmailAddress; + WriteInfoMessage ($LocalizedStrings.DoneSyncingMailPublicFolder -f $alias); + Write-Host ""; + } + } + + else + { + WriteInfoMessage ($LocalizedStrings.NoMailPublicFoldersToSync); + Write-Host ""; + } + + WriteInfoMessage ($LocalizedStrings.DeleteSyncMailPublicFolderTitle); + + $localMailPublicFolders = Get-MailPublicFolder -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue; + + $foldersToMailDisable = GetFoldersToMailDisable $localMailPublicFolders $validExternalEmailAddresses; + if ($foldersToMailDisable -eq $null) + { + return; + } + + $foldersToMailDisableFile = Join-Path $PWD "FoldersToMailDisable.txt" + Set-Content -Path $foldersToMailDisableFile -Value $foldersToMailDisable + do + { + $confirmation = Read-Host ($LocalizedStrings.FoldersToMailDisableConfirmation -f $foldersToMailDisableFile); + } + while ($confirmation -ne "Y" -and $confirmation -ne "y" -and $confirmation -ne "N" -and $confirmation -ne "n") + + if ($confirmation -eq "N" -or $confirmation -eq "n") + { + return; + } + + foreach($syncPublicFolder in $localMailPublicFolders) + { + $localExternalEmailAddress = [String]::Empty; + + if ($syncPublicFolder.ExternalEmailAddress -ne $null) + { + $localExternalEmailAddress = RemoveSmtpPrefix $syncPublicFolder.ExternalEmailAddress.ToString(); + } + + if (-not $validExternalEmailAddresses.ContainsKey($localExternalEmailAddress)) + { + WriteInfoMessage ($LocalizedStrings.DeletingSyncMailPublicFolder -f $syncPublicFolder); + try + { + $deleteParams = @{}; + $deleteParams.Add("Identity", $syncPublicFolder); + $deleteParams.Add("Confirm", $false); + [string]$disableMailPublicFolder = (FormatCommand $script:DeletePublicFolderCommand $deleteParams); + + # Deleting sync mail public folder + &$script:DeletePublicFolderCommand @deleteParams; + + WriteOperationSummary $syncPublicFolder $LocalizedStrings.DeleteOperationName $LocalizedStrings.CsvSuccessResult $disableMailPublicFolder; + $script:RemovedPublicFoldersCount++; + } + catch + { + WriteErrorSummary $syncPublicFolder $LocalizedStrings.DeleteOperationName $_.Exception.Message $disableMailPublicFolder; + Write-Error $_; + } + } + } +} + +################ DECLARING GLOBAL VARIABLES ################ +$script:session = $null; + +$script:csvSpecialChars = @("`r", "`n"); +$script:csvEscapeChar = '"'; +$script:csvFieldDelimiter = ','; +$script:separator = "`n`t"; +$script:NewSyncMailPublicFolderCommand = "New-SyncMailPublicFolder"; +$script:SetMailPublicFolderCommand = "Set-MailPublicFolder"; +$script:DeletePublicFolderCommand = "Disable-MailPublicFolder"; +$script:CreatedPublicFoldersCount = 0; +$script:UpdatedPublicFoldersCount = 0; +$script:RemovedPublicFoldersCount = 0; +$script:ExternalEmailAddressDomains = @{}; +$script:SendConnectorToO365 = "Outbound to Office 365"; +$script:OnMicrosoftDomain = ".onmicrosoft.com"; +$script:SmtpPrefix = "smtp:"; +$script:SmtpPrefixLength = $script:SmtpPrefix.Length; +[char[]]$script:proxyAddressSeparators = ':','@'; + +#load hashtable of localized string +Import-LocalizedData -BindingVariable LocalizedStrings -FileName SyncMailPublicFoldersCloudToOnprem.strings.psd1 + +#minimum supported exchange version to run this script +$minSupportedVersion = 15 +################ END OF DECLARATION ######################### + +if (Test-Path $CsvSummaryFile) +{ + Remove-Item $CsvSummaryFile -Confirm:$Confirm -Force; +} + +# Write the output CSV headers +$csvFile = New-Item -Path $CsvSummaryFile -ItemType File -Force -ErrorAction:Stop -Value ("#{0},{1},{2},{3},{4}`r`n" -f $LocalizedStrings.TimestampCsvHeader, + $LocalizedStrings.IdentityCsvHeader, + $LocalizedStrings.OperationCsvHeader, + $LocalizedStrings.ResultCsvHeader, + $LocalizedStrings.CommandCsvHeader); + +$localServerVersion = (Get-ExchangeServer $env:COMPUTERNAME -ErrorAction:Stop).AdminDisplayVersion; +# This script can run from Exchange 2013 Management shell and above +if ($localServerVersion.Major -lt $minSupportedVersion) +{ + Write-Error ($LocalizedStrings.LocalServerVersionNotSupported -f $localServerVersion) -ErrorAction:Continue; + Exit; +} + +# Create a PSSession +WriteInfoMessage ($LocalizedStrings.CreatingRemoteSession); + +InitializeExchangeOnlineRemoteSession; + +# Get mail enabled public folders in cloud +WriteInfoMessage ($LocalizedStrings.StartedImportingMailPublicFolders); +Write-Host ""; + +$mailPublicFoldersEXO = GetRemoteMailPublicFolders; + +# Create sync mail public folders in on-premise +SyncMailPublicFolders $mailPublicFoldersEXO; + +Write-Host ""; +WriteInfoMessage ($LocalizedStrings.CompletedImportingMailPublicFolders); +WriteInfoMessage ($LocalizedStrings.CompletedStatsCount -f $script:CreatedPublicFoldersCount, $script:UpdatedPublicFoldersCount, $script:RemovedPublicFoldersCount); +Write-Host ""; + +if ((IsCentralizedTransportEnabled) -and $script:ExternalEmailAddressDomains.Count -gt 0) +{ + $sendConnectorDomains = GetSendConnectorDomains; + + if ($sendConnectorDomains -eq $null -or $sendConnectorDomains.Count -eq 0) + { + $domains = ConcatDomains $script:ExternalEmailAddressDomains.Keys; + Write-Warning ($LocalizedStrings.VerifyConnectorAcceptedDomainsMessage -f $domains); + } + else + { + $missingDomains = GetMissingDomainsFromConnector $sendConnectorDomains; + + if ($missingDomains -ne $null -and $missingDomains.Count -gt 0) + { + $domains = ConcatDomains $missingDomains; + Write-Warning ($LocalizedStrings.VerifyConnectorAcceptedDomainsMessage -f $domains); + } + } +} + +# Terminate the PSSession +Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue; diff --git a/PublicFolders/Hybrid/SyncMailPublicFoldersCloudToOnprem.strings.psd1 b/PublicFolders/Hybrid/SyncMailPublicFoldersCloudToOnprem.strings.psd1 new file mode 100644 index 0000000000000000000000000000000000000000..aa1490db5b009572bf481f3e53500b36b465cf52 GIT binary patch literal 4594 zcmcJTTW{l36ovOQ693_7c<3OJmML#Y2v9o}m8PAFmJ#!qCh0Vg*dcc4AjDq>)_1lK z$959M+*CDhkvK1*BL$ZmDDwz;0YvvV8kYHY#2vLpM@ zKCqAC&yA#B*=swC_ipVMNlzM`E6LAoVK=t1d;Oj3$%Sk$Y#Mz9`$G1*IzgUYE7+=~ z7wn;wcl@ z6se!VFzl0V#@w}sX4}X5TCp7RT-lwZQnb#)yO=#J0w?|!I!8`(tZ$$-gqfA(h(8F$ zg?wyu?m<>ceI`yhW6$Mbq2F4+DUw5R-^q3zwLIo*BVm?&42zaCpR2M`j$GHmfm#oW zoKtlSxJQQ|uTtoMPbDrOF^w6&)3d*a5T4v(LaL99XEhX^{{|E9G;3wEuckq1tn3{t z>IW40ETjVpAVg-)gnfDDt}{~y^|}K2c|`SEK9$YXlg|TEok0GCxGO5I*Z**@g=Fm%0{q-f$zl<_PM5R_0?$hqep}URgKJ9Hu z^uLX9Z&YvfUcYS_beN?}{m?U}d$qb!@Q6-aHORNwD|w#C z?;>(z-OTeh5#j1DPL=Y4?kd(UeSD@k-R4_a*yuY}ipXWU0Fg1*Hc__C`AT=$4_10Y z)%e~JbV}IaT&Zi1DcDQdnZ(}0K0$R<`b=?tuIp#b)O@C?62zQG*`}(xempv=vygrI z@_n?{jgy>mMBBbkwLQGB&m=|9E%ZxFFcq$`mC&0O`tja~y|Fx%5!c;22d6~sdq;=! z33tag*Kq3Ac;1D~>zEr-;IwYpb=0!uV`1gd?DwAia@JkuJm{pz5*+C>+ei(M1?CVd zJ??UovT3`cl|2Pkgt4BZsCT-0%&uZHKO#Ex{A!+px)$&XpQo2^F8t zQ+1(i(a&yZY+mVYTmMIVjwD+g{x%JV!G=8tbl~x>-C!6l{gb6yeZyrI=1q}P_h&D( zo9D`*&u27U;#3ljxA!Ra5c)fH4e!~cVZ+xAgXl8YXJ7QF3uSbrdsKthuk+z1`eH&; zJ;?qTzrjZM+1_!|Z1%Sc`qh*81N(gcNuHj|+Q%^?x4`TotGwMMOT<4Cdyrek4u;1^ z<#w-o^QbY|%95Da9oVwUMvAvBD#EM%UUzwiVhV1vn#kGBJ@$M)OosM5mv6pT@`JT= zeS#ftXR`+T2i;po)Y2y6sHW-QyK(AMu8n=SWY}cH}$? z_T1juiCqijT!onl{l3)hN*7Hw>2%?Jmq)%)r8xVxM%omZC8^)C>k{8XW0zBg=kO0? zkn=swd6G`PJ*PZ}m-fAEzp+zgUdjr+cBUMu$xB)Bt%&~XO!A7|J@+<}hU>mx`sT#+ z7|W_h;9C;&v72v~ZTnCNJrsO{@#*uEe839#?^%VISBl0nOSXL?gHty{=6bg}+jWPw zzY!$OwfxctJRfrRHHjx>0vbFuB>(^b literal 0 HcmV?d00001 From 28c5c44733883fdd01039e3b318d8618421ebaf2 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 17 Mar 2025 15:50:04 -0500 Subject: [PATCH 02/10] Codeformatter and PSScriptAnalyzer fixes --- .../Hybrid/Import-PublicFolderMailboxes.ps1 | Bin 11986 -> 5404 bytes .../Sync-MailPublicFoldersCloudToOnprem.ps1 | 658 ++++++++---------- 2 files changed, 285 insertions(+), 373 deletions(-) diff --git a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 index 3edbc30b9e364a26ac2ebe9b473d09428c7fb345..c7279c12092b449131411e56273fe864bccc3f1f 100644 GIT binary patch literal 5404 zcmb_g%W~T`6y5VH?#Nt7laNXJ$U@U}lGsk8nb_8ZPSTFY3z&o?G$gM!&HASjZe95qufEQti}F!9Tak3M=hOSlFdd__aWk{Q;Ddo7|{ZR4Q0VQ9zoDDg!M*@TiouV2TDuDEM?u ziZZ9bcv)%l-WaP=$x1!73%+I?M8|@2ni}8VNd`YTO_j`lw)-zp9=Q>>6sT8a-BmE? z1&Ev9QCP?7VYfQI#6d|UDPSBJsIKPb2h~)L&j`soxBesR2_>BosxW%kziMO}-La!u^o zq3clb?W_;~j?SKC(Ol96Tf^urErh0{1K_v=ikzxJHU@u?Ow;HCksJvC;Tw@6MLyFk zv5Zjh>8}^>zjNRrX=Yq$l4GVe4%0Q$_QzdtXCrR~DvZ40S%zu45VEj^1`5O#BWIE} z6eoVP7+r+$fK$s|4& zl}Wo?!;^N(NT=8;so7vZ{PRJq>X;-Op6-jsa4U$nVKSThak6cwuB)y%UtMEq8??bY z(Wf7bcT?en<`fId(uDP$X;g-;j*lTWD9pg73Dz{YkW0eZXUl%93=6h|KOsResg5pe z+rW^Sje{<@sABq1lCIDolDR!IlU2}H8Q&wxdub9rG~L&jU)$)tfwug1UEN{ZVQe(I zO!2LW$LUsc+Z?m(5b%w6w{7qtTC^luwi#LGFTo5PfWFSkL9F5V7psB7vn@ba(40W` z{}3}RM=TF4#QSY@+M+G`O<2r$vS6~9y4Ky8aA&`{|KH%@G&CLGc@*B7IpB1S^9CpR zsZsqR%dli%=R;aNAJ?q*Hoak3Xx(#bL@%!QW|qM&ToKiVJ)zRX@-?H9$aJ+q**#~s ztRE0==|AwCsy?%(Xt*u{3eS-##EV; z%{k6`#^)i+%)3?-i!UWB>UeZSaVMkh{qx1y%{m&v0|?tX9>B;xyjoT)yPi&Qkog6_ z-6Jm?yWkotaGYA|6+}x7Pk%sYAv8*3Ro%FGas2q9ec^`+_a5%Hrhrk>? zCUTP$x~y5*!lRp#M*bmbY1#x5ZoYe9v1OzM8IG^sX#cq_2(Gzrb9T_H|EYF*?X6rh z&D*DBpXq-2A!yj)yj4qgG4RsbpIUAPa>1`?48y3Jak&90vShc+!69de2E*itPFlIr zCiPByc1*D4>H+4L2WxQ49i&8KV|51(FQolxv5f4L8B%l5xO(gkW9>!eyDQ4=r#>YH zH6IcqkKgGUYp=Se**X0?dJfUyMv}@L{k*!KqHhv=A~^jo3D?HWd~d-%8g(q#;_SZF z4ceqg#D)H9pJ^w}Apo6=rbP{uj%}ISwYauy+r~T=n*0DJgKn`;^n3&{OB5nL+>>H~Xoe#FBVf#@aU8M5m=k<9LF zUf0J}HgvlHWJwsFQJWhBa90NKiD4rv1YT5e zAeCOlfs@g>eF%a>NRenF@x(S@sdHiCq`10)1V(%DNZ7sSD%~yy8WZDA8@T-5PhQ~W;&l>yATEX74)Zn0khvlB5hk$o1%;nwv@F8HAR2Gbz8zh5E cbM+EnPr#c4b9uLW!_t|WJLhTWpQnR=0WVGAlmGw# literal 11986 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 -CsvSummaryFile # # The above example imports new mail public folders objects from Exchange Online as sync mail public folders to on-premise. -# -# .DESCRIPTION -# -# Copyright (c) 2016 Microsoft Corporation. All rights reserved. -# -# THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK -# OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER. param ( [Parameter(Mandatory=$false)] [PSCredential] $Credential, @@ -26,27 +22,24 @@ param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $CsvSummaryFile - ) +) # Writes a dated information message to console -function WriteInfoMessage() -{ +function WriteInfoMessage() { param ($message) - Write-Host "[$($(Get-Date).ToString())]" $message; + Write-Host "[$($(Get-Date).ToString())]" $message } # Writes an error importing a mail public folder to the CSV summary -function WriteErrorSummary() -{ +function WriteErrorSummary() { param ($folder, $operation, $errorMessage, $commandtext) - WriteOperationSummary $folder.Guid $operation $errorMessage $commandtext; - $script:errorsEncountered++; + WriteOperationSummary -folder $folder.Guid -operation $operation -result $errorMessage -commandtext $commandtext + $script:errorsEncountered++ } # Writes the operation executed and its result to the output CSV -function WriteOperationSummary() -{ +function WriteOperationSummary() { param ($folder, $operation, $result, $commandtext) $columns = @( @@ -55,496 +48,422 @@ function WriteOperationSummary() $operation, (EscapeCsvColumn $result), (EscapeCsvColumn $commandtext) - ); + ) - Add-Content $CsvSummaryFile -Value ("{0},{1},{2},{3},{4}" -f $columns); + Add-Content $CsvSummaryFile -Value ("{0},{1},{2},{3},{4}" -f $columns) } #Escapes a column value based on RFC 4180 (http://tools.ietf.org/html/rfc4180) -function EscapeCsvColumn() -{ +function EscapeCsvColumn() { param ([string]$text) - if ($text -eq $null) - { - return $text; + if ($text -eq $null) { + return $text } - $hasSpecial = $false; - for ($i=0; $i -lt $text.Length; $i++) - { - $c = $text[$i]; + $hasSpecial = $false + for ($i=0; $i -lt $text.Length; $i++) { + $c = $text[$i] if ($c -eq $script:csvEscapeChar -or $c -eq $script:csvFieldDelimiter -or - $script:csvSpecialChars -contains $c) - { - $hasSpecial = $true; - break; + $script:csvSpecialChars -contains $c) { + $hasSpecial = $true + break } } - if (-not $hasSpecial) - { - return $text; + if (-not $hasSpecial) { + return $text } - $ch = $script:csvEscapeChar.ToString([System.Globalization.CultureInfo]::InvariantCulture); - return $ch + $text.Replace($ch, $ch + $ch) + $ch; + $ch = $script:csvEscapeChar.ToString([System.Globalization.CultureInfo]::InvariantCulture) + return $ch + $text.Replace($ch, $ch + $ch) + $ch } ## Create a tenant PSSession against Exchange Online using modern auth. -function InitializeExchangeOnlineRemoteSession() -{ - WriteInfoMessage $LocalizedStrings.CreatingRemoteSession; - - $oldWarningPreference = $WarningPreference; - $oldVerbosePreference = $VerbosePreference; - - try - { - Import-Module ExchangeOnlineManagement -ErrorAction SilentlyContinue; - if (Get-Module ExchangeOnlineManagement) - { - $sessionOption = (New-PSSessionOption -SkipCACheck); - Connect-ExchangeOnline -Credential $Credential -ConnectionURI $ConnectionUri -PSSessionOption $sessionOption -Prefix "Remote" -ErrorAction SilentlyContinue; - $script:isConnectedToExchangeOnline = $true; - } - else - { - Write-Warning $LocalizedStrings.EXOV2ModuleNotInstalled; - Exit; - } - } - catch - { - Write-Error "Error message: $($_.Exception.Message)"; - WriteInfoMessage ($LocalizedStrings.ConnectExchangeOnlineFailure); - Exit; +function InitializeExchangeOnlineRemoteSession() { + WriteInfoMessage $LocalizedStrings.CreatingRemoteSession + + try { + Import-Module ExchangeOnlineManagement -ErrorAction SilentlyContinue + if (Get-Module ExchangeOnlineManagement) { + $sessionOption = (New-PSSessionOption -SkipCACheck) + Connect-ExchangeOnline -Credential $Credential -ConnectionURI $ConnectionUri -PSSessionOption $sessionOption -Prefix "Remote" -ErrorAction SilentlyContinue + $script:isConnectedToExchangeOnline = $true + } else { + Write-Warning $LocalizedStrings.EXOV2ModuleNotInstalled + exit + } + } catch { + Write-Error "Error message: $($_.Exception.Message)" + WriteInfoMessage ($LocalizedStrings.ConnectExchangeOnlineFailure) + exit } - WriteInfoMessage ($LocalizedStrings.RemoteSessionCreatedSuccessfully); + WriteInfoMessage ($LocalizedStrings.RemoteSessionCreatedSuccessfully) } ## Formats the command and its parameters to be printed on console or to file -function FormatCommand() -{ +function FormatCommand() { param ([string]$command, [System.Collections.IDictionary]$parameters) - $commandText = New-Object System.Text.StringBuilder; - [void]$commandText.Append($command); - foreach ($name in $parameters.Keys) - { - [void]$commandText.AppendFormat(" -{0}:",$name); - - $value = $parameters[$name]; - if ($value -isnot [Array]) - { - [void]$commandText.AppendFormat("`"{0}`"", $value); - } - elseif ($value.Length -eq 0) - { - [void]$commandText.Append("@()"); - } - else - { - [void]$commandText.Append("@("); - foreach ($subValue in $value) - { - [void]$commandText.AppendFormat("`"{0}`",",$subValue); + $commandText = New-Object System.Text.StringBuilder + [void]$commandText.Append($command) + foreach ($name in $parameters.Keys) { + [void]$commandText.AppendFormat(" -{0}:", $name) + + $value = $parameters[$name] + if ($value -isnot [Array]) { + [void]$commandText.AppendFormat("`"{0}`"", $value) + } elseif ($value.Length -eq 0) { + [void]$commandText.Append("@()") + } else { + [void]$commandText.Append("@(") + foreach ($subValue in $value) { + [void]$commandText.AppendFormat("`"{0}`",", $subValue) } - [void]$commandText.Remove($commandText.Length - 1, 1); - [void]$commandText.Append(")"); + [void]$commandText.Remove($commandText.Length - 1, 1) + [void]$commandText.Append(")") } } - return $commandText.ToString(); + return $commandText.ToString() } ## Get external email address domains which are not configured on the on-premise connector -function GetMissingDomainsFromConnector() -{ +function GetMissingDomainsFromConnector() { param($sendConnectorDomains) - $missingDomains = @(); + $missingDomains = @() - foreach ($domain in $script:ExternalEmailAddressDomains.Keys) - { - if (-not $sendConnectorDomains.ContainsKey($domain)) - { - $missingDomains += $domain; + foreach ($domain in $script:ExternalEmailAddressDomains.Keys) { + if (-not $sendConnectorDomains.ContainsKey($domain)) { + $missingDomains += $domain } } - return $missingDomains; + return $missingDomains } ## Check if the centralized transport feature is enabled -function IsCentralizedTransportEnabled() -{ +function IsCentralizedTransportEnabled() { $hybridConf = Get-HybridConfiguration - if ($hybridConf -ne $null -and $hybridConf.Features -ne $null -and $hybridConf.Features.Contains("CentralizedTransport")) - { - return $true; + if ($null -ne $hybridConf -and $null -ne $hybridConf.Features -and $hybridConf.Features.Contains("CentralizedTransport")) { + return $true } - return $false; + return $false } ## Get send connector (on-premise) domains configured for O365 -function GetSendConnectorDomains() -{ - $connector = Get-SendConnector -Identity $script:SendConnectorToO365 -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue; - $domains = @{}; - - if ($connector -ne $null -and $connector.AddressSpaces -ne $null) - { - foreach ($addressSpace in $connector.AddressSpaces) - { - $domain = $addressSpace.Address.ToString(); - - if(-not $domains.ContainsKey($domain)) - { - $domains.Add($domain, $false); +function GetSendConnectorDomains() { + $connector = Get-SendConnector -Identity $script:SendConnectorToO365 -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue + $domains = @{} + + if ($null -ne $connector -and $null -ne $connector.AddressSpaces) { + foreach ($addressSpace in $connector.AddressSpaces) { + $domain = $addressSpace.Address.ToString() + + if (-not $domains.ContainsKey($domain)) { + $domains.Add($domain, $false) } } } - return $domains; + return $domains } ## Store distinct domains used in the external email addresses function StoreExternalEmailAddressDomain( - [string] $emailAddress) -{ - if (-not [string]::IsNullOrEmpty($emailAddress)) - { + [string] $emailAddress) { + if (-not [string]::IsNullOrEmpty($emailAddress)) { $index = $emailAddress.IndexOf('@') $domain = $emailAddress.Substring($index + 1) - if (-not $script:ExternalEmailAddressDomains.ContainsKey($domain)) - { + if (-not $script:ExternalEmailAddressDomains.ContainsKey($domain)) { $script:ExternalEmailAddressDomains.Add($domain, $false) } } } ## Concatenate a list of domains -function ConcatDomains() -{ +function ConcatDomains() { param($domainList) - $domains = $script:separator; + $domains = $script:separator $domains += $domainList -join $script:separator - return $domains; + return $domains } ## Get external email address from an EXO mail public folder -function GetExternalEmailAddress() -{ +function GetExternalEmailAddress() { param ($remotePublicFolder) - $primarySmtpAddress = $remotePublicFolder.PrimarySmtpAddress.ToString(); + $primarySmtpAddress = $remotePublicFolder.PrimarySmtpAddress.ToString() - if ($primarySmtpAddress.EndsWith($script:OnMicrosoftDomain, [StringComparison]::InvariantCultureIgnoreCase)) - { - return $primarySmtpAddress; + if ($primarySmtpAddress.EndsWith($script:OnMicrosoftDomain, [StringComparison]::InvariantCultureIgnoreCase)) { + return $primarySmtpAddress } - $externalEmailAddress = $primarySmtpAddress; + $externalEmailAddress = $primarySmtpAddress $primarySmtpAddressParts = $primarySmtpAddress.Split($script:proxyAddressSeparators); # alias@domain - if ($remotePublicFolder.EmailAddresses -ne $null -and $remotePublicFolder.EmailAddresses.Count -gt 0) - { - foreach ($address in $remotePublicFolder.EmailAddresses) - { - if ($address.StartsWith($script:SmtpPrefix, [StringComparison]::InvariantCultureIgnoreCase)) - { + if ($null -ne $remotePublicFolder.EmailAddresses -and $remotePublicFolder.EmailAddresses.Count -gt 0) { + foreach ($address in $remotePublicFolder.EmailAddresses) { + if ($address.StartsWith($script:SmtpPrefix, [StringComparison]::InvariantCultureIgnoreCase)) { $addressParts = $address.Split($script:proxyAddressSeparators); # smtp:alias@domain if ($addressParts.Count -eq 3 -and $addressParts[1].Equals($primarySmtpAddressParts[0], [StringComparison]::InvariantCultureIgnoreCase) -and - $addressParts[2].EndsWith($script:OnMicrosoftDomain, [StringComparison]::InvariantCultureIgnoreCase)) - { - return (RemoveSmtpPrefix $address); + $addressParts[2].EndsWith($script:OnMicrosoftDomain, [StringComparison]::InvariantCultureIgnoreCase)) { + return (RemoveSmtpPrefix $address) } } } } - return $externalEmailAddress; + return $externalEmailAddress } ## Remove smtp prefix from the email address -function RemoveSmtpPrefix() -{ +function RemoveSmtpPrefix() { param ($emailAddress) - if ([String]::IsNullOrEmpty($emailAddress)) - { - return [String]::Empty; + if ([String]::IsNullOrEmpty($emailAddress)) { + return [String]::Empty } - if ($emailAddress.StartsWith($script:SmtpPrefix, [StringComparison]::InvariantCultureIgnoreCase)) - { - return $emailAddress.Substring($script:SmtpPrefixLength); + if ($emailAddress.StartsWith($script:SmtpPrefix, [StringComparison]::InvariantCultureIgnoreCase)) { + return $emailAddress.Substring($script:SmtpPrefixLength) } - return $emailAddress; + return $emailAddress } ## Retrieve mail enabled public folders from EXO -function GetRemoteMailPublicFolders() -{ - $mailPublicFolders = Get-RemoteMailPublicFolder -ResultSize:Unlimited -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue; +function GetRemoteMailPublicFolders() { + $mailPublicFolders = Get-RemoteMailPublicFolder -ResultSize:Unlimited -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue # Return the results - if ($mailPublicFolders -eq $null -or ([array]($mailPublicFolders)).Count -eq 0) - { - return $null; - } + if ($null -eq $mailPublicFolders -or ([array]($mailPublicFolders)).Count -eq 0) { + return $null + } - return $mailPublicFolders; + return $mailPublicFolders } #Get list of MEPF's in OnPrem to be deleted function GetFoldersToMailDisable( [object[]] $localMailPublicFolders, - [hashtable] $validExternalEmailAddresses) -{ - $foldersToMailDisable = @(); - foreach($syncPublicFolder in $localMailPublicFolders) - { - $localExternalEmailAddress = [String]::Empty; - - if ($syncPublicFolder.ExternalEmailAddress -ne $null) - { - $localExternalEmailAddress = RemoveSmtpPrefix $syncPublicFolder.ExternalEmailAddress.ToString(); + [hashtable] $validExternalEmailAddresses) { + $foldersToMailDisable = @() + foreach ($syncPublicFolder in $localMailPublicFolders) { + $localExternalEmailAddress = [String]::Empty + + if ($null -ne $syncPublicFolder.ExternalEmailAddress) { + $localExternalEmailAddress = RemoveSmtpPrefix $syncPublicFolder.ExternalEmailAddress.ToString() } - if (-not $validExternalEmailAddresses.ContainsKey($localExternalEmailAddress)) - { - $foldersToMailDisable += $syncPublicFolder.Identity; - } - } - return $foldersToMailDisable + if (-not $validExternalEmailAddresses.ContainsKey($localExternalEmailAddress)) { + $foldersToMailDisable += $syncPublicFolder.Identity + } + } + return $foldersToMailDisable } ## Sync mail public folders from cloud to on-premise. -function SyncMailPublicFolders( - [object[]] $mailPublicFolders) -{ - $validExternalEmailAddresses = @{}; - - if ($mailPublicFolders -ne $null) - { - foreach ($mailPublicFolder in $mailPublicFolders) - { +function SyncMailPublicFolders { + [CmdletBinding(SupportsShouldProcess)] + param( + [object[]] $mailPublicFolders + ) + + $validExternalEmailAddresses = @{} + + if ($null -ne $mailPublicFolders) { + foreach ($mailPublicFolder in $mailPublicFolders) { # extracting properties - $alias = $mailPublicFolder.Alias.Trim(); - $identity = $mailPublicFolder.PrimarySmtpAddress.ToString(); - $externalEmailAddress = GetExternalEmailAddress $mailPublicFolder; - $entryId = $mailPublicFolder.EntryId.ToString(); - $name = $mailPublicFolder.Name.Trim(); + $alias = $mailPublicFolder.Alias.Trim() + $identity = $mailPublicFolder.PrimarySmtpAddress.ToString() + $externalEmailAddress = GetExternalEmailAddress $mailPublicFolder + $entryId = $mailPublicFolder.EntryId.ToString() + $name = $mailPublicFolder.Name.Trim() $displayName = $mailPublicFolder.DisplayName.Trim() - $hiddenFromAddressListsEnabled = $mailPublicFolder.HiddenFromAddressListsEnabled; + $hiddenFromAddressListsEnabled = $mailPublicFolder.HiddenFromAddressListsEnabled - $windowsEmailAddress = $mailPublicFolder.WindowsEmailAddress.ToString(); - if ($windowsEmailAddress -eq "") - { - $windowsEmailAddress = $externalEmailAddress; + $windowsEmailAddress = $mailPublicFolder.WindowsEmailAddress.ToString() + if ($windowsEmailAddress -eq "") { + $windowsEmailAddress = $externalEmailAddress } # extracting all the EmailAddress - $emailAddress = @(); - foreach($address in $mailPublicFolder.EmailAddresses) - { - $emailAddress += $address.ToString(); + $emailAddress = @() + foreach ($address in $mailPublicFolder.EmailAddresses) { + $emailAddress += $address.ToString() } # preserve the ability to reply via Outlook's nickname cache post-migration - $x500Proxy = ("X500:" + $mailPublicFolder.LegacyExchangeDN); - if ($x500Proxy -notin $emailAddress) - { - $emailAddress += $x500Proxy; + $x500Proxy = ("X500:" + $mailPublicFolder.LegacyExchangeDN) + if ($x500Proxy -notin $emailAddress) { + $emailAddress += $x500Proxy } - WriteInfoMessage ($LocalizedStrings.SyncingMailPublicFolder -f $alias); + WriteInfoMessage ($LocalizedStrings.SyncingMailPublicFolder -f $alias) - $syncPublicFolder = Get-MailPublicFolder -Identity $identity -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue; + $syncPublicFolder = Get-MailPublicFolder -Identity $identity -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue - if ($syncPublicFolder -eq $null) - { - WriteInfoMessage ($LocalizedStrings.CreatingSyncMailPublicFolder -f $alias); - try - { - $newParams = @{}; - $newParams.Add("Name", $name); - $newParams.Add("ExternalEmailAddress", $externalEmailAddress); - $newParams.Add("Alias", $alias); - $newParams.Add("EntryId", $entryId); - $newParams.Add("WindowsEmailAddress", $windowsEmailAddress); - $newParams.Add("WarningAction", "SilentlyContinue"); - $newParams.Add("ErrorAction", "Stop"); + if ($null -eq $syncPublicFolder) { + WriteInfoMessage ($LocalizedStrings.CreatingSyncMailPublicFolder -f $alias) + try { + $newParams = @{} + $newParams.Add("Name", $name) + $newParams.Add("ExternalEmailAddress", $externalEmailAddress) + $newParams.Add("Alias", $alias) + $newParams.Add("EntryId", $entryId) + $newParams.Add("WindowsEmailAddress", $windowsEmailAddress) + $newParams.Add("WarningAction", "SilentlyContinue") + $newParams.Add("ErrorAction", "Stop") - [string]$createSyncPublicFolder = (FormatCommand $script:NewSyncMailPublicFolderCommand $newParams); + [string]$createSyncPublicFolder = (FormatCommand $script:NewSyncMailPublicFolderCommand $newParams) # Creating new sync mail public folder - $newMailPublicFolder = &$script:NewSyncMailPublicFolderCommand @newParams; + $null = &$script:NewSyncMailPublicFolderCommand @newParams - WriteOperationSummary $mailPublicFolder $LocalizedStrings.CreateOperationName $LocalizedStrings.CsvSuccessResult $createSyncPublicFolder; + WriteOperationSummary -folder $mailPublicFolder -operation $LocalizedStrings.CreateOperationName -result $LocalizedStrings.CsvSuccessResult -commandtext $createSyncPublicFolder - $setParams = @{}; - $setParams.Add("Identity", $name); - $setParams.Add("EmailAddresses", $emailAddress); - $setParams.Add("DisplayName", $displayName); - $setParams.Add("HiddenFromAddressListsEnabled", $hiddenFromAddressListsEnabled); - $setParams.Add("WarningAction", "SilentlyContinue"); - $setParams.Add("ErrorAction", "Stop"); - $setParams.Add("EmailAddressPolicyEnabled", $false); + $setParams = @{} + $setParams.Add("Identity", $name) + $setParams.Add("EmailAddresses", $emailAddress) + $setParams.Add("DisplayName", $displayName) + $setParams.Add("HiddenFromAddressListsEnabled", $hiddenFromAddressListsEnabled) + $setParams.Add("WarningAction", "SilentlyContinue") + $setParams.Add("ErrorAction", "Stop") + $setParams.Add("EmailAddressPolicyEnabled", $false) - [string]$setOtherProperties = (FormatCommand $script:SetMailPublicFolderCommand $setParams); + [string]$setOtherProperties = (FormatCommand $script:SetMailPublicFolderCommand $setParams) # Setting other properties to the newly created sync mail public folder - &$script:SetMailPublicFolderCommand @setParams; + &$script:SetMailPublicFolderCommand @setParams - WriteOperationSummary $mailPublicFolder $LocalizedStrings.SetOperationName $LocalizedStrings.CsvSuccessResult $setOtherProperties; + WriteOperationSummary -folder $mailPublicFolder -operation $LocalizedStrings.SetOperationName -result $LocalizedStrings.CsvSuccessResult -commandtext $setOtherProperties - $validExternalEmailAddresses.Add($externalEmailAddress, $false); - $script:CreatedPublicFoldersCount++; + $validExternalEmailAddresses.Add($externalEmailAddress, $false) + $script:CreatedPublicFoldersCount++ } - catch - { - WriteErrorSummary $mailPublicFolder $LocalizedStrings.CreateOperationName $_.Exception.Message ""; - Write-Error $_; + catch { + WriteErrorSummary -folder $mailPublicFolder -operation $LocalizedStrings.CreateOperationName -errorMessage $_.Exception.Message -commandtext "" + Write-Error $_ } } - else - { - WriteInfoMessage ($LocalizedStrings.UpdatingSyncMailPublicFolder -f $syncPublicFolder); - try - { - $updateParams = @{}; - $updateParams.Add("Identity", $syncPublicFolder); - $updateParams.Add("EmailAddresses", $emailAddress); - $updateParams.Add("HiddenFromAddressListsEnabled", $hiddenFromAddressListsEnabled); - $updateParams.Add("DisplayName", $displayName); - $updateParams.Add("Name", $name); - $updateParams.Add("ExternalEmailAddress", $externalEmailAddress); - $updateParams.Add("Alias", $alias); - $updateParams.Add("WindowsEmailAddress", $windowsEmailAddress); - $updateParams.Add("WarningAction", "SilentlyContinue"); - $updateParams.Add("ErrorAction", "Stop"); - - [string]$updateProperties = (FormatCommand $script:SetMailPublicFolderCommand $updateParams); + else { + WriteInfoMessage ($LocalizedStrings.UpdatingSyncMailPublicFolder -f $syncPublicFolder) + try { + $updateParams = @{} + $updateParams.Add("Identity", $syncPublicFolder) + $updateParams.Add("EmailAddresses", $emailAddress) + $updateParams.Add("HiddenFromAddressListsEnabled", $hiddenFromAddressListsEnabled) + $updateParams.Add("DisplayName", $displayName) + $updateParams.Add("Name", $name) + $updateParams.Add("ExternalEmailAddress", $externalEmailAddress) + $updateParams.Add("Alias", $alias) + $updateParams.Add("WindowsEmailAddress", $windowsEmailAddress) + $updateParams.Add("WarningAction", "SilentlyContinue") + $updateParams.Add("ErrorAction", "Stop") + + [string]$updateProperties = (FormatCommand $script:SetMailPublicFolderCommand $updateParams) # Setting properties to the existing sync mail public folder - &$script:SetMailPublicFolderCommand @updateParams; - - WriteOperationSummary $mailPublicFolder $LocalizedStrings.UpdateOperationName $LocalizedStrings.CsvSuccessResult $updateProperties; + &$script:SetMailPublicFolderCommand @updateParams - $validExternalEmailAddresses.Add($externalEmailAddress, $false); - $script:UpdatedPublicFoldersCount++; - } + WriteOperationSummary -folder $mailPublicFolder -operation $LocalizedStrings.UpdateOperationName -result $LocalizedStrings.CsvSuccessResult -commandtext $updateProperties - catch - { - WriteErrorSummary $mailPublicFolder $LocalizedStrings.UpdateOperationName $_.Exception.Message $updateProperties; - Write-Error $_; - } + $validExternalEmailAddresses.Add($externalEmailAddress, $false) + $script:UpdatedPublicFoldersCount++ + } + catch { + WriteErrorSummary -folder $mailPublicFolder -operation $LocalizedStrings.UpdateOperationName -errorMessage $_.Exception.Message -commandtext $updateProperties + Write-Error $_ + } } - StoreExternalEmailAddressDomain $externalEmailAddress; - WriteInfoMessage ($LocalizedStrings.DoneSyncingMailPublicFolder -f $alias); - Write-Host ""; + StoreExternalEmailAddressDomain $externalEmailAddress + WriteInfoMessage ($LocalizedStrings.DoneSyncingMailPublicFolder -f $alias) + Write-Host "" } } - else - { - WriteInfoMessage ($LocalizedStrings.NoMailPublicFoldersToSync); - Write-Host ""; + else { + WriteInfoMessage ($LocalizedStrings.NoMailPublicFoldersToSync) + Write-Host "" } - WriteInfoMessage ($LocalizedStrings.DeleteSyncMailPublicFolderTitle); + WriteInfoMessage ($LocalizedStrings.DeleteSyncMailPublicFolderTitle) - $localMailPublicFolders = Get-MailPublicFolder -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue; + $localMailPublicFolders = Get-MailPublicFolder -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue - $foldersToMailDisable = GetFoldersToMailDisable $localMailPublicFolders $validExternalEmailAddresses; - if ($foldersToMailDisable -eq $null) - { - return; - } + $foldersToMailDisable = GetFoldersToMailDisable $localMailPublicFolders $validExternalEmailAddresses + if ($null -eq $foldersToMailDisable) { + return + } $foldersToMailDisableFile = Join-Path $PWD "FoldersToMailDisable.txt" Set-Content -Path $foldersToMailDisableFile -Value $foldersToMailDisable - do - { - $confirmation = Read-Host ($LocalizedStrings.FoldersToMailDisableConfirmation -f $foldersToMailDisableFile); - } - while ($confirmation -ne "Y" -and $confirmation -ne "y" -and $confirmation -ne "N" -and $confirmation -ne "n") - if ($confirmation -eq "N" -or $confirmation -eq "n") - { - return; + if (-not $PSCmdlet.ShouldProcess($LocalizedStrings.FoldersToMailDisableConfirmation -f $foldersToMailDisableFile)) { + return } - foreach($syncPublicFolder in $localMailPublicFolders) - { - $localExternalEmailAddress = [String]::Empty; + foreach ($syncPublicFolder in $localMailPublicFolders) { + $localExternalEmailAddress = [String]::Empty - if ($syncPublicFolder.ExternalEmailAddress -ne $null) - { - $localExternalEmailAddress = RemoveSmtpPrefix $syncPublicFolder.ExternalEmailAddress.ToString(); + if ($null -ne $syncPublicFolder.ExternalEmailAddress) { + $localExternalEmailAddress = RemoveSmtpPrefix $syncPublicFolder.ExternalEmailAddress.ToString() } - if (-not $validExternalEmailAddresses.ContainsKey($localExternalEmailAddress)) - { - WriteInfoMessage ($LocalizedStrings.DeletingSyncMailPublicFolder -f $syncPublicFolder); - try - { - $deleteParams = @{}; - $deleteParams.Add("Identity", $syncPublicFolder); - $deleteParams.Add("Confirm", $false); - [string]$disableMailPublicFolder = (FormatCommand $script:DeletePublicFolderCommand $deleteParams); + if (-not $validExternalEmailAddresses.ContainsKey($localExternalEmailAddress)) { + WriteInfoMessage ($LocalizedStrings.DeletingSyncMailPublicFolder -f $syncPublicFolder) + try { + $deleteParams = @{} + $deleteParams.Add("Identity", $syncPublicFolder) + $deleteParams.Add("Confirm", $false) + [string]$disableMailPublicFolder = (FormatCommand $script:DeletePublicFolderCommand $deleteParams) # Deleting sync mail public folder - &$script:DeletePublicFolderCommand @deleteParams; + &$script:DeletePublicFolderCommand @deleteParams - WriteOperationSummary $syncPublicFolder $LocalizedStrings.DeleteOperationName $LocalizedStrings.CsvSuccessResult $disableMailPublicFolder; - $script:RemovedPublicFoldersCount++; - } - catch - { - WriteErrorSummary $syncPublicFolder $LocalizedStrings.DeleteOperationName $_.Exception.Message $disableMailPublicFolder; - Write-Error $_; + WriteOperationSummary -folder $syncPublicFolder -operation $LocalizedStrings.DeleteOperationName -result $LocalizedStrings.CsvSuccessResult -commandtext $disableMailPublicFolder + $script:RemovedPublicFoldersCount++ + } catch { + WriteErrorSummary -folder $syncPublicFolder -operation $LocalizedStrings.DeleteOperationName -errorMessage $_.Exception.Message -commandtext $disableMailPublicFolder + Write-Error $_ } } } } ################ DECLARING GLOBAL VARIABLES ################ -$script:session = $null; - -$script:csvSpecialChars = @("`r", "`n"); -$script:csvEscapeChar = '"'; -$script:csvFieldDelimiter = ','; -$script:separator = "`n`t"; -$script:NewSyncMailPublicFolderCommand = "New-SyncMailPublicFolder"; -$script:SetMailPublicFolderCommand = "Set-MailPublicFolder"; -$script:DeletePublicFolderCommand = "Disable-MailPublicFolder"; -$script:CreatedPublicFoldersCount = 0; -$script:UpdatedPublicFoldersCount = 0; -$script:RemovedPublicFoldersCount = 0; -$script:ExternalEmailAddressDomains = @{}; -$script:SendConnectorToO365 = "Outbound to Office 365"; -$script:OnMicrosoftDomain = ".onmicrosoft.com"; -$script:SmtpPrefix = "smtp:"; -$script:SmtpPrefixLength = $script:SmtpPrefix.Length; -[char[]]$script:proxyAddressSeparators = ':','@'; +$script:session = $null + +$script:csvSpecialChars = @("`r", "`n") +$script:csvEscapeChar = '"' +$script:csvFieldDelimiter = ',' +$script:separator = "`n`t" +$script:NewSyncMailPublicFolderCommand = "New-SyncMailPublicFolder" +$script:SetMailPublicFolderCommand = "Set-MailPublicFolder" +$script:DeletePublicFolderCommand = "Disable-MailPublicFolder" +$script:CreatedPublicFoldersCount = 0 +$script:UpdatedPublicFoldersCount = 0 +$script:RemovedPublicFoldersCount = 0 +$script:ExternalEmailAddressDomains = @{} +$script:SendConnectorToO365 = "Outbound to Office 365" +$script:OnMicrosoftDomain = ".onmicrosoft.com" +$script:SmtpPrefix = "smtp:" +$script:SmtpPrefixLength = $script:SmtpPrefix.Length +[char[]]$script:proxyAddressSeparators = ':', '@' #load hashtable of localized string Import-LocalizedData -BindingVariable LocalizedStrings -FileName SyncMailPublicFoldersCloudToOnprem.strings.psd1 @@ -553,65 +472,58 @@ Import-LocalizedData -BindingVariable LocalizedStrings -FileName SyncMailPublicF $minSupportedVersion = 15 ################ END OF DECLARATION ######################### -if (Test-Path $CsvSummaryFile) -{ - Remove-Item $CsvSummaryFile -Confirm:$Confirm -Force; +if (Test-Path $CsvSummaryFile) { + Remove-Item $CsvSummaryFile -Confirm:$Confirm -Force } # Write the output CSV headers -$csvFile = New-Item -Path $CsvSummaryFile -ItemType File -Force -ErrorAction:Stop -Value ("#{0},{1},{2},{3},{4}`r`n" -f $LocalizedStrings.TimestampCsvHeader, +$null = New-Item -Path $CsvSummaryFile -ItemType File -Force -ErrorAction:Stop -Value ("#{0},{1},{2},{3},{4}`r`n" -f $LocalizedStrings.TimestampCsvHeader, $LocalizedStrings.IdentityCsvHeader, $LocalizedStrings.OperationCsvHeader, $LocalizedStrings.ResultCsvHeader, - $LocalizedStrings.CommandCsvHeader); + $LocalizedStrings.CommandCsvHeader) -$localServerVersion = (Get-ExchangeServer $env:COMPUTERNAME -ErrorAction:Stop).AdminDisplayVersion; +$localServerVersion = (Get-ExchangeServer $env:COMPUTERNAME -ErrorAction:Stop).AdminDisplayVersion # This script can run from Exchange 2013 Management shell and above -if ($localServerVersion.Major -lt $minSupportedVersion) -{ - Write-Error ($LocalizedStrings.LocalServerVersionNotSupported -f $localServerVersion) -ErrorAction:Continue; - Exit; +if ($localServerVersion.Major -lt $minSupportedVersion) { + Write-Error ($LocalizedStrings.LocalServerVersionNotSupported -f $localServerVersion) -ErrorAction:Continue + exit } # Create a PSSession -WriteInfoMessage ($LocalizedStrings.CreatingRemoteSession); +WriteInfoMessage ($LocalizedStrings.CreatingRemoteSession) -InitializeExchangeOnlineRemoteSession; +InitializeExchangeOnlineRemoteSession # Get mail enabled public folders in cloud -WriteInfoMessage ($LocalizedStrings.StartedImportingMailPublicFolders); -Write-Host ""; +WriteInfoMessage ($LocalizedStrings.StartedImportingMailPublicFolders) +Write-Host "" -$mailPublicFoldersEXO = GetRemoteMailPublicFolders; +$mailPublicFoldersEXO = GetRemoteMailPublicFolders # Create sync mail public folders in on-premise -SyncMailPublicFolders $mailPublicFoldersEXO; +SyncMailPublicFolders $mailPublicFoldersEXO -Write-Host ""; -WriteInfoMessage ($LocalizedStrings.CompletedImportingMailPublicFolders); -WriteInfoMessage ($LocalizedStrings.CompletedStatsCount -f $script:CreatedPublicFoldersCount, $script:UpdatedPublicFoldersCount, $script:RemovedPublicFoldersCount); -Write-Host ""; +Write-Host "" +WriteInfoMessage ($LocalizedStrings.CompletedImportingMailPublicFolders) +WriteInfoMessage ($LocalizedStrings.CompletedStatsCount -f $script:CreatedPublicFoldersCount, $script:UpdatedPublicFoldersCount, $script:RemovedPublicFoldersCount) +Write-Host "" -if ((IsCentralizedTransportEnabled) -and $script:ExternalEmailAddressDomains.Count -gt 0) -{ - $sendConnectorDomains = GetSendConnectorDomains; +if ((IsCentralizedTransportEnabled) -and $script:ExternalEmailAddressDomains.Count -gt 0) { + $sendConnectorDomains = GetSendConnectorDomains - if ($sendConnectorDomains -eq $null -or $sendConnectorDomains.Count -eq 0) - { - $domains = ConcatDomains $script:ExternalEmailAddressDomains.Keys; - Write-Warning ($LocalizedStrings.VerifyConnectorAcceptedDomainsMessage -f $domains); - } - else - { - $missingDomains = GetMissingDomainsFromConnector $sendConnectorDomains; - - if ($missingDomains -ne $null -and $missingDomains.Count -gt 0) - { - $domains = ConcatDomains $missingDomains; - Write-Warning ($LocalizedStrings.VerifyConnectorAcceptedDomainsMessage -f $domains); + if ($null -eq $sendConnectorDomains -or $sendConnectorDomains.Count -eq 0) { + $domains = ConcatDomains $script:ExternalEmailAddressDomains.Keys + Write-Warning ($LocalizedStrings.VerifyConnectorAcceptedDomainsMessage -f $domains) + } else { + $missingDomains = GetMissingDomainsFromConnector $sendConnectorDomains + + if ($null -ne $missingDomains -and $missingDomains.Count -gt 0) { + $domains = ConcatDomains $missingDomains + Write-Warning ($LocalizedStrings.VerifyConnectorAcceptedDomainsMessage -f $domains) } } } # Terminate the PSSession -Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue; +Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue From 357215674e52d6f4957d1805eb8e1b998be27650 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 17 Mar 2025 15:56:33 -0500 Subject: [PATCH 03/10] CSpell fixes --- .../Hybrid/Import-PublicFolderMailboxes.ps1 | 2 ++ .../Sync-MailPublicFoldersCloudToOnprem.ps1 | 28 ++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 index c7279c1209..b7e54c9ed0 100644 --- a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 +++ b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 @@ -19,6 +19,8 @@ param ( [string] $ConnectionUri = "https://outlook.office365.com/powerShell-liveID" ) +#cspell:words EXOV2 + ## Create a tenant PSSession. function CreateTenantSession() { Import-Module ExchangeOnlineManagement -ErrorAction SilentlyContinue diff --git a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 index e40f0d061b..5a98bbb3a8 100644 --- a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 +++ b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 @@ -4,7 +4,7 @@ # .SYNOPSIS # Sync-MailPublicFoldersCloudToOnprem.ps1 # This script imports the new mail public folders as sync mail public folders from Exchange Online to on-premise. -# And also synchronizes the properites of existing mail-enabled public folders from Exchange Online to on-premises (thereby overriding the mail public folder properties in on-premise). +# And also synchronizes the properties of existing mail-enabled public folders from Exchange Online to on-premises (thereby overriding the mail public folder properties in on-premise). # # Example input to the script: # @@ -24,6 +24,8 @@ param ( [string] $CsvSummaryFile ) +# cspell:words EXOV2 MEPF + # Writes a dated information message to console function WriteInfoMessage() { param ($message) @@ -32,22 +34,22 @@ function WriteInfoMessage() { # Writes an error importing a mail public folder to the CSV summary function WriteErrorSummary() { - param ($folder, $operation, $errorMessage, $commandtext) + param ($folder, $operation, $errorMessage, $commandText) - WriteOperationSummary -folder $folder.Guid -operation $operation -result $errorMessage -commandtext $commandtext + WriteOperationSummary -folder $folder.Guid -operation $operation -result $errorMessage -commandText $commandText $script:errorsEncountered++ } # Writes the operation executed and its result to the output CSV function WriteOperationSummary() { - param ($folder, $operation, $result, $commandtext) + param ($folder, $operation, $result, $commandText) $columns = @( (Get-Date).ToString(), $folder.Guid, $operation, (EscapeCsvColumn $result), - (EscapeCsvColumn $commandtext) + (EscapeCsvColumn $commandText) ) Add-Content $CsvSummaryFile -Value ("{0},{1},{2},{3},{4}" -f $columns) @@ -253,7 +255,7 @@ function GetRemoteMailPublicFolders() { return $mailPublicFolders } -#Get list of MEPF's in OnPrem to be deleted +#Get list of MEPFs in OnPrem to be deleted function GetFoldersToMailDisable( [object[]] $localMailPublicFolders, [hashtable] $validExternalEmailAddresses) { @@ -330,7 +332,7 @@ function SyncMailPublicFolders { # Creating new sync mail public folder $null = &$script:NewSyncMailPublicFolderCommand @newParams - WriteOperationSummary -folder $mailPublicFolder -operation $LocalizedStrings.CreateOperationName -result $LocalizedStrings.CsvSuccessResult -commandtext $createSyncPublicFolder + WriteOperationSummary -folder $mailPublicFolder -operation $LocalizedStrings.CreateOperationName -result $LocalizedStrings.CsvSuccessResult -commandText $createSyncPublicFolder $setParams = @{} $setParams.Add("Identity", $name) @@ -346,14 +348,14 @@ function SyncMailPublicFolders { # Setting other properties to the newly created sync mail public folder &$script:SetMailPublicFolderCommand @setParams - WriteOperationSummary -folder $mailPublicFolder -operation $LocalizedStrings.SetOperationName -result $LocalizedStrings.CsvSuccessResult -commandtext $setOtherProperties + WriteOperationSummary -folder $mailPublicFolder -operation $LocalizedStrings.SetOperationName -result $LocalizedStrings.CsvSuccessResult -commandText $setOtherProperties $validExternalEmailAddresses.Add($externalEmailAddress, $false) $script:CreatedPublicFoldersCount++ } catch { - WriteErrorSummary -folder $mailPublicFolder -operation $LocalizedStrings.CreateOperationName -errorMessage $_.Exception.Message -commandtext "" + WriteErrorSummary -folder $mailPublicFolder -operation $LocalizedStrings.CreateOperationName -errorMessage $_.Exception.Message -commandText "" Write-Error $_ } } @@ -378,14 +380,14 @@ function SyncMailPublicFolders { # Setting properties to the existing sync mail public folder &$script:SetMailPublicFolderCommand @updateParams - WriteOperationSummary -folder $mailPublicFolder -operation $LocalizedStrings.UpdateOperationName -result $LocalizedStrings.CsvSuccessResult -commandtext $updateProperties + WriteOperationSummary -folder $mailPublicFolder -operation $LocalizedStrings.UpdateOperationName -result $LocalizedStrings.CsvSuccessResult -commandText $updateProperties $validExternalEmailAddresses.Add($externalEmailAddress, $false) $script:UpdatedPublicFoldersCount++ } catch { - WriteErrorSummary -folder $mailPublicFolder -operation $LocalizedStrings.UpdateOperationName -errorMessage $_.Exception.Message -commandtext $updateProperties + WriteErrorSummary -folder $mailPublicFolder -operation $LocalizedStrings.UpdateOperationName -errorMessage $_.Exception.Message -commandText $updateProperties Write-Error $_ } } @@ -435,10 +437,10 @@ function SyncMailPublicFolders { # Deleting sync mail public folder &$script:DeletePublicFolderCommand @deleteParams - WriteOperationSummary -folder $syncPublicFolder -operation $LocalizedStrings.DeleteOperationName -result $LocalizedStrings.CsvSuccessResult -commandtext $disableMailPublicFolder + WriteOperationSummary -folder $syncPublicFolder -operation $LocalizedStrings.DeleteOperationName -result $LocalizedStrings.CsvSuccessResult -commandText $disableMailPublicFolder $script:RemovedPublicFoldersCount++ } catch { - WriteErrorSummary -folder $syncPublicFolder -operation $LocalizedStrings.DeleteOperationName -errorMessage $_.Exception.Message -commandtext $disableMailPublicFolder + WriteErrorSummary -folder $syncPublicFolder -operation $LocalizedStrings.DeleteOperationName -errorMessage $_.Exception.Message -commandText $disableMailPublicFolder Write-Error $_ } } From 7221c29792809d6a664c6b83624ec2d49a2850f2 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 17 Mar 2025 15:59:58 -0500 Subject: [PATCH 04/10] Embed strings into scripts --- .../Hybrid/Import-PublicFolderMailboxes.ps1 | 18 +++++++++- .../ImportPublicFolderMailboxes.strings.psd1 | Bin 2668 -> 0 bytes .../Sync-MailPublicFoldersCloudToOnprem.ps1 | 32 +++++++++++++++++- ...ailPublicFoldersCloudToOnprem.strings.psd1 | Bin 4594 -> 0 bytes 4 files changed, 48 insertions(+), 2 deletions(-) delete mode 100644 PublicFolders/Hybrid/ImportPublicFolderMailboxes.strings.psd1 delete mode 100644 PublicFolders/Hybrid/SyncMailPublicFoldersCloudToOnprem.strings.psd1 diff --git a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 index b7e54c9ed0..d8ef80357e 100644 --- a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 +++ b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 @@ -119,7 +119,23 @@ function SyncPublicFolderMailboxes( } #load hashtable of localized string -Import-LocalizedData -BindingVariable LocalizedStrings -FileName ImportPublicFolderMailboxes.strings.psd1 +$LocalizedStrings = ConvertFrom-StringData @' +###PSLOC +SyncingPublicFolderMailbox = Syncing public folder mailbox '{0}'. +CreatingMailUser = Creating mailuser object '{0}'. +MailUserExists = Mailuser object '{0}' already exists for this public folder mailbox. +ConfiguringMailUser = Adding '{0}' to RemotePublicFolderMailboxes. +DoneSyncingPublicFolderMailbox = Done syncing public folder mailbox '{0}' +NoHierarchyPublicFolderMailbox = There aren't any public folder mailboxes, serving hierarchy, to import. +DeletingMailUsersInfo = Deleting mailusers, if any, that don't have corresponding public folder mailboxes in the cloud, serving hierarchy. +RemovingMailUsers = Removing '{0}' from RemotePublicFolderMailboxes. +DeleteMailUser = Deleting mailuser object '{0}'. +IncorrectCredentials = Please provide correct credentials to establish remote session. +StartedPublicFolderMailboxImport = Started import of public folder mailboxes. +CompletedPublicFolderMailboxImport = Completed import of public folder mailboxes. +EXOV2ModuleNotInstalled = This script uses modern authenticaion to connect to Exchange Online and requires EXO V2 module to be installed. Please follow the instructions at https://docs.microsoft.com/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps#install-the-exo-v2-module to install EXO V2 module. +###PSLOC +'@ # Create a tenant PSSession against Exchange Online with modern auth. CreateTenantSession diff --git a/PublicFolders/Hybrid/ImportPublicFolderMailboxes.strings.psd1 b/PublicFolders/Hybrid/ImportPublicFolderMailboxes.strings.psd1 deleted file mode 100644 index d6529d52e8aba719fc7f4ec3132de7b7eb6711fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2668 zcmb_eO>fgc5S=p;|6wH#x!@G_h*SYei>L%NqJX$Mwv!m8c8Q%rh4|~hdo$U3*NKXw zBFl+)Js)r0e60Wco=7c~+(;p7Y2`J3Yq^p{F0oomiFXS*l@zN~oV=8gJeEiDM6W5N z-pYkcbnjAbA)Re?rm~Tltgy;pwZ>DfKHkAjiPa2yKVb0$cdY5;I?#18540Vy#rh-p zDbM5=aE%qk1gHzeYuLTNPtvG=^VpZ_>~8MlYGC{doeZ%%*pL16?Vcf;61pvJ^Wi1_ zZP5YmYKuDuiw<5xYh+-J-By&^Xd`C+`K}ja!4}sVchgzW9I|V zUE!(4|FA3<;KXp8La%~eobh2+j8$*J!Dd)*hj0=`#cF;JRBsS5dmzP~f#=(Su^Fn5 z(4ck!erTaUlp`?2RizmnUejdjDKxEKZ|*X|+{XwhWjQI4k!wW39`;NZsHp<${e95L zIgqikJg>3*ZbulZ5;{IA^S?xlj5OEWTGZU*OmX0nsK`j82&MZ%|1@!N~?pj z$45QYnn|_x*inpj?`MZqcmt^tUaD=~^4wtu zak}IKd+I^$Ka^+k1*h_7IYw30U~!{( zIptdIddtg7cXKeoGnf;M=NGfWyq1WPU2Ufq_n9|4!z-*(yy1D|t0=>c<6h}CQc3j!^ zO~Lu`9q%j-n=jN{!!Ik`;-PPwa7*m9;H8o0a)gT$FT-jKZup(UZfS7OAvMOD89stM zy>N1~2iWl|?DB*UdDz`apw}^z?6s4iJCEgs_GkewCp&hj*Wha{W+eeaVo6lK#y{;O W+Z@NN#+vu#!Fpuhx8Vq1j(-7%rQpy2 diff --git a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 index 5a98bbb3a8..3b271cb509 100644 --- a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 +++ b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 @@ -468,7 +468,37 @@ $script:SmtpPrefixLength = $script:SmtpPrefix.Length [char[]]$script:proxyAddressSeparators = ':', '@' #load hashtable of localized string -Import-LocalizedData -BindingVariable LocalizedStrings -FileName SyncMailPublicFoldersCloudToOnprem.strings.psd1 +$LocalizedStrings = ConvertFrom-StringData @' +###PSLOC +SyncingMailPublicFolder = Syncing mail public folder '{0}'. +CreatingSyncMailPublicFolder = Creating sync mail public folder object '{0}'. +UpdatingSyncMailPublicFolder = Sync mail public folder object '{0}' already exists, hence updating properties. +DoneSyncingMailPublicFolder = Done syncing mail public folder '{0}'. +NoMailPublicFoldersToSync = There aren't any mail public folders in cloud to sync. +DeleteSyncMailPublicFolderTitle = Deleting sync mail public folder, if any, that don't have corresponding mail public folders in the cloud. +DeletingSyncMailPublicFolder = Deleting sync mail public folder for object '{0}', as this is no more in the cloud. +CreateOperationName = Create +SetOperationName = Set +UpdateOperationName = Update +DeleteOperationName = Delete +TimestampCsvHeader = Timestamp +IdentityCsvHeader = Identity +OperationCsvHeader = Operation +ResultCsvHeader = Result +CommandCsvHeader = Command text +CsvSuccessResult = Success +LocalServerVersionNotSupported = You cannot execute this script from your local Exchange server: "{0}". This script can only be executed from Exchange 2013 Management Shell and above. +CreatingRemoteSession = Creating an Exchange Online remote session... +RemoteSessionCreatedSuccessfully = Exchange Online remote session created successfully. +StartedImportingMailPublicFolders = Started import of mail public folders. +CompletedImportingMailPublicFolders = Completed import of mail public folders. +CompletedStatsCount = Total sync mail mail public folders created: {0}, updated: {1} and deleted: {2}. +VerifyConnectorAcceptedDomainsMessage = Please make sure that the following domain(s) are added to the on-premise hybrid connector to avoid a possibility of mail looping: {0} +FoldersToMailDisableConfirmation = You are about to mail-disable the MEPF's in {0} (Y/N). +ConnectExchangeOnlineFailure = Connection to Exchange-Online has failed, terminating the script. +EXOV2ModuleNotInstalled = This script uses modern authenticaion to connect to Exchange Online and requires EXO V2 module to be installed. Please follow the instructions at https://docs.microsoft.com/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps#install-the-exo-v2-module to install EXO V2 module. +###PSLOC +'@ #minimum supported exchange version to run this script $minSupportedVersion = 15 diff --git a/PublicFolders/Hybrid/SyncMailPublicFoldersCloudToOnprem.strings.psd1 b/PublicFolders/Hybrid/SyncMailPublicFoldersCloudToOnprem.strings.psd1 deleted file mode 100644 index aa1490db5b009572bf481f3e53500b36b465cf52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4594 zcmcJTTW{l36ovOQ693_7c<3OJmML#Y2v9o}m8PAFmJ#!qCh0Vg*dcc4AjDq>)_1lK z$959M+*CDhkvK1*BL$ZmDDwz;0YvvV8kYHY#2vLpM@ zKCqAC&yA#B*=swC_ipVMNlzM`E6LAoVK=t1d;Oj3$%Sk$Y#Mz9`$G1*IzgUYE7+=~ z7wn;wcl@ z6se!VFzl0V#@w}sX4}X5TCp7RT-lwZQnb#)yO=#J0w?|!I!8`(tZ$$-gqfA(h(8F$ zg?wyu?m<>ceI`yhW6$Mbq2F4+DUw5R-^q3zwLIo*BVm?&42zaCpR2M`j$GHmfm#oW zoKtlSxJQQ|uTtoMPbDrOF^w6&)3d*a5T4v(LaL99XEhX^{{|E9G;3wEuckq1tn3{t z>IW40ETjVpAVg-)gnfDDt}{~y^|}K2c|`SEK9$YXlg|TEok0GCxGO5I*Z**@g=Fm%0{q-f$zl<_PM5R_0?$hqep}URgKJ9Hu z^uLX9Z&YvfUcYS_beN?}{m?U}d$qb!@Q6-aHORNwD|w#C z?;>(z-OTeh5#j1DPL=Y4?kd(UeSD@k-R4_a*yuY}ipXWU0Fg1*Hc__C`AT=$4_10Y z)%e~JbV}IaT&Zi1DcDQdnZ(}0K0$R<`b=?tuIp#b)O@C?62zQG*`}(xempv=vygrI z@_n?{jgy>mMBBbkwLQGB&m=|9E%ZxFFcq$`mC&0O`tja~y|Fx%5!c;22d6~sdq;=! z33tag*Kq3Ac;1D~>zEr-;IwYpb=0!uV`1gd?DwAia@JkuJm{pz5*+C>+ei(M1?CVd zJ??UovT3`cl|2Pkgt4BZsCT-0%&uZHKO#Ex{A!+px)$&XpQo2^F8t zQ+1(i(a&yZY+mVYTmMIVjwD+g{x%JV!G=8tbl~x>-C!6l{gb6yeZyrI=1q}P_h&D( zo9D`*&u27U;#3ljxA!Ra5c)fH4e!~cVZ+xAgXl8YXJ7QF3uSbrdsKthuk+z1`eH&; zJ;?qTzrjZM+1_!|Z1%Sc`qh*81N(gcNuHj|+Q%^?x4`TotGwMMOT<4Cdyrek4u;1^ z<#w-o^QbY|%95Da9oVwUMvAvBD#EM%UUzwiVhV1vn#kGBJ@$M)OosM5mv6pT@`JT= zeS#ftXR`+T2i;po)Y2y6sHW-QyK(AMu8n=SWY}cH}$? z_T1juiCqijT!onl{l3)hN*7Hw>2%?Jmq)%)r8xVxM%omZC8^)C>k{8XW0zBg=kO0? zkn=swd6G`PJ*PZ}m-fAEzp+zgUdjr+cBUMu$xB)Bt%&~XO!A7|J@+<}hU>mx`sT#+ z7|W_h;9C;&v72v~ZTnCNJrsO{@#*uEe839#?^%VISBl0nOSXL?gHty{=6bg}+jWPw zzY!$OwfxctJRfrRHHjx>0vbFuB>(^b From a984fbdcb8a292a6e0e5974b2b61a2b6a7a5c838 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 17 Mar 2025 16:03:04 -0500 Subject: [PATCH 05/10] cspell fixes for embedded strings --- .../Hybrid/Import-PublicFolderMailboxes.ps1 | 12 +++++------- .../Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 | 4 +--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 index d8ef80357e..40e549811b 100644 --- a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 +++ b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 @@ -120,21 +120,19 @@ function SyncPublicFolderMailboxes( #load hashtable of localized string $LocalizedStrings = ConvertFrom-StringData @' -###PSLOC SyncingPublicFolderMailbox = Syncing public folder mailbox '{0}'. -CreatingMailUser = Creating mailuser object '{0}'. -MailUserExists = Mailuser object '{0}' already exists for this public folder mailbox. +CreatingMailUser = Creating MailUser object '{0}'. +MailUserExists = MailUser object '{0}' already exists for this public folder mailbox. ConfiguringMailUser = Adding '{0}' to RemotePublicFolderMailboxes. DoneSyncingPublicFolderMailbox = Done syncing public folder mailbox '{0}' NoHierarchyPublicFolderMailbox = There aren't any public folder mailboxes, serving hierarchy, to import. -DeletingMailUsersInfo = Deleting mailusers, if any, that don't have corresponding public folder mailboxes in the cloud, serving hierarchy. +DeletingMailUsersInfo = Deleting MailUsers, if any, that don't have corresponding public folder mailboxes in the cloud, serving hierarchy. RemovingMailUsers = Removing '{0}' from RemotePublicFolderMailboxes. -DeleteMailUser = Deleting mailuser object '{0}'. +DeleteMailUser = Deleting MailUser object '{0}'. IncorrectCredentials = Please provide correct credentials to establish remote session. StartedPublicFolderMailboxImport = Started import of public folder mailboxes. CompletedPublicFolderMailboxImport = Completed import of public folder mailboxes. -EXOV2ModuleNotInstalled = This script uses modern authenticaion to connect to Exchange Online and requires EXO V2 module to be installed. Please follow the instructions at https://docs.microsoft.com/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps#install-the-exo-v2-module to install EXO V2 module. -###PSLOC +EXOV2ModuleNotInstalled = This script uses modern authentication to connect to Exchange Online and requires EXO V2 module to be installed. Please follow the instructions at https://docs.microsoft.com/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps#install-the-exo-v2-module to install EXO V2 module. '@ # Create a tenant PSSession against Exchange Online with modern auth. diff --git a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 index 3b271cb509..f4c65792fb 100644 --- a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 +++ b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 @@ -469,7 +469,6 @@ $script:SmtpPrefixLength = $script:SmtpPrefix.Length #load hashtable of localized string $LocalizedStrings = ConvertFrom-StringData @' -###PSLOC SyncingMailPublicFolder = Syncing mail public folder '{0}'. CreatingSyncMailPublicFolder = Creating sync mail public folder object '{0}'. UpdatingSyncMailPublicFolder = Sync mail public folder object '{0}' already exists, hence updating properties. @@ -496,8 +495,7 @@ CompletedStatsCount = Total sync mail mail public folders created: {0}, updated: VerifyConnectorAcceptedDomainsMessage = Please make sure that the following domain(s) are added to the on-premise hybrid connector to avoid a possibility of mail looping: {0} FoldersToMailDisableConfirmation = You are about to mail-disable the MEPF's in {0} (Y/N). ConnectExchangeOnlineFailure = Connection to Exchange-Online has failed, terminating the script. -EXOV2ModuleNotInstalled = This script uses modern authenticaion to connect to Exchange Online and requires EXO V2 module to be installed. Please follow the instructions at https://docs.microsoft.com/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps#install-the-exo-v2-module to install EXO V2 module. -###PSLOC +EXOV2ModuleNotInstalled = This script uses modern authentication to connect to Exchange Online and requires EXO V2 module to be installed. Please follow the instructions at https://docs.microsoft.com/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps#install-the-exo-v2-module to install EXO V2 module. '@ #minimum supported exchange version to run this script From adb3e6c6a796e3e2ccded4efaad12afcb8d5c41c Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 17 Mar 2025 20:30:04 -0500 Subject: [PATCH 06/10] Add minimal docs --- .../Hybrid/Import-PublicFolderMailboxes.ps1 | 2 +- .../Sync-MailPublicFoldersCloudToOnprem.ps1 | 2 +- .../Import-PublicFolderMailboxes.md | 16 ++++++++++++++++ .../Sync-MailPublicFoldersCloudToOnprem.md | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 docs/PublicFolders/Import-PublicFolderMailboxes.md create mode 100644 docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md diff --git a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 index 40e549811b..a527789427 100644 --- a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 +++ b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 @@ -14,7 +14,7 @@ param ( [Parameter(Mandatory=$false)] [PSCredential] $Credential, - [Parameter(Mandatory = $false)] + [Parameter(Mandatory = $true)] [ValidateNotNull()] [string] $ConnectionUri = "https://outlook.office365.com/powerShell-liveID" ) diff --git a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 index f4c65792fb..993a3c8080 100644 --- a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 +++ b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 @@ -15,7 +15,7 @@ param ( [Parameter(Mandatory=$false)] [PSCredential] $Credential, - [Parameter(Mandatory = $false)] + [Parameter(Mandatory = $true)] [ValidateNotNull()] [string] $ConnectionUri = "https://outlook.office365.com/powerShell-liveID", diff --git a/docs/PublicFolders/Import-PublicFolderMailboxes.md b/docs/PublicFolders/Import-PublicFolderMailboxes.md new file mode 100644 index 0000000000..2bfeb6a067 --- /dev/null +++ b/docs/PublicFolders/Import-PublicFolderMailboxes.md @@ -0,0 +1,16 @@ +# Import-PublicFolderMailboxes + +Download the latest release: [Import-PublicFolderMailboxes.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/Import-PublicFolderMailboxes.ps1) + +## Syntax + +```powershell +Import-PublicFolderMailboxes.ps1 + -ConnectionUri + [-Credential ] + [] +``` + +## Usage + +For usage details, please see [Configure Exchange Online public folders for a hybrid deployment | Microsoft Learn](https://learn.microsoft.com/en-us/exchange/collaboration-exo/public-folders/set-up-exo-hybrid-public-folders#configure-exchange-online-public-folders-for-a-hybrid-deployment). diff --git a/docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md b/docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md new file mode 100644 index 0000000000..4ce1cb53ec --- /dev/null +++ b/docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md @@ -0,0 +1,17 @@ +# Sync-ModernPublicFoldersCloudToOnprem + +Download the latest release: [Sync-ModernPublicFoldersCloudToOnprem.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/Sync-ModernPublicFoldersCloudToOnprem.ps1) + +## Syntax + +```powershell +Sync-ModernPublicFoldersCloudToOnprem.ps1 + -ConnectionUri + -CsvSummaryFile + [-Credential ] + [] +``` + +## Usage + +For usage details, please see [Configure Exchange Online public folders for a hybrid deployment | Microsoft Learn](https://learn.microsoft.com/en-us/exchange/collaboration-exo/public-folders/set-up-exo-hybrid-public-folders#configure-exchange-online-public-folders-for-a-hybrid-deployment). From b98bac3a6e3a79cde94ac9a6bcacfdc73e754af1 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 17 Mar 2025 20:40:30 -0500 Subject: [PATCH 07/10] Add docs to mkdcos.yml --- docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md | 6 +++--- mkdocs.yml | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md b/docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md index 4ce1cb53ec..a6b1387a43 100644 --- a/docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md +++ b/docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md @@ -1,11 +1,11 @@ -# Sync-ModernPublicFoldersCloudToOnprem +# Sync-MailPublicFoldersCloudToOnprem -Download the latest release: [Sync-ModernPublicFoldersCloudToOnprem.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/Sync-ModernPublicFoldersCloudToOnprem.ps1) +Download the latest release: [Sync-MailPublicFoldersCloudToOnprem.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/Sync-MailPublicFoldersCloudToOnprem.ps1) ## Syntax ```powershell -Sync-ModernPublicFoldersCloudToOnprem.ps1 +Sync-MailPublicFoldersCloudToOnprem.ps1 -ConnectionUri -CsvSummaryFile [-Credential ] diff --git a/mkdocs.yml b/mkdocs.yml index 5f8b662e94..b5507611d3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -97,9 +97,11 @@ nav: - SimplePerf: Performance/SimplePerf.md - PublicFolders: - Export-ModernPublicFolderStatistics: PublicFolders/Export-ModernPublicFolderStatistics.md + - Import-PublicFolderMailboxes: PublicFolders/Import-PublicFolderMailboxes.md - ModernPublicFolderToMailboxMapGenerator: PublicFolders/ModernPublicFolderToMailboxMapGenerator.md - SetMailPublicFolderExternalAddress: PublicFolders/SetMailPublicFolderExternalAddress.md - SourceSideValidations: PublicFolders/SourceSideValidations.md + - Sync-MailPublicFoldersCloudToOnprem: PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md - Sync-ModernMailPublicFolders: PublicFolders/Sync-ModernMailPublicFolders.md - Update-PublicFolderPermissions: PublicFolders/Update-PublicFolderPermissions.md - ValidateMailEnabledPublicFolders: PublicFolders/ValidateMailEnabledPublicFolders.md From 7d1b81bb9527cf42e51febed7af1e90a8fc78c37 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Wed, 19 Mar 2025 09:00:16 -0500 Subject: [PATCH 08/10] Reorganize PF script docs and update owners --- .github/CODEOWNERS | 4 ++++ .../{ => Hybrid}/Import-PublicFolderMailboxes.md | 0 .../Sync-MailPublicFoldersCloudToOnprem.md | 0 .../Export-ModernPublicFolderStatistics.md | 0 .../ModernPublicFolderToMailboxMapGenerator.md | 0 .../SetMailPublicFolderExternalAddress.md | 0 .../Sync-ModernMailPublicFolders.md | 0 mkdocs.yml | 14 ++++++++------ 8 files changed, 12 insertions(+), 6 deletions(-) rename docs/PublicFolders/{ => Hybrid}/Import-PublicFolderMailboxes.md (100%) rename docs/PublicFolders/{ => Hybrid}/Sync-MailPublicFoldersCloudToOnprem.md (100%) rename docs/PublicFolders/{ => Migration}/Export-ModernPublicFolderStatistics.md (100%) rename docs/PublicFolders/{ => Migration}/ModernPublicFolderToMailboxMapGenerator.md (100%) rename docs/PublicFolders/{ => Migration}/SetMailPublicFolderExternalAddress.md (100%) rename docs/PublicFolders/{ => Migration}/Sync-ModernMailPublicFolders.md (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6c7770beae..3883cdc252 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,5 +14,9 @@ /docs/M365/MDO/ @iserrano76 @rosspa05 @microsoft/css-exchange-admins # EXO PF Team +/PublicFolders/Migration/ @vishmittal @microsoft/css-exchange-admins +/PublicFolders/Hybrid/ @vishmittal @microsoft/css-exchange-admins /PublicFolders/Update-PublicFolderPermissions.ps1 @vishmittal @microsoft/css-exchange-admins +/docs/PublicFolders/Migration/ @vishmittal @microsoft/css-exchange-admins +/docs/PublicFolders/Hybrid/ @vishmittal @microsoft/css-exchange-admins /docs/PublicFolders/Update-PublicFolderPermissions.md @vishmittal @microsoft/css-exchange-admins diff --git a/docs/PublicFolders/Import-PublicFolderMailboxes.md b/docs/PublicFolders/Hybrid/Import-PublicFolderMailboxes.md similarity index 100% rename from docs/PublicFolders/Import-PublicFolderMailboxes.md rename to docs/PublicFolders/Hybrid/Import-PublicFolderMailboxes.md diff --git a/docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md b/docs/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.md similarity index 100% rename from docs/PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md rename to docs/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.md diff --git a/docs/PublicFolders/Export-ModernPublicFolderStatistics.md b/docs/PublicFolders/Migration/Export-ModernPublicFolderStatistics.md similarity index 100% rename from docs/PublicFolders/Export-ModernPublicFolderStatistics.md rename to docs/PublicFolders/Migration/Export-ModernPublicFolderStatistics.md diff --git a/docs/PublicFolders/ModernPublicFolderToMailboxMapGenerator.md b/docs/PublicFolders/Migration/ModernPublicFolderToMailboxMapGenerator.md similarity index 100% rename from docs/PublicFolders/ModernPublicFolderToMailboxMapGenerator.md rename to docs/PublicFolders/Migration/ModernPublicFolderToMailboxMapGenerator.md diff --git a/docs/PublicFolders/SetMailPublicFolderExternalAddress.md b/docs/PublicFolders/Migration/SetMailPublicFolderExternalAddress.md similarity index 100% rename from docs/PublicFolders/SetMailPublicFolderExternalAddress.md rename to docs/PublicFolders/Migration/SetMailPublicFolderExternalAddress.md diff --git a/docs/PublicFolders/Sync-ModernMailPublicFolders.md b/docs/PublicFolders/Migration/Sync-ModernMailPublicFolders.md similarity index 100% rename from docs/PublicFolders/Sync-ModernMailPublicFolders.md rename to docs/PublicFolders/Migration/Sync-ModernMailPublicFolders.md diff --git a/mkdocs.yml b/mkdocs.yml index b5507611d3..e9bb26dafc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -96,13 +96,15 @@ nav: - ExPerfAnalyzer: Performance/ExPerfAnalyzer.md - SimplePerf: Performance/SimplePerf.md - PublicFolders: - - Export-ModernPublicFolderStatistics: PublicFolders/Export-ModernPublicFolderStatistics.md - - Import-PublicFolderMailboxes: PublicFolders/Import-PublicFolderMailboxes.md - - ModernPublicFolderToMailboxMapGenerator: PublicFolders/ModernPublicFolderToMailboxMapGenerator.md - - SetMailPublicFolderExternalAddress: PublicFolders/SetMailPublicFolderExternalAddress.md + - Hybrid: + - Import-PublicFolderMailboxes: PublicFolders/Hybrid/Import-PublicFolderMailboxes.md + - Sync-MailPublicFoldersCloudToOnprem: PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.md + - Migration: + - Export-ModernPublicFolderStatistics: PublicFolders/Migration/Export-ModernPublicFolderStatistics.md + - ModernPublicFolderToMailboxMapGenerator: PublicFolders/Migration/ModernPublicFolderToMailboxMapGenerator.md + - SetMailPublicFolderExternalAddress: PublicFolders/Migration/SetMailPublicFolderExternalAddress.md + - Sync-ModernMailPublicFolders: PublicFolders/Migration/Sync-ModernMailPublicFolders.md - SourceSideValidations: PublicFolders/SourceSideValidations.md - - Sync-MailPublicFoldersCloudToOnprem: PublicFolders/Sync-MailPublicFoldersCloudToOnprem.md - - Sync-ModernMailPublicFolders: PublicFolders/Sync-ModernMailPublicFolders.md - Update-PublicFolderPermissions: PublicFolders/Update-PublicFolderPermissions.md - ValidateMailEnabledPublicFolders: PublicFolders/ValidateMailEnabledPublicFolders.md - ValidateEXOPFDumpster: PublicFolders/ValidateEXOPFDumpster.md From 86fcee5cb200b32434f321285efd01aa0417427b Mon Sep 17 00:00:00 2001 From: Bill Long Date: Wed, 19 Mar 2025 13:20:36 -0500 Subject: [PATCH 09/10] Params with default values should not be mandatory --- PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 | 2 +- PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 index a527789427..40e549811b 100644 --- a/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 +++ b/PublicFolders/Hybrid/Import-PublicFolderMailboxes.ps1 @@ -14,7 +14,7 @@ param ( [Parameter(Mandatory=$false)] [PSCredential] $Credential, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [ValidateNotNull()] [string] $ConnectionUri = "https://outlook.office365.com/powerShell-liveID" ) diff --git a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 index 993a3c8080..f4c65792fb 100644 --- a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 +++ b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 @@ -15,7 +15,7 @@ param ( [Parameter(Mandatory=$false)] [PSCredential] $Credential, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [ValidateNotNull()] [string] $ConnectionUri = "https://outlook.office365.com/powerShell-liveID", From 4244c4e9a8580e4b8d037c63ecc6b7368073f131 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Wed, 19 Mar 2025 13:29:46 -0500 Subject: [PATCH 10/10] Don't write the same output twice --- PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 index f4c65792fb..bbbca14d5d 100644 --- a/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 +++ b/PublicFolders/Hybrid/Sync-MailPublicFoldersCloudToOnprem.ps1 @@ -520,9 +520,6 @@ if ($localServerVersion.Major -lt $minSupportedVersion) { exit } -# Create a PSSession -WriteInfoMessage ($LocalizedStrings.CreatingRemoteSession) - InitializeExchangeOnlineRemoteSession # Get mail enabled public folders in cloud