Skip to content

Commit

Permalink
New script Remove-DuplicateEntriesFromIanaMappings.ps1
Browse files Browse the repository at this point in the history
  • Loading branch information
lusassl-msft committed Dec 18, 2024
1 parent ca2292e commit 5b44989
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 11 deletions.
206 changes: 206 additions & 0 deletions Admin/Remove-DuplicateEntriesFromIanaMappings.ps1
Original file line number Diff line number Diff line change
@@ -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 [email protected].") -ForegroundColor Green
Write-Host ""
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down Expand Up @@ -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
}
Expand All @@ -72,6 +80,7 @@ function Test-IanaTimeZoneMapping {
}
} end {
return [PSCustomObject]@{
IanaMappingXml = $IanaMappingFile
NodeMissingAttributes = $xmlMissingAttributesList
DuplicateEntries = $xmlDuplicateEntriesList
}
Expand Down
43 changes: 43 additions & 0 deletions docs/Admin/Remove-DuplicateEntriesFromIanaMappings.md
Original file line number Diff line number Diff line change
@@ -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 <string[]>]
[-RestartServices <bool>]
[-ScriptUpdateOnly <switch>]
[-SkipVersionCheck <switch>]
```

## 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
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 5b44989

Please sign in to comment.