Updating Microsoft 365 User Accounts to use a New Domain

Update User Email Addresses and User Principal Names

A recent reader question asked about the best way to update a bunch of user accounts after the organization buys a new vanity domain and wants the domain used for email addresses and user principal names (sign-in addresses). This sometimes happens when a business goes through a rebranding exercise and ends up with a new name. The requirement to update email addresses and user principal names also occurs in tenant-to-tenant migrations.

Tenant-to-tenant migrations are a specialized kind of activity that’s usually managed with software built for this purpose. We won’t plunge into the challenges that these projects can encounter. Instead, we’ll focus on the scenario where someone in authority decides that all accounts should use different email addresses and user principal names.

Registered Domains

The first requirement is to add the domain to Office 365. Until this is done, you cannot use the domain. Once the domain is known to the tenant, it appears in the set of verified domains seen in the Microsoft 365 admin center (Figure 1).

Verified domains in a Microsoft 365 tenant.
Figure 1: Verified domains in a Microsoft 365 tenant

After verifying the domain for Microsoft 365, we can write some code to ask the administrator what domain to use. Here’s an example that uses the Get-MgOrganization cmdlet from the Microsoft Graph PowerShell SDK to fetch the verified domains:

Connect-MgGraph -Scopes Directory.Read.All, User.ReadWrite.All
# Get tenant information and the verified domains for the tenant
$TenantInfo = (Get-MgOrganization)
[array]$Domains = $TenantInfo.VerifiedDomains.Name
$DomainsList = $Domains -join ", "
Write-Host "Verified domains for this tenant:"
Write-Host "---------------------------------"
Write-Host ""
$Domains
Write-Host ""
$DomainToUse = Read-Host "What domain do you want to use for primary SMTP addresses and UPNs"
Write-Host ""
If ($DomainToUse -notin $Domains) {Write-Host ("The selected domain ({0}) is not in the set supported by the tenant ({1}). Please try again." -f $DomainToUse, $DomainsList); break }
$CompareDomain = "*" + $DomainToUse + "*"

Finding Mail Recipients

The next step is to find mail-enabled recipients that have email addresses that might need updating. This code finds user mailboxes, shared mailboxes, group mailboxes (for Microsoft 365 groups), distribution lists, and security-enabled distribution lists.

For each object, the code calculates a new primary SMTP address based on their existing address by swapping the existing domain for the new domain. A check makes sure that the new address isn’t already in use, and if it is, creates a new address by adding “.EXO” to the username. The code then checks if it’s necessary to update the user principal name for the Entra ID accounts used by user mailboxes and shared mailboxes. An account might already use the new domain, so the code checks the account’s current user principal name and updates it with the new domain if necessary.

The output is captured in a PowerShell list that’s exported to a CSV file.

If (!($DomainToUse)) {
   Write-Host "No domain to move to is defined. Please make sure that the $DomainToUse variable is defined"
   Break
} Else {
   Write-Host ("Processing accounts to move them to the {0} domain..." -f $DomainToUse)
}

[array]$Recipients = Get-Recipient -ResultSize Unlimited -RecipientTypeDetails UserMailbox, SharedMailbox, GroupMailbox, MailUniversalDistributionGroup, MailUniversalSecurityGroup, DynamicDistributionGroup

