diff --git a/Admin/Remove-DuplicateEntriesFromIanaMappings.ps1 b/Admin/Remove-DuplicateEntriesFromIanaMappings.ps1 new file mode 100644 index 0000000000..8c37b44283 --- /dev/null +++ b/Admin/Remove-DuplicateEntriesFromIanaMappings.ps1 @@ -0,0 +1,206 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +#Requires -Version 3.0 + +<# +.SYNOPSIS + Removes duplicate entries from the IanaTimeZoneMappings.xml file which is used by Exchange Server +.DESCRIPTION + Removes duplicate entries from the IanaTimeZoneMappings.xml file which is used by Exchange Server. + Duplicate entries can lead to exceptions and break functionalities on Microsoft Exchange Server such as processing .ics files. + + For more information see: https://aka.ms/ExchangeIanaTimeZoneIssue +.PARAMETER Server + The Exchange server that should be validated by the script. It also accepts values directly from the pipeline for seamless integration. +.PARAMETER RestartServices + Specifies whether the following services should be restarted on the target system: W3SVC, WAS, MSExchangeTransport + Default value: $false +.PARAMETER ScriptUpdateOnly + This optional parameter allows you to only update the script without performing any other actions. +.PARAMETER SkipVersionCheck + This optional parameter allows you to skip the automatic version check and script update. +.EXAMPLE + PS C:\> .\Remove-DuplicateEntriesFromIanaMappings.ps1 -Server exch1.contoso.com +.EXAMPLE + PS C:\> Get-ExchangeServer | .\Remove-DuplicateEntriesFromIanaMappings.ps1 +.EXAMPLE + PS C:\> .\Remove-DuplicateEntriesFromIanaMappings.ps1 -Server exch1.contoso.com -RestartServices $true +#> + +[CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess, ConfirmImpact = 'High')] +param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "Default")] + [string[]]$Server = $env:COMPUTERNAME, + + [Parameter(Mandatory = $false, ParameterSetName = "Default")] + [bool]$RestartServices = $false, + + [Parameter(Mandatory = $true, ParameterSetName = "ScriptUpdateOnly")] + [switch]$ScriptUpdateOnly, + + [Parameter(Mandatory = $false, ParameterSetName = "Default")] + [switch]$SkipVersionCheck +) + +begin { + . $PSScriptRoot\..\Shared\Confirm-Administrator.ps1 + . $PSScriptRoot\..\Shared\Get-ExSetupFileVersionInfo.ps1 + . $PSScriptRoot\..\Shared\Invoke-ScriptBlockHandler.ps1 + . $PSScriptRoot\..\Shared\ScriptUpdateFunctions\GenericScriptUpdate.ps1 + + Write-Verbose "PowerShell version: $($PSVersionTable.PSVersion)" + + $scriptBlock = { + param( + $PerformServiceRestart, + $LocalComputerName + ) + + . $PSScriptRoot\..\Shared\ValidatorFunctions\Test-IanaTimeZoneMapping.ps1 + . $PSScriptRoot\..\Shared\Get-RemoteRegistryValue.ps1 + + # This needs to be set if the server, which is processed is not the local computer + # If we don't set it, we won't see verbose output if -Verbose parameter is used + if ($LocalComputerName -ne $env:COMPUTERNAME) { + $VerbosePreference = $Using:VerbosePreference + } + + try { + Write-Host "[+] Validating IanaTimeZoneMappings.xml on server $env:COMPUTERNAME" + + $activityBase = "[$env:COMPUTERNAME]" + $writeProgressParams = @{ + Activity = "$activityBase Getting IanaTimeZoneMappings.xml Path" + Id = [Math]::Abs(($env:COMPUTERNAME).GetHashCode()) + } + Write-Progress @writeProgressParams + + # Locate the path where the IanaTimeZoneMappings.xml resides - to do this, read the MsiInstallPath from the registry + $exchangeServerSetupPathParams = @{ + MachineName = $env:COMPUTERNAME + SubKey = "SOFTWARE\Microsoft\ExchangeServer\v15\Setup" + GetValue = "MsiInstallPath" + } + $exchangeSetupPath = Get-RemoteRegistryValue @exchangeServerSetupPathParams + + if (([System.String]::IsNullOrEmpty($exchangeSetupPath))) { + Write-Host "[+] Unable to locate the Exchange Server setup path" + return + } + + # Define the final full path to the mapping file and for the backup file (in case we need to create it) + $mappingFilePath = "$exchangeSetupPath\Bin\IanaTimeZoneMappings.xml" + $mappingBackupFilePath = "$mappingFilePath.{0}.bak" -f $(Get-Date -Format MMddyyyyHHmmss) + + if ((Test-Path -Path $mappingFilePath) -eq $false) { + Write-Host "[+] Iana mappings file was not found" -ForegroundColor Red + + return + } + + $writeProgressParams.Activity = $activityBase + " Searching for duplicate entries in IanaTimeZoneMappings.xml" + Write-Progress @writeProgressParams + + # Check if IanaTimeZoneMappings.xml contains duplicate entries - this is done by the custom Test-IanaTimeZoneMapping function + $testIanaTimeZoneMappingResults = Test-IanaTimeZoneMapping -FilePath $mappingFilePath + + if ($null -ne $testIanaTimeZoneMappingResults -and + $testIanaTimeZoneMappingResults.DuplicateEntries.Count -ge 1) { + + $duplicateEntries = $testIanaTimeZoneMappingResults.DuplicateEntries + $ianaMappingXml = $testIanaTimeZoneMappingResults.IanaMappingXml + + Write-Host "[+] Duplicate entries detected!" -ForegroundColor Yellow + + try { + $writeProgressParams.Activity = $activityBase + " Creating backup $mappingBackupFilePath" + Write-Progress @writeProgressParams + + # If duplicate entries were detected, create a backup of the file before modifying it + Copy-Item -Path $mappingFilePath -Destination $mappingBackupFilePath + Write-Host "[+] Backup created: $mappingBackupFilePath" + } catch { + Write-Host "[+] Failed to create backup file. Inner Exception: $_" -ForegroundColor Red + + return + } + + $dupeIndex = 0 + + # Iterate through all of the duplicate entries and remove them one by one + foreach ($dupe in $duplicateEntries) { + Write-Host "[+] Processing duplicate entry: $dupe" + + $writeProgressParams.Activity = $activityBase + " Processing duplicate entry: $dupe" + Write-Progress @writeProgressParams + + try { + # Select the duplicate node and remove it - we use SelectSingleNode here as it could be that there are multiple duplicates + $singleNode = $ianaMappingXml.SelectSingleNode("//Map[@IANA='$($dupe.IANA)' and @Win='$($dupe.Win)']") + $singleNode.ParentNode.RemoveChild($singleNode) | Out-Null + + $dupeIndex++ + } catch { + Write-Host "[+] Failed to fix duplicate entry $dupe. Inner Exception: $_" -ForegroundColor Red + + return + } + } + + # Validate that all duplicate entries were removed before saving the mapping file + if ($duplicateEntries.Count -eq $dupeIndex) { + Write-Host "[+] All duplicate entries were removed" -ForegroundColor Green + + $writeProgressParams.Activity = $activityBase + " Saving changes to file IanaTimeZoneMappings.xml" + Write-Progress @writeProgressParams + + try { + # Save the modified IanaTimeZoneMappings.xml file and override the existing one + $ianaMappingXml.Save($mappingFilePath) + + # Restart services if the -RestartServices $true was used when running the script - otherwise do nothing + if ($PerformServiceRestart) { + Write-Host "[+] Restart services: W3SVC, WAS & MSExchangeTransport" + + $writeProgressParams.Activity = $activityBase + " Restarting services W3SVC, WAS & MSExchangeTransport" + Write-Progress @writeProgressParams + + try { + Restart-Service -Name W3SVC, WAS, MSExchangeTransport -Force + } catch { + Write-Host "[+] Failed to restart services. Inner Exception: $_" -ForegroundColor Red + } + } + } catch { + Write-Host "[+] Failed to save the modified mapping file. Inner Exception: $_" -ForegroundColor Red + } + } + } else { + Write-Host "[+] No duplicate entries were found" -ForegroundColor Green + } + } finally { + Write-Progress @writeProgressParams -Completed + } + } +} process { + if (-not(Confirm-Administrator)) { + Write-Host "The script needs to be executed in elevated mode. Start the PowerShell as an administrator." -ForegroundColor Yellow + exit + } + + foreach ($srv in $Server) { + # Check if the target server is online / reachable to us, we use the Get-ExSetupFileVersionInfo custom function to do this + $exchangeFileVersionInfo = Get-ExSetupFileVersionInfo -Server $srv + + if (-not([System.String]::IsNullOrEmpty($exchangeFileVersionInfo))) { + Invoke-ScriptBlockHandler -ComputerName $srv -ScriptBlock $scriptBlock -ArgumentList $RestartServices, $env:COMPUTERNAME + } else { + Write-Host "[+] Server: $srv is offline or not reachable" -ForegroundColor Yellow + } + Write-Host "" + } +} end { + Write-Host ("Do you have feedback regarding the script? Please email ExToolsFeedback@microsoft.com.") -ForegroundColor Green + Write-Host "" +} diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1 index 80a2bf192a..923b2e3fdb 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1 @@ -3,9 +3,9 @@ . $PSScriptRoot\Add-AnalyzedResultInformation.ps1 . $PSScriptRoot\Get-DisplayResultsGroupingKey.ps1 -. $PSScriptRoot\Test-IanaTimeZoneMapping.ps1 . $PSScriptRoot\..\..\..\Shared\CompareExchangeBuildLevel.ps1 . $PSScriptRoot\..\..\..\Shared\ErrorMonitorFunctions.ps1 +. $PSScriptRoot\..\..\..\Shared\ValidatorFunctions\Test-IanaTimeZoneMapping.ps1 function Invoke-AnalyzerFrequentConfigurationIssues { [CmdletBinding()] param( @@ -260,11 +260,11 @@ function Invoke-AnalyzerFrequentConfigurationIssues { $ianaTimeZoneInvalidEntriesList = New-Object System.Collections.Generic.List[string] foreach ($invalid in $ianaTimeZoneStatusMissingAttributes) { - $ianaTimeZoneInvalidEntriesList.Add("Invalid entry - IANA: $($invalid.IANA) Win: $($invalid.Win)") + $ianaTimeZoneInvalidEntriesList.Add("[Invalid entry] - IANA: $($invalid.IANA) Win: $($invalid.Win)") } foreach ($dupe in $ianaTimeZoneStatusDuplicateEntries) { - $ianaTimeZoneInvalidEntriesList.Add("Duplicate entry - IANA: $($dupe.IANA) Win: $($dupe.Win)") + $ianaTimeZoneInvalidEntriesList.Add("[Duplicate entry] - IANA: $($dupe.IANA) Win: $($dupe.Win)") } if ($ianaTimeZoneInvalidEntriesList.Count -ge 1) { diff --git a/Diagnostics/HealthChecker/Analyzer/Tests/DataCollection/IanaTimeZoneMappings.xml b/Shared/Tests/ValidatorFunctions/Data/IanaTimeZoneMappings.xml similarity index 100% rename from Diagnostics/HealthChecker/Analyzer/Tests/DataCollection/IanaTimeZoneMappings.xml rename to Shared/Tests/ValidatorFunctions/Data/IanaTimeZoneMappings.xml diff --git a/Diagnostics/HealthChecker/Analyzer/Tests/DataCollection/IanaTimeZoneMappings_broken.xml b/Shared/Tests/ValidatorFunctions/Data/IanaTimeZoneMappings_broken.xml similarity index 100% rename from Diagnostics/HealthChecker/Analyzer/Tests/DataCollection/IanaTimeZoneMappings_broken.xml rename to Shared/Tests/ValidatorFunctions/Data/IanaTimeZoneMappings_broken.xml diff --git a/Diagnostics/HealthChecker/Analyzer/Tests/Test-IanaTimeZoneMapping.Tests.ps1 b/Shared/Tests/ValidatorFunctions/Test-IanaTimeZoneMapping.Tests.ps1 similarity index 87% rename from Diagnostics/HealthChecker/Analyzer/Tests/Test-IanaTimeZoneMapping.Tests.ps1 rename to Shared/Tests/ValidatorFunctions/Test-IanaTimeZoneMapping.Tests.ps1 index 17a52421bc..824ebaa560 100644 --- a/Diagnostics/HealthChecker/Analyzer/Tests/Test-IanaTimeZoneMapping.Tests.ps1 +++ b/Shared/Tests/ValidatorFunctions/Test-IanaTimeZoneMapping.Tests.ps1 @@ -5,10 +5,10 @@ BeforeAll { $parent = Split-Path -Parent $PSScriptRoot $scriptName = "Test-IanaTimeZoneMapping.ps1" - . "$parent\$scriptName" + . "$parent\..\ValidatorFunctions\$scriptName" - $script:brokenIanaTimeZoneMappingsPath = "$parent\Tests\DataCollection\IanaTimeZoneMappings_broken.xml" - $script:workingIanaTimeZoneMappingsPath = "$parent\Tests\DataCollection\IanaTimeZoneMappings.xml" + $script:brokenIanaTimeZoneMappingsPath = "$parent\ValidatorFunctions\Data\IanaTimeZoneMappings_broken.xml" + $script:workingIanaTimeZoneMappingsPath = "$parent\ValidatorFunctions\Data\IanaTimeZoneMappings.xml" [xml]$script:brokenIanaTimeZoneMappingsXml = Get-Content $brokenIanaTimeZoneMappingsPath -Raw -Encoding UTF8 [xml]$script:workingIanaTimeZoneMappingsXml = Get-Content $workingIanaTimeZoneMappingsPath -Raw -Encoding UTF8 @@ -23,10 +23,10 @@ Describe "Testing Test-IanaTimeZoneMapping" { $results.DuplicateEntries | Should -HaveCount 2 } - It "Should add duplicates as XmlElement type" { + It "Should return duplicates as PSCustomObject" { $results = Test-IanaTimeZoneMapping -FilePath $brokenIanaTimeZoneMappingsPath foreach ($entry in $results.DuplicateEntries) { - $entry | Should -BeOfType System.Xml.XmlLinkedNode + $entry | Should -BeOfType PSCustomObject } } diff --git a/Diagnostics/HealthChecker/Analyzer/Test-IanaTimeZoneMapping.ps1 b/Shared/ValidatorFunctions/Test-IanaTimeZoneMapping.ps1 similarity index 76% rename from Diagnostics/HealthChecker/Analyzer/Test-IanaTimeZoneMapping.ps1 rename to Shared/ValidatorFunctions/Test-IanaTimeZoneMapping.ps1 index 6736719999..7ea4efc7ae 100644 --- a/Diagnostics/HealthChecker/Analyzer/Test-IanaTimeZoneMapping.ps1 +++ b/Shared/ValidatorFunctions/Test-IanaTimeZoneMapping.ps1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -. $PSScriptRoot\..\..\..\Shared\ErrorMonitorFunctions.ps1 +. $PSScriptRoot\..\ErrorMonitorFunctions.ps1 function Test-IanaTimeZoneMapping { [CmdletBinding(DefaultParameterSetName = "FilePath")] @@ -52,14 +52,22 @@ function Test-IanaTimeZoneMapping { if ([System.String]::IsNullOrEmpty($iana) -or [System.String]::IsNullOrEmpty($win)) { Write-Verbose "Map node missing required attribute: $xmlMapKey" - $xmlMissingAttributesList.Add($node) + #$xmlMissingAttributesList.Add("@IANA='$iana' and @Win='$win'") + $xmlMissingAttributesList.Add([PSCustomObject]@{ + IANA = if ([System.String]::IsNullOrEmpty($iana)) { "N/A" } else { $iana } + Win = if ([System.String]::IsNullOrEmpty($win)) { "N/A" } else { $win } + }) continue } if ($xmlMap -contains $xmlMapKey) { Write-Verbose "Duplicate entry found: $xmlMapKey" - $xmlDuplicateEntriesList.Add($node) + #$xmlDuplicateEntriesList.Add("@IANA='$iana' and @Win='$win'") + $xmlDuplicateEntriesList.Add([PSCustomObject]@{ + IANA = $iana + Win = $win + }) continue } @@ -72,6 +80,7 @@ function Test-IanaTimeZoneMapping { } } end { return [PSCustomObject]@{ + IanaMappingXml = $IanaMappingFile NodeMissingAttributes = $xmlMissingAttributesList DuplicateEntries = $xmlDuplicateEntriesList } diff --git a/docs/Admin/Remove-DuplicateEntriesFromIanaMappings.md b/docs/Admin/Remove-DuplicateEntriesFromIanaMappings.md new file mode 100644 index 0000000000..e35b2490c7 --- /dev/null +++ b/docs/Admin/Remove-DuplicateEntriesFromIanaMappings.md @@ -0,0 +1,43 @@ +# Remove-DuplicateEntriesFromIanaMappings + +Download the latest release: [Remove-DuplicateEntriesFromIanaMappings.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/Remove-DuplicateEntriesFromIanaMappings.ps1) + +## Description + +After installing the Exchange Server November 2024 Security Update (SU) Version 1 or Version 2, you may encounter issues when the Exchange Server processes calendar-related information and files, such as `.iCal` or `.ics` attachments. Specifically, you may be unable to preview these files or add them to your calendar. This issue affects users who utilize Outlook on the Web (OWA) and the Exchange Active Sync (EAS) mail client on mobile devices. Additionally, this problem may impact Exchange Transport when processing emails that include `.iCal` or `.ics` file attachments. + +More information about the issue can be found in the [Time zone exception occurs after installing Exchange Server November 2024 SU (Version 1 or Version 2)](https://support.microsoft.com/topic/time-zone-exception-occurs-after-installing-exchange-server-november-2024-su-version-1-or-version-2-851b3005-6d39-49a9-a6b5-5b4bb42a606f) knowledge base article. The `Remove-DuplicateEntriesFromIanaMappings.ps1` PowerShell script can be used to apply the workaround on one or multiple servers at once. + +## Syntax + +```powershell +Remove-DuplicateEntriesFromIanaMappings.ps1 + [-Server ] + [-RestartServices ] + [-ScriptUpdateOnly ] + [-SkipVersionCheck ] +``` + +## Usage + +Copy the script to an Exchange server. Then, run it from there using an elevated Windows PowerShell or Exchange Management Shell (EMS). + +**Examples:** + +When you run the script in this manner, it will validate the `IanaTimeZoneMappings.xml` file located on the server `exch1.contoso.com`. The script will then identify and remove any duplicate entries found within the file: + +```powershell +.\Remove-DuplicateEntriesFromIanaMappings.ps1 -Server exch1.contoso.com +``` + +When you run the script in this manner, it will validate and correct the `IanaTimeZoneMappings.xml` file on all Exchange servers that are returned by the `Get-ExchangeServer` command. The script will ensure that any duplicate entries within the file are identified and removed: + +```powershell +Get-ExchangeServer | .\Remove-DuplicateEntriesFromIanaMappings.ps1 +``` + +When you run the script in this manner, it will validate the `IanaTimeZoneMappings.xml` file on the server `exch1.contoso.com` and remove any duplicate entries. Additionally, it will restart the `W3SVC`, `WAS`, and `MSExchangeTransport` services: + +```powershell +.\Remove-DuplicateEntriesFromIanaMappings.ps1 -Server exch1.contoso.com -RestartServices $true +``` diff --git a/mkdocs.yml b/mkdocs.yml index d01b8ae98b..873c486e31 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,6 +10,7 @@ nav: - Get-SimpleAuditLogReport: Admin/Get-SimpleAuditLogReport.md - MonitorExchangeAuthCertificate: Admin/MonitorExchangeAuthCertificate.md - Remove-CertExpiryNotifications: Admin/Remove-CertExpiryNotifications.md + - Remove-DuplicateEntriesFromIanaMappings: Admin/Remove-DuplicateEntriesFromIanaMappings.md - Reset-ScanEngineVersion: Admin/Reset-ScanEngineVersion.md - SetUnifiedContentPath: Admin/SetUnifiedContentPath.md - Test-AMSI: Admin/Test-AMSI.md