Public/Exchange/Mailbox/Set/Set-ExMailboxRegionalConfiguration.ps1

<#
    .SYNOPSIS
    Sets the regional configuration for Exchange mailboxes, including language, time zone, date format, and time format.
 
    .DESCRIPTION
    This function sets the regional configuration settings (language, time zone, date format, time format)
    for Exchange Online mailboxes. It can target mailboxes by identity, by domain, from a CSV file, or all mailboxes in the organization.
    It also supports generating the corresponding Set-MailboxRegionalConfiguration cmdlets without executing them.
 
    .PARAMETER Identity
    The identity of the mailbox(es) to set the regional configuration for. This can be an array of email addresses, usernames, or display names.
 
    .PARAMETER ByDomain
    Filter mailboxes by domain name. All mailboxes with a primary SMTP address in this domain will be processed.
 
    .PARAMETER FromCSV
    The path to a CSV file containing mailbox identities and optional regional configuration settings to process.
 
    .PARAMETER AllMailboxes
    If specified, all mailboxes in the organization will be processed.
 
    .PARAMETER Language
    The language code to set for the mailbox regional configuration (e.g., 'en-US', 'fr-FR').
    See the ValidateSet in the parameter definition for supported language codes.
 
    .PARAMETER TimeZone
    The time zone to set for the mailbox regional configuration. This can be a time zone ID (e.g., 'Pacific Standard Time')
    or a simplified offset format (e.g., '+1', '-5').
    See the ValidateSet in the parameter definition for supported offset values.
 
    .PARAMETER DateFormat
    The date format to set for the mailbox regional configuration (e.g., 'MM/dd/yyyy', 'dd/MM/yyyy').
    Auto-detected based on the specified language if not provided.
 
    .PARAMETER TimeFormat
    The time format to set for the mailbox regional configuration (e.g., 'HH:mm', 'hh:mm tt').
    Auto-detected based on the specified language if not provided.
 
    .PARAMETER OnlyIfLanguageEmpty
    If specified, the function will only modify mailboxes that currently have no language configured (empty Language property).
    Useful to avoid overwriting existing configurations set by users or administrators.
 
    .PARAMETER GenerateCmdlets
    If specified, the function will generate the cmdlets and save them to a file instead of executing them.
 
    .PARAMETER OutputFile
    The path to the output file where generated cmdlets will be saved. Default is a timestamped file in the current directory.
 
    .EXAMPLE
    Set-ExMailboxRegionalConfiguration -Identity "user@example.com" -Language "en-US" -TimeZone "+1" -DateFormat "MM/dd/yyyy" -TimeFormat "HH:mm" -GenerateCmdlets -OutputFile "C:\temp\cmdlets.txt"
 
    Generates the Set-MailboxRegionalConfiguration cmdlet for the specified mailbox with the given regional settings and saves it to the specified file without executing it.
 
    .EXAMPLE
    Set-ExMailboxRegionalConfiguration -ByDomain "example.com" -Language "fr-FR" -TimeZone "Romance Standard Time"
 
    Sets the regional configuration for all mailboxes in the specified domain to French language and Romance Standard Time zone.
 
    .EXAMPLE
    Set-ExMailboxRegionalConfiguration -FromCSV "C:\path\to\file.csv" -GenerateCmdlets -OutputFile "C:\temp\cmdlets.txt"
     
    Generates the Set-MailboxRegionalConfiguration cmdlets for mailboxes listed in the specified CSV file and saves them to the specified file without executing them.
 
    .EXAMPLE
    Set-ExMailboxRegionalConfiguration -ByDomain "example.com" -Language "en-US" -TimeZone "+1" -OnlyIfLanguageEmpty
 
    Sets the regional configuration only for mailboxes in the specified domain that currently have no language configured.
 
    .LINK
    https://ps365.clidsys.com/docs/commands/Set-ExMailboxRegionalConfiguration
#>