$i = 0
$Report = [System.Collections.Generic.List[Object]]::new()
ForEach ($R in $Recipients) {
     $i++
     If ($R.PrimarySmtpAddress.Split("@")[1] -ne $DomainToUse) { #Need to process this mailbox
      Write-Host ("Processing {0} {1} ({2}/{3})" -f $R.RecipientTypeDetails, $R.DisplayName, $i, $Recipients.Count)
      $NewUPN = $Null
      # Figure out new email address
      $NewAddress = $R.Alias + "@" + $DomainToUse
      # Check that the address is available
      $Status = Get-Recipient -Identity $NewAddress -ErrorAction SilentlyContinue
      # If we get a status the recipient address already exists, so create a new address
      If ($Status) { $NewAddress = $M.Alias + ".EXO@" + $DomainToUse }
      
      # Figure out if the account's user principal name needs to change
      If ($R.RecipientType -eq "SharedMailbox" -or $R.RecipientType -eq "UserMailbox") {
        $UPNDomain = $R.WindowsLiveId.Split("@")[1]
        If ($UPNDomain -ne $DomainToUse) { # New UPN needed
          $NewUPN = $R.WindowsLiveId.Split("@")[0] + "@" + $DomainToUse
          $Status = Get-MgUser -UserId $NewUPN -ErrorAction SilentlyContinue
          If ($Status) { # UPN already exists, so create a new one
            $NewUPN = $R.WindowsLiveId.Split("@")[0] + ".EXO@" + $DomainToUse }
          }
       }

      $ReportLine   = [PSCustomObject] @{ 
         DisplayName            = $R.DisplayName
         OldUPN                 = $R.WindowsLiveId
         NewUPN                 = $NewUPN
         PrimarySmtpAddress     = $R.PrimarySmtpAddress
         NewAddress             = $NewAddress
         Type                   = $R.RecipientTypeDetails
         Alias                  = $R.Alias
    }
    $Report.Add($ReportLine) }
}
$Report = $Report | Sort-Object Type
$Report | Export-CSV -NoTypeInformation c:\temp\MailObjects.Csv

Administrators can check the CSV to remove any mail-enabled recipients they don’t want to receive new email addresses (Figure 2).

Update User Email Addresses with a New Domain

The next step is reads in and processes an array of objects from the updated CSV file.

The code uses a Switch statement to check the object type and calls the appropriate cmdlet to assign the new primary SMTP address to the object. If the account used for a mailbox (user or shared) requires an update for its user principal name, we go ahead and do it.

The final step in the loop through the objects is to report what’s been done, noting the old and new SMTP address and the old and new user principal name.

# Process mail objects array to update primary SMTP addresses and UPNs as necessary
[array]$MailObjects = Import-CSV MailObjects.CSV
$Report = [System.Collections.Generic.List[Object]]::new()

Write-Host "Processing mail-enabled objects..."
$i = 0
ForEach ($M in $MailObjects)  {
   $i++
   Write-Host ("Processing {0} {1} ({2}/{3})" -f $M.Type, $M.DisplayName, $i, $MailObjects.Count)

   # Assign new primary SMTP Address
   Switch ($M.Type) {
      "DynamicDistributionGroup" { # Dynamic distribution list
        Set-DynamicDistributionGroup -Identity $M.PrimarySmtpAddress -PrimarySmtpAddress $NewAddress
     }
      "GroupMailbox" { # Microsoft 365 group
        Set-UnifiedGroup -Identity $M.PrimarySmtpAddress -PrimarySmtpAddress $NewAddress
     }
      "MailUniversalDistributionGroup" { # Distribution list
        Set-DistributionGroup -Identity $M.PrimarySmtpAddress -PrimarySmtpAddress $NewAddress
     }
      "MailUniversalSecurityGroup" { #Mail-enabled security group
        Set-DistributionGroup -Identity $M.PrimarySmtpAddress -PrimarySmtpAddress $NewAddress
     }
      "SharedMailbox" { # Shared mailbox
        Set-Mailbox -Identity $M.PrimarySmtpAddress -WindowsEmailAddress $NewAddress 
     }
      "UserMailbox" { # User mailbox
        Set-Mailbox -Identity $M.PrimarySmtpAddress -WindowsEmailAddress $NewAddress 
     }
    }

   # Update UPN if necessary
   If ($M.NewUPN) {  
     Update-MgUser -UserId $M.UPN -UserPrincipalName $M.NewUPN }

   $ReportLine   = [PSCustomObject] @{ 
          DisplayName            = $M.DisplayName
          OldUPN                 = $M.UPN
          NewUPN                 = $M.NewUPN
          OldPrimarySmtpAddress  = $M.PrimarySmtpAddress
          NewPrimarySmtpAddress  = $M.NewAddress
          Type                   = $M.Type
          Alias                  = $M.Alias
    }
    $Report.Add($ReportLine) 

} # End ForEach 
Write-Host "All done!"

Figure 3 shows an example of the report that allows administrators to check that the expected email addresses and user principal names are in place.

The updated accounts with new primary SMTP addresses and some new user principal names
Figure 3: The updated accounts with new primary SMTP addresses and some new user principal names

The User Issue

Updated user principal names take effect the next time users sign in. If you want to force the switchover, you could disconnect users from their current sessions by invalidating refresh tokens using the Graph revokeSignInSessions API. Invaliding access tokens forces users to reauthenticate and reconnect, and to do that, they must use their new user principal names.

Be aware that some issues exist when changing user principal names such as the need to set up the new user principal name on the Microsoft Authenticator app so that MFA challenges work It’s worthwhile reading through this Microsoft article to understand and test problems that users might encounter in your organization. Knowing what might happen and being prepared to fix the issues will ensure a smoother transition.

Any change to the way people sign-in is likely to cause some angst if it’s not communicated clearly so that everyone understands why the change is happening and what they must do to sign-in to access services.

Tidying Up Entra ID

The process outlined above takes care of the bulk of the work. If some Entra ID accounts that don’t have email addresses need to receive updated user principal names, you can do this with the Update-MgUser cmdlet from the Microsoft Graph PowerShell SDK.

Giving accounts new email addresses and user principal names isn’t a difficult technical challenge. The likely problems arise in preparation and communication. Isn’t that always the way?


Keep up with the changing world of the Microsoft 365 ecosystem by subscribing to the Office 365 for IT Pros eBook. Monthly updates mean that our subscribers learn about new developments as they happen.

15 Replies to “Updating Microsoft 365 User Accounts to use a New Domain”

  1. Keep in mind that changing UserPrincipalName also changes the OneNote url. So if users have OneNote open in the desktop app they need to close the document and open it again with the new url, otherwise changes will not sync.

  2. Hi Tony, can I use this script for hybrid AD objects that live on prem? My users live on-prem but I would like to change everyone’s primary email domain using this script.

    1. I would do some testing before being sure that everything will work as it does online. Remember, the mail attributes for hybrid users must be managed on-premises, and some of the cmdlets used in the script won’t work on-premises.

  3. Hi it seems like MgUser returns empty in the end, how can i replace that?

    “Update-MgUser : Cannot bind argument to parameter ‘UserId’ because it is an empty string.”

  4. HI there, THANKS so much for this script! I am getting an error in the final stage when it is updating the UPNs:
    Write-ErrorMessage : The email address “.EXO@xxx.com” isn’t correct. Please use this format: user name, the @ sign, followed by the domain name. For example,
    tonysmith@contoso.com or tony.smith@contoso.com.
    At C:\Users\admin\AppData\Local\Temp\tmpEXO_kjc1hqr5.e2o\tmpEXO_kjc1hqr5.e2o.psm1:1120 char:13
    + Write-ErrorMessage $ErrorObject
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Set-DistributionGroup], Exception
    + FullyQualifiedErrorId : [Server=YQBPR0101MB8800,RequestId=14aded9c-f63a-3f95-9c81-c9824889d20e,TimeStamp=Sat, 20 May 2023 07:37:29 GMT],Write-ErrorMessage

    1. The new mail address generated by the script doesn’t seem to include a first name and last name part. You’ll have to debug the script from this point and find out why this is happening.

  5. I am receiving the same error as Ken. just as a test I ran the first two scripts and then I grabbed the value of $NewAddress variable thats listed in the third script. Its value is “.EXO@domainname.co” so that makes sense in regards to the error. i am not sure how to fix this script to make it work as that type of work is above my pay grade, LOL!

    1. Does the CSV file generated by the script that’s the input to the last part where addresses are reset contain all the required information? I’m suspicious that the NewUPN column might not be populated, maybe because the $DomainToUse variable wasn’t set when generating the CSV.

  6. Its definitely populated with the correct info. Frankly, I could cobble something together to complete the task but it would be no where near as elegant what you are doing, LOL!

  7. Great script! Like others above it error’d for me, and I think it’s because variables in the final “update users” script aren’t coming from the imported CSV but instead.

    To fix, simply update all the variables in the final loop from eg $NewUPN to $M.NewUPN (and same for NewAddress, etc). Make sure any variable that has the same name as a header in the CSV file is prefixed with “M.”, there are quite a few! Then it works beautifully – thank you so much for sharing.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.