<# .SYNOPSIS Bulk update WorkingHoursTimeZone for room mailboxes from a two-column CSV (Identity, WorkingHoursTimeZone) using Set-MailboxCalendarConfiguration. .DESCRIPTION - Reads a CSV (default: .\batch_01.csv) with headers: Identity, WorkingHoursTimeZone - Validates timezone values against Windows time zone IDs. - Compares with current mailbox value; updates only if different. - Runs as a DRY RUN by default. Use -Apply to commit. - Produces a results log CSV in the same folder with a timestamped file name. .PARAMETER CsvPath Path to the input CSV file (default: .\batch_01.csv). .PARAMETER Apply Switch to commit changes. If omitted, runs in WhatIf (dry run) mode. .EXAMPLE .\Set-RoomWorkingHoursTimeZone.ps1 -CsvPath .\batch_01.csv # Dry run (no changes). Shows what *would* change. .EXAMPLE .\Set-RoomWorkingHoursTimeZone.ps1 -CsvPath .\batch_01.csv -Apply # Commits the timezone updates. .NOTES Requires the ExchangeOnlineManagement module. #> [CmdletBinding(SupportsShouldProcess=$true)] param( [Parameter(Mandatory=$false)] [string]$CsvPath = ".\batch_01.csv", [switch]$Apply ) # region Module & Connection try { if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) { Write-Host "Installing ExchangeOnlineManagement module for current user..." -ForegroundColor Yellow Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force -AllowClobber } Import-Module ExchangeOnlineManagement -ErrorAction Stop # Try a simple call; if it fails, connect. try { Get-EXOMailbox -ResultSize 1 -ErrorAction Stop | Out-Null } catch { Write-Host "Connecting to Exchange Online..." -ForegroundColor Cyan try { Connect-ExchangeOnline -ShowBanner:$false } catch { Write-Warning "Interactive sign-in failed or blocked. Trying device code flow..." Connect-ExchangeOnline -UseDeviceAuthentication -ShowBanner:$false } } } catch { Write-Error "Failed to prepare Exchange Online session: $($_.Exception.Message)" return } # endregion # region Read & validate CSV if (-not (Test-Path -Path $CsvPath)) { Write-Error "CSV not found: $CsvPath" return } # Import and trim whitespace from the two required fields $rows = Import-Csv -Path $CsvPath | ForEach-Object { [PSCustomObject]@{ Identity = (($_.Identity) -as [string]).Trim() WorkingHoursTimeZone = (($_.WorkingHoursTimeZone) -as [string]).Trim() } } if (-not $rows -or $rows.Count -eq 0) { Write-Error "CSV is empty or could not be parsed." return } # Build a set of valid Windows time zone IDs from .NET (matches what EXO expects) $validTimeZones = [System.TimeZoneInfo]::GetSystemTimeZones().Id # Prepare output log $timestamp = (Get-Date).ToString("yyyyMMdd_HHmmss") $logPath = Join-Path -Path (Split-Path -Parent $CsvPath) -ChildPath ("WorkingHoursTimeZone_UpdateResults_{0}.csv" -f $timestamp) $results = @() # Configure WhatIf behavior (default dry run) $useWhatIf = -not $Apply.IsPresent if ($useWhatIf) { Write-Host "Running in DRY RUN mode (WhatIf). Use -Apply to commit changes." -ForegroundColor Yellow } else { Write-Host "Applying changes..." -ForegroundColor Green } # endregion # region Process rows $index = 0 $total = $rows.Count foreach ($row in $rows) { $index++ Write-Progress -Activity "Updating WorkingHoursTimeZone" -Status ("{0} of {1}: {2}" -f $index, $total, $row.Identity) -PercentComplete (($index / $total) * 100) $identity = $row.Identity $targetTz = $row.WorkingHoursTimeZone $result = [PSCustomObject]@{ Identity = $identity PreviousTimeZone = $null TargetTimeZone = $targetTz EffectiveTimeZone = $null Action = $null Success = $false Error = $null } try { if ([string]::IsNullOrWhiteSpace($identity) -or [string]::IsNullOrWhiteSpace($targetTz)) { throw "Missing Identity or WorkingHoursTimeZone in CSV row." } if ($validTimeZones -notcontains $targetTz) { throw ("Invalid WorkingHoursTimeZone '{0}'. Must be a valid Windows time zone ID (e.g., 'W. Europe Standard Time')." -f $targetTz) } # Get current config to avoid unnecessary updates $current = Get-MailboxCalendarConfiguration -Identity $identity -ErrorAction Stop $result.PreviousTimeZone = $current.WorkingHoursTimeZone if ($current.WorkingHoursTimeZone -eq $targetTz) { $result.Action = "NoChange" $result.Success = $true $result.EffectiveTimeZone = $current.WorkingHoursTimeZone $results += $result continue } # Apply the update (or WhatIf) $splat = @{ Identity = $identity WorkingHoursTimeZone = $targetTz ErrorAction = 'Stop' } if ($useWhatIf) { Set-MailboxCalendarConfiguration @splat -WhatIf $result.Action = "WouldUpdate" $result.Success = $true $result.EffectiveTimeZone = $current.WorkingHoursTimeZone } else { Set-MailboxCalendarConfiguration @splat # Verify after change Start-Sleep -Milliseconds 200 $verify = Get-MailboxCalendarConfiguration -Identity $identity -ErrorAction SilentlyContinue $result.Action = "Updated" $result.EffectiveTimeZone = $verify.WorkingHoursTimeZone $result.Success = ($verify.WorkingHoursTimeZone -eq $targetTz) if (-not $result.Success) { $result.Error = ("Post-update verification mismatch. Expected '{0}', got '{1}'." -f $targetTz, $verify.WorkingHoursTimeZone) } } } catch { if (-not $result.Action) { $result.Action = "Error" } # <- replaces '??' for PS 5.1 $result.Error = $_.Exception.Message $result.Success = $false } $results += $result } # endregion # region Write results $results | Export-Csv -Path $logPath -NoTypeInformation -Encoding UTF8 Write-Host ("Done. Results saved to: {0}" -f $logPath) -ForegroundColor Cyan # endregion