{"id":2190,"date":"2025-11-15T10:07:36","date_gmt":"2025-11-15T10:07:36","guid":{"rendered":"https:\/\/stevenwhiting.com\/blog\/?p=2190"},"modified":"2025-11-15T10:52:32","modified_gmt":"2025-11-15T10:52:32","slug":"powershell-to-change-all-users-unp-from-swhiting-to-steven-whiting","status":"publish","type":"post","link":"https:\/\/stevenwhiting.com\/blog\/?p=2190","title":{"rendered":"Powershell &#8211; to change all users UNP from swhiting to steven.whiting"},"content":{"rendered":"\n<p>Wasn&#8217;t perfect but did what needed. Changed all user names from swhiting to steven.whiting. Doesn&#8217;t change the login so can still use the old swhiting for logging in, was just easier. Despite the UPN changing, it oddly doesn&#8217;t update the Entra ID when it syncs to Azure. Still looking into that, but this ended up being for the best. Cause of the way our domain was originally setup you have to sign in as swhiting@stevenwhiting.co.uk so having to do the full steven.whiting@ would be annoying.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;#!\n.SYNOPSIS\n    Update UPN and Primary SMTP Address for specific users identified by sAMAccountName (hybrid AD + Entra ID).\n\n.DESCRIPTION\n    - Targets ONLY the sAMAccountNames you specify (hard list below).\n    - For each user, builds the new identity as firstname.surname@stevenwhiting.co.uk\n      based on their AD GivenName + Surname (lowercased; non-alphanumerics removed).\n    - Updates on-prem AD so hybrid sync flows changes to Entra ID\/Exchange Online:\n        * userPrincipalName (UPN)\n        * mail (primary SMTP)\n        * proxyAddresses (sets new Primary as 'SMTP:' and preserves old sAMAccountName as alias 'smtp:')\n        * mailNickname (left part)\n    - Writes a dated CSV log for auditing and rollback.\n    - Includes rollback mode.\n\n.PARAMETER AlsoSetExchangeOnline\n    Attempt to set primary SMTP in Exchange Online as well (OFF by default; hybrid sync usually overwrites EXO-only changes).\n\n.PARAMETER RollbackPath\n    Path to a previous CSV log from this script to revert changes.\n\n.NOTES\n    Author: Steven Whiting \u2013 Using Chat GPT so may not be perfect but it worked and did what I needed it to do. Probably better ways to do it. With this script you also have to manually edit the users you want changed. I quite liked that for safety.\n    Date:   2025-09-23\n\n    This script uses CmdletBinding(SupportsShouldProcess=$true).\n    Use -WhatIf on the script to preview, and -Confirm to be prompted.\n\t\n\t\n#>\n\n&#91;CmdletBinding(SupportsShouldProcess=$true)]\nparam(\n    &#91;switch]$AlsoSetExchangeOnline,\n    &#91;string]$RollbackPath\n)\n\nImport-Module ActiveDirectory -ErrorAction Stop\n\n# ---------- Helpers ----------\nfunction Write-Info($msg)  { Write-Host \"&#91;INFO ] $msg\" -ForegroundColor Cyan }\nfunction Write-Warn($msg)  { Write-Warning $msg }\nfunction Write-Err ($msg)  { Write-Host \"&#91;ERROR] $msg\" -ForegroundColor Red }\n\nfunction Get-PrimarySmtpFromProxies(&#91;string&#91;]]$Proxies) {\n    if (-not $Proxies) { return $null }\n    $p = $Proxies | Where-Object { $_ -cmatch '^SMTP:' } | Select-Object -First 1\n    if ($p) { return ($p -replace '^SMTP:','') }\n    return $null\n}\n\nfunction Build-NewAddressParts(&#91;string]$GivenName,&#91;string]$Surname,&#91;string]$Domain) {\n    if (&#91;string]::IsNullOrWhiteSpace($GivenName) -or &#91;string]::IsNullOrWhiteSpace($Surname)) {\n        throw \"GivenName\/Surname missing; cannot build firstname.surname.\"\n    }\n    $fn = ($GivenName  -replace \"&#91;^A-Za-z0-9]\", \"\").ToLower()\n    $sn = ($Surname    -replace \"&#91;^A-Za-z0-9]\", \"\").ToLower()\n    $local = (\"{0}.{1}\" -f $fn, $sn)\n    return @{ Local=$local; Upn=(\"{0}@{1}\" -f $local,$Domain); Primary=(\"{0}@{1}\" -f $local,$Domain); Nick=$local }\n}\n\nfunction Ensure-UniqueUpn(&#91;string]$CandidateUpn,&#91;string]$DomainNC,&#91;string]$UserDN) {\n    $exists = Get-ADUser -Filter (\"userPrincipalName -eq '{0}'\" -f $CandidateUpn) -SearchBase $DomainNC -ErrorAction SilentlyContinue\n    if (-not $exists -or ($exists.DistinguishedName -eq $UserDN)) { return $CandidateUpn }\n    $prefix,$suffix = $CandidateUpn.Split('@')\n    for ($i=1; $i -lt 1000; $i++) {\n        $try = \"{0}{1}@{2}\" -f $prefix,$i,$suffix\n        $exists = Get-ADUser -Filter (\"userPrincipalName -eq '{0}'\" -f $try) -SearchBase $DomainNC -ErrorAction SilentlyContinue\n        if (-not $exists -or ($exists.DistinguishedName -eq $UserDN)) { return $try }\n    }\n    throw \"Unable to find a unique UPN after 999 attempts for $CandidateUpn\"\n}\n\nfunction Set-UserMailAttributes {\n    &#91;CmdletBinding(SupportsShouldProcess=$true)]\n    param(\n        &#91;Microsoft.ActiveDirectory.Management.ADUser]$User,\n        &#91;string]$NewUpn,\n        &#91;string]$NewPrimarySmtp,\n        &#91;string]$NewMailNick,\n        &#91;string]$DomainSuffix\n    )\n\n    $dn = $User.DistinguishedName\n    $currentProxies = @($User.proxyAddresses)\n\n    # Demote any existing primary entries to alias form (lowercase)\n    $proxiesNoPrimary = @()\n    foreach ($p in $currentProxies) { $proxiesNoPrimary += ($p -replace '^SMTP:','smtp:') }\n\n    # Add old sAMAccountName as alias\n    $newAliasAdded = \"$($User.SamAccountName)@$DomainSuffix\"\n    if ($proxiesNoPrimary -notcontains (\"smtp:$newAliasAdded\")) {\n        Write-Info \"Adding old sAMAccountName as alias: smtp:$newAliasAdded\"\n        $proxiesNoPrimary += \"smtp:$newAliasAdded\"\n    }\n\n    # Remove duplicates and any lingering entry that collides with new primary\n    $proxiesNoPrimary = $proxiesNoPrimary | Sort-Object -Unique | Where-Object { $_ -ne (\"smtp:$NewPrimarySmtp\") -and $_ -ne (\"SMTP:$NewPrimarySmtp\") }\n\n    # Final list: new primary first, then aliases\n    $finalProxies = @(\"SMTP:$NewPrimarySmtp\") + $proxiesNoPrimary\n\n    # Ensure array of strings\n    if ($finalProxies -isnot &#91;array]) { $finalProxies = @($finalProxies) }\n    $finalProxies = $finalProxies | ForEach-Object { &#91;string]$_ }\n\n    $replace = @{\n        proxyAddresses = $finalProxies\n        mail           = $NewPrimarySmtp\n        mailNickname   = $NewMailNick\n    }\n\n    if ($PSCmdlet.ShouldProcess($User.SamAccountName, \"Set mail attributes and UPN\")) {\n        Set-ADUser -Identity $dn -UserPrincipalName $NewUpn -Replace $replace -ErrorAction Stop\n    }\n\n    return @{\n        NewProxies     = $finalProxies\n        NewAliasAdded  = \"smtp:$newAliasAdded\"\n    }\n}\n\n# ---------- Config ----------\n$DomainSuffix = 'stevenwhiting.co.uk'\n$DomainNC     = 'DC=stevenwhiting,DC=co,DC=uk'\n$SearchBaseDN = 'OU=IT,OU=formation,OU=.Users,OU=Steven Whiting,DC=stevenwhiting,DC=co,DC=uk'\n$SamAccountNames = @('jwhiting','ldave','swhiting')\n\n# ---------- Resolve targets ----------\n$ResolvedUsers = @()\nforeach ($sam in $SamAccountNames) {\n    $found = Get-ADUser -Filter \"SamAccountName -eq '$sam'\" -SearchBase $SearchBaseDN -Properties GivenName,Surname,mail,mailNickname,proxyAddresses,DistinguishedName,SamAccountName,userPrincipalName\n    if (-not $found) { Write-Err \"User not found in OU scope: $sam\"; continue }\n    if ($found.Count -gt 1) { Write-Err \"Multiple matches for sAMAccountName $sam in the scope. Aborting for safety.\"; exit 1 }\n    $ResolvedUsers += $found\n}\n\nif ($ResolvedUsers.Count -ne $SamAccountNames.Count) {\n    Write-Err \"Expected to resolve $($SamAccountNames.Count) users, but resolved $($ResolvedUsers.Count). Aborting.\"\n    return\n}\n\n# Confirmation\nWrite-Host \"The following users will be updated:\" -ForegroundColor Yellow\n$ResolvedUsers | Select-Object SamAccountName, UserPrincipalName, GivenName, Surname, DistinguishedName | Format-Table -AutoSize\n$ans = Read-Host \"Type YES to proceed (anything else aborts)\"\nif ($ans -ne 'YES') { Write-Host 'Aborted by operator.'; return }\n\n# ---------- Processing ----------\n$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'\n$LogPath   = Join-Path -Path (Get-Location) -ChildPath (\"UPN_SMTP_Change_Log_{0}.csv\" -f $timestamp)\n$logRows   = @()\n\nforeach ($user in $ResolvedUsers) {\n    try {\n        $parts = Build-NewAddressParts -GivenName $user.GivenName -Surname $user.Surname -Domain $DomainSuffix\n        $targetUpn = Ensure-UniqueUpn -CandidateUpn $parts.Upn -DomainNC $DomainNC -UserDN $user.DistinguishedName\n        $oldPrimarySmtp = Get-PrimarySmtpFromProxies $user.proxyAddresses\n        if (-not $oldPrimarySmtp -and $user.mail) { $oldPrimarySmtp = $user.mail }\n\n        # Skip if already changed\n        if ($user.UserPrincipalName -eq $targetUpn -and $oldPrimarySmtp -eq $parts.Primary) {\n            Write-Host \"$($user.SamAccountName) is already updated. Skipping...\" -ForegroundColor Yellow\n            $logRows += &#91;pscustomobject]@{\n                When                = (Get-Date)\n                DN                  = $user.DistinguishedName\n                SamAccountName      = $user.SamAccountName\n                DisplayName         = $user.Name\n                OldUPN              = $user.UserPrincipalName\n                NewUPN              = $targetUpn\n                OldPrimarySMTP      = $oldPrimarySmtp\n                NewPrimarySMTP      = $parts.Primary\n                FinalProxyAddresses = ($user.proxyAddresses -join ';')\n                NewAliasAdded       = \"$($user.SamAccountName)@$DomainSuffix\"\n                Result              = \"Already changed\"\n            }\n            continue\n        }\n\n        Write-Info (\"Processing {0}:\" -f $user.SamAccountName)\n        $result = Set-UserMailAttributes -User $user -NewUpn $targetUpn -NewPrimarySmtp $parts.Primary -NewMailNick $parts.Nick -DomainSuffix $DomainSuffix\n\n        Write-Host \"  Final ProxyAddresses:\"\n        foreach ($proxy in $result.NewProxies) {\n            if ($proxy -cmatch '^SMTP:') { Write-Host \"    $proxy (Primary)\" -ForegroundColor Green }\n            else { Write-Host \"    $proxy (Alias)\" -ForegroundColor DarkGray }\n        }\n        Write-Host \"  New alias added: $($result.NewAliasAdded)\" -ForegroundColor Magenta\n\n        $logRows += &#91;pscustomobject]@{\n            When                = (Get-Date)\n            DN                  = $user.DistinguishedName\n            SamAccountName      = $user.SamAccountName\n            DisplayName         = $user.Name\n            OldUPN              = $user.UserPrincipalName\n            NewUPN              = $targetUpn\n            OldPrimarySMTP      = $oldPrimarySmtp\n            NewPrimarySMTP      = $parts.Primary\n            FinalProxyAddresses = ($result.NewProxies -join ';')\n            NewAliasAdded       = $result.NewAliasAdded\n            Result              = \"Success\"\n        }\n    }\n    catch {\n        Write-Err $_.Exception.Message\n    }\n}\n\n$logRows | Export-Csv -NoTypeInformation -Path $LogPath\nWrite-Info \"Log written to: $LogPath\"\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Wasn&#8217;t perfect but did what needed. Changed all user names from swhiting to steven.whiting. Doesn&#8217;t change the login so can still use the old swhiting for logging in, was just easier. Despite the UPN changing, it oddly doesn&#8217;t update the &hellip; <a href=\"https:\/\/stevenwhiting.com\/blog\/?p=2190\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[346],"tags":[333],"class_list":["post-2190","post","type-post","status-publish","format-standard","hentry","category-powershell","tag-powershell"],"_links":{"self":[{"href":"https:\/\/stevenwhiting.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/2190","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/stevenwhiting.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/stevenwhiting.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/stevenwhiting.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/stevenwhiting.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2190"}],"version-history":[{"count":2,"href":"https:\/\/stevenwhiting.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/2190\/revisions"}],"predecessor-version":[{"id":2194,"href":"https:\/\/stevenwhiting.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/2190\/revisions\/2194"}],"wp:attachment":[{"href":"https:\/\/stevenwhiting.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2190"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/stevenwhiting.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2190"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/stevenwhiting.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2190"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}