function Set-ExMailboxRegionalConfiguration {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Identity')]
        [string[]]$Identity,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByDomain')]
        [string]$ByDomain,

        [Parameter(Mandatory = $true, ParameterSetName = 'FromCSV')]
        [string]$FromCSV,

        [Parameter(Mandatory = $true, ParameterSetName = 'AllMailboxes')]
        [switch]$AllMailboxes,

        [Parameter(Mandatory = $false, ParameterSetName = 'Identity')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ByDomain')]
        [Parameter(Mandatory = $false, ParameterSetName = 'AllMailboxes')]
        [ValidateSet('af-ZA', 'am-ET', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-LB', 'ar-LY', 'ar-MA', 'ar-OM', 'ar-QA', 'ar-SA', 'ar-SY', 'ar-TN', 'ar-YE', 'as-IN', 'az-Cyrl-AZ', 'az-Latn-AZ', 'ba-RU', 'be-BY', 'bg-BG', 'bn-BD', 'bn-IN', 'bo-CN', 'br-FR', 'bs-Cyrl-BA', 'bs-Latn-BA', 'ca-ES', 'co-FR', 'cs-CZ', 'cy-GB', 'da-DK', 'de-AT', 'de-CH', 'de-DE', 'de-LI', 'de-LU', 'el-GR', 'en-029', 'en-AU', 'en-BZ', 'en-CA', 'en-GB', 'en-IE', 'en-IN', 'en-JM', 'en-MY', 'en-NZ', 'en-PH', 'en-SG', 'en-TT', 'en-US', 'en-ZA', 'en-ZW', 'es-AR', 'es-BO', 'es-CL', 'es-CO', 'es-CR', 'es-DO', 'es-EC', 'es-ES', 'es-GT', 'es-HN', 'es-MX', 'es-NI', 'es-PA', 'es-PE', 'es-PR', 'es-PY', 'es-SV', 'es-US', 'es-UY', 'es-VE', 'et-EE', 'eu-ES', 'fa-IR', 'fi-FI', 'fil-PH', 'fo-FO', 'fr-BE', 'fr-CA', 'fr-CH', 'fr-FR', 'fr-LU', 'fr-MC', 'fy-NL', 'ga-IE', 'gd-GB', 'gl-ES', 'gu-IN', 'ha-Latn-NG', 'he-IL', 'hi-IN', 'hr-BA', 'hr-HR', 'hu-HU', 'hy-AM', 'id-ID', 'ig-NG', 'is-IS', 'it-CH', 'it-IT', 'ja-JP', 'ka-GE', 'kk-KZ', 'km-KH', 'kn-IN', 'ko-KR', 'ky-KG', 'lb-LU', 'lo-LA', 'lt-LT', 'lv-LV', 'mi-NZ', 'mk-MK', 'ml-IN', 'mn-MN', 'mr-IN', 'ms-BN', 'ms-MY', 'mt-MT', 'nb-NO', 'ne-NP', 'nl-BE', 'nl-NL', 'nn-NO', 'or-IN', 'pa-IN', 'pl-PL', 'ps-AF', 'pt-BR', 'pt-PT', 'rm-CH', 'ro-RO', 'ru-RU', 'rw-RW', 'sa-IN', 'se-FI', 'se-NO', 'se-SE', 'si-LK', 'sk-SK', 'sl-SI', 'sq-AL', 'sr-Cyrl-BA', 'sr-Cyrl-RS', 'sr-Latn-BA', 'sr-Latn-RS', 'sv-FI', 'sv-SE', 'sw-KE', 'ta-IN', 'te-IN', 'th-TH', 'tk-TM', 'tr-TR', 'tt-RU', 'uk-UA', 'ur-PK', 'uz-Cyrl-UZ', 'uz-Latn-UZ', 'vi-VN', 'wo-SN', 'xh-ZA', 'yo-NG', 'zh-CN', 'zh-HK', 'zh-MO', 'zh-SG', 'zh-TW', 'zu-ZA')]
        [string]$Language,

        [Parameter(Mandatory = $false, ParameterSetName = 'Identity')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ByDomain')]
        [Parameter(Mandatory = $false, ParameterSetName = 'AllMailboxes')]
        [ValidateSet('+1', '+2', '+3', '+4', '+5', '+6', '+7', '+8', '+9', '+10', '+11', '+12', '0', '-1', '-2', '-3', '-4', '-5', '-6', '-7', '-8', '-9', '-10', '-11', '-12')]
        [string]$TimeZone,

        [Parameter(Mandatory = $false, ParameterSetName = 'Identity')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ByDomain')]
        [Parameter(Mandatory = $false, ParameterSetName = 'AllMailboxes')]
        [Parameter(Mandatory = $false)]
        [string]$DateFormat,

        [Parameter(Mandatory = $false, ParameterSetName = 'Identity')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ByDomain')]
        [Parameter(Mandatory = $false, ParameterSetName = 'AllMailboxes')]
        [Parameter(Mandatory = $false)]
        [string]$TimeFormat,

        [Parameter(Mandatory = $false)]
        [switch]$OnlyIfLanguageEmpty,

        [Parameter(Mandatory = $false)]
        [switch]$GenerateCmdlets,

        [Parameter(Mandatory = $false)]
        [string]$OutputFile = "$(Get-Date -Format 'yyyy-MM-dd_HHmmss')-SetMailboxRegionalConfig_Commands.txt"
    )

    # Time zone mappings for simplified input (e.g., '+1' -> 'Romance Standard Time')
    # Based on Exchange data from https://github.com/bastienperez/AD-Exchange-M365-Localization/blob/main/Exchange_TimeZones.csv
    $TimeZoneMappings = @{
        '+1'  = 'Romance Standard Time'
        '+2'  = 'GTB Standard Time'
        '+3'  = 'Arab Standard Time'
        '+4'  = 'Arabian Standard Time'
        '+5'  = 'West Asia Standard Time'
        '+6'  = 'Central Asia Standard Time'
        '+7'  = 'SE Asia Standard Time'
        '+8'  = 'China Standard Time'
        '+9'  = 'Tokyo Standard Time'
        '+10' = 'AUS Eastern Standard Time'
        '+11' = 'Central Pacific Standard Time'
        '+12' = 'New Zealand Standard Time'
        '0'   = 'GMT Standard Time'
        '-1'  = 'Azores Standard Time'
        '-2'  = 'Mid-Atlantic Standard Time'
        '-3'  = 'E. South America Standard Time'
        '-4'  = 'Atlantic Standard Time'
        '-5'  = 'Eastern Standard Time'
        '-6'  = 'Central Standard Time'
        '-7'  = 'Mountain Standard Time'
        '-8'  = 'Pacific Standard Time'
        '-9'  = 'Alaskan Standard Time'
        '-10' = 'Hawaiian Standard Time'
        '-11' = 'UTC-11'
        '-12' = 'Dateline Standard Time'
    }

    if ($PSCmdlet.ParameterSetName -eq 'ByDomain') {
        # we can't filter PrimarySmtpAddress with `-like '*domain'` so we first find mailboxes with emailaddresses matching the domain then filter primarySMTPAddress
        # https://michev.info/blog/post/2404/exchange-online-now-supports-the-like-operator-for-primarysmtpaddress-filtering-sort-of

        Write-Host -ForegroundColor Cyan "Searching mailboxes with PrimarySmtpAddress matching '*@$ByDomain'"
        $Mailboxes = Get-Mailbox -Filter "EmailAddresses -like '*@$ByDomain'" -ResultSize Unlimited | Where-Object { $_.PrimarySmtpAddress -like "*@$ByDomain" }
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'AllMailboxes') {
        $Mailboxes = Get-Mailbox -ResultSize Unlimited
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'FromCSV') {
        [System.Collections.Generic.List[PSCustomObject]]$Mailboxes = @()
        if (-not (Test-Path $FromCSV)) {
            Write-Error "CSV file not found: $FromCSV"
            return
        }

        $csvData = Import-Csv -Path $FromCSV

        foreach ($row in $csvData) {
            try {
                $Mailbox = Get-Mailbox -Identity $row.PrimarySmtpAddress -ErrorAction Stop
                $Mailbox | Add-Member -NotePropertyName 'CSVLanguage' -NotePropertyValue $row.Language -Force
                $Mailbox | Add-Member -NotePropertyName 'CSVTimeZone' -NotePropertyValue $row.TimeZone -Force

                if ($row.DateFormat) {
                    $Mailbox | Add-Member -NotePropertyName 'CSVDateFormat' -NotePropertyValue $row.DateFormat -Force
                }
                if ($row.TimeFormat) {
                    $Mailbox | Add-Member -NotePropertyName 'CSVTimeFormat' -NotePropertyValue $row.TimeFormat -Force
                }
                
                $Mailboxes.Add($Mailbox)
            }
            catch {
                Write-Warning "Mailbox not found: $($row.PrimarySmtpAddress)"
            }
        }
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'Identity') {
        [System.Collections.Generic.List[PSCustomObject]]$Mailboxes = @()

        foreach ($id in $Identity) {
            try {
                $mbx = Get-Mailbox -Identity $id -ErrorAction Stop
                $Mailboxes.Add($mbx)
            }
            catch {
                Write-Warning "Mailbox not found: $id"
            }
        }
    }
    else {
        Write-Warning 'ParameterSetName Identity is not supported in this function.'
        return 1
    }

    $commands = @()
    $totalCount = @($Mailboxes).Count
    $currentCount = 0

    foreach ($mbx in $Mailboxes) {
        $currentCount++

        # Check if OnlyIfLanguageEmpty is specified and skip if language is already configured
        if ($OnlyIfLanguageEmpty) {
            try {
                $currentConfig = Get-MailboxRegionalConfiguration -Identity $mbx.PrimarySmtpAddress -ErrorAction Stop
                if ($currentConfig.Language -and $currentConfig.Language -ne '') {
                    Write-Host "[$currentCount/$totalCount] $($mbx.PrimarySmtpAddress) - Skipped (Language already configured: $($currentConfig.Language))" -ForegroundColor Yellow
                    continue
                }
            }
            catch {
                Write-Warning "[$currentCount/$totalCount] $($mbx.PrimarySmtpAddress) - Could not retrieve current configuration: $($_.Exception.Message)"
                continue
            }
        }

        $cmdParams = @{
            Identity = $mbx.PrimarySmtpAddress
        }

        # Use CSV-specific settings if available, otherwise use parameter values
        if ($PSCmdlet.ParameterSetName -eq 'FromCSV') {
            if ($mbx.CSVLanguage) { 
                $cmdParams['Language'] = $mbx.CSVLanguage 
                # Auto-detect date and time formats based on language culture
            }

            if ($mbx.CSVDateFormat) {
                $cmdParams['DateFormat'] = $mbx.CSVDateFormat
            }

            if ($mbx.CSVTimeFormat) {
                $cmdParams['TimeFormat'] = $mbx.CSVTimeFormat
            }

            if ($mbx.CSVTimeZone) {
                $TZ = $mbx.CSVTimeZone
                if ($TimeZoneMappings.ContainsKey($TZ)) {
                    $TZ = $TimeZoneMappings[$TZ]
                }
                $cmdParams['TimeZone'] = $TZ
            }
        }
        else {
            if ($Language) {
                $cmdParams['Language'] = $Language
            }
            # Auto-detect date and time formats based on language culture
            if ($DateFormat) {
                $cmdParams['DateFormat'] = $DateFormat
            }

            if ($TimeFormat) {
                $cmdParams['TimeFormat'] = $TimeFormat
            }
        
            if ($TimeZone) {
                $TZ = $TimeZone
                if ($TimeZoneMappings.ContainsKey($TZ)) {
                    $TZ = $TimeZoneMappings[$TZ]
                }
                $cmdParams['TimeZone'] = $TZ
            }
        }

        $cmdletString = 'Set-MailboxRegionalConfiguration'
        
        $cmdParamstring = ($cmdParams.GetEnumerator() | ForEach-Object { 
                if ($_.Value -match '\s') {
                    "-$($_.Key) '$($_.Value)'"
                }
                else {
                    "-$($_.Key) $($_.Value)"
                }
            }) -join ' '

        # The LocalizeDefaultFolderName switch localizes the default folder names of the mailbox in the current or specified language.
        # You don't need to specify a value with this switch.
        $cmdParams['LocalizeDefaultFolderName'] = $true
        
        $cmdParamstring = ($cmdParams.GetEnumerator() | ForEach-Object { 
                if ($_.Key -eq 'LocalizeDefaultFolderName') {
                    "-$($_.Key)"
                }
                elseif ($_.Value -match '\s') {
                    "-$($_.Key) '$($_.Value)'"
                }
                else {
                    "-$($_.Key) $($_.Value)"
                }
            }) -join ' '
        
        $FullCommand = "$cmdletString $cmdParamstring"

        if ($GenerateCmdlets) {
            $Commands += $FullCommand
        }

        if (-not $GenerateCmdlets -and $PSCmdlet.ShouldProcess($mbx.PrimarySmtpAddress, 'Set regional configuration')) {
            Write-Host -ForegroundColor Cyan "[$currentCount/$totalCount] $($mbx.PrimarySmtpAddress) - Setting configuration..."
            try {
                Set-MailboxRegionalConfiguration @cmdParams -ErrorAction Stop
                Write-Host "[$currentCount/$totalCount] $($mbx.PrimarySmtpAddress) - Configuration applied successfully." -ForegroundColor Green
            }
            catch {
                Write-Host "[$currentCount/$totalCount] $($mbx.PrimarySmtpAddress) - $($_.Exception.Message)" -ForegroundColor Red
            }
        }
    }

    if ($GenerateCmdlets -and $Commands.Count -gt 0) {
        $Commands | Out-File -FilePath $OutputFile -Encoding UTF8
        Write-Host "Commands generated in file: $OutputFile" -ForegroundColor Cyan
    }
}