Deploy New Teams with Intune
I have done lots of deployment of Microsoft Teams classic to Intune clients over the years. And it has not been crystal clear on how to do it. There is a wide choice of installers like the machine-wide installer MSI, the single user EXE and finally the M365 apps deployment. So now that the New Teams client is released and will replace the old. How do we deploy New Teams?
Microsoft Teams classic Machine Wide Installer MSI
This model is now retired. The Microsoft Teams machine-wide installer uses the MSI method to install Teams for all users on a computer in the Program Files directory. When a user login and start Teams, the Classic Teams installer will run and install Teams from the program files directory (%programfiles%\Teams installer\teams.exe) and install a personal installation into the user profile. Classic Teams in the personal profile was auto updated but the Teams wide installer was static and not easy to update, so every user installing Teams from program files got an old version that needed to be updated. There has been some trouble when the Teams Wide Installer got to old and needed a replacement.
Microsoft Teams classic installer EXE
This model is now retired. The old Teams.exe and also update.exe installer will install Teams only for the logged on user. Teams will be installed in the personal profile and no other user on the same computer will have Teams Installed. Teams in the personal profile was auto updated.
Microsoft Teams classic included in M365 Apps package
You could also deploy Microsoft Classic Teams together with M365 Apps deployment package. In Intune we have a predefined package to deploy M365 apps. In that package, when Skype was deprecated, Teams was added as an application to deploy together with M365 apps. Sounds great! But now again, Teams is installed with Classic Teams machine wide installer MSI and we have some difficulties keeping it up to date.
New Teams MSIX
In new Teams we have an MSIX installer. If you run this installer, it will install New Teams only for the user running the installation in the user profile. We can as an admin install an MSIX for all users with for example DISM.exe. But don´t go there. Use the New Teams Bootstrapper instead.
New Teams Bootstrapper EXE
In new Teams we have a bootstrapper instead of a machine wide installer. It installs the Teams MSIX package on the target computer placing the installation in C:\Program Files\WindowsApps\PublisherName.AppName_AppVersion_architecture_PublisherID. The difference here is the package type that will be placed in that folder, MSI vs MSIX. The old MSI did not update while the MSIX will auto update the package and always keep it up to date. All users will have access to the central MSIX installation and new users logging in will get the latest version of Teams. The personal profile will store only the settings and cache. You can basically package teamsbotstrapper.exe in a win32app and then run install command: .\teamsbootstrapper.exe -p and Uninstall command: .\teamsbootstrapper.exe -x
New Teams M365 Apps package
You can still deploy Teams in the M365 apps package. New installations of the M365 apps will now install new Teams. But heads up! You need an active internet connection to install Teams, it´s not included in the package downloaded for M365 apps. it´s a separate download. When we do this with Intune, this is probably no problem. We must have an internet connection to connect to Intune to get the initial M365 package, so no problem there. But good to know if you deploy with GPO or other methods. Another thing to notice is that New Teams seems to be a bit delayed when distributing this way. Probably because it has to install M365 apps first and then download and install Teams. And also, if you already have Classic Teams it will remain installed and show the toggle “Try the new Teams”. This is probably temporary until Classic Teams is deprecated.
Deployment of New Teams with Intune
So, when it comes to deploy the new teams with Intune, what is the best model? Right now I prefer using a scripted installation with the Bootstrapper. You can also do it with M365 apps package deployment, but there is a risk that the Classic Teams will remain installed. So right now I prefer a custom win32app with a PowerShell script that first make sure to uninstall the Classic Teams and then install the New Teams with the bootstrapper method. This makes the installation smooth and the platform clean.
The Win32app
To install the win32app build a Intunewin package with the following files:
Intune-NewTeamsInstaller.ps1
teamsbootstrapper.exe
Win32app config
Deploy the intunewin as a win32app with the folowing config:
Install command: PowerShell.exe -ExecutionPolicy Bypass -File .\Intune-NewTeamsInstaller.ps1
Uninstall command: .\teamsbootstrapper.exe -x
Install behavior: System
Operating system architecture: x64 & x86
Minimum operating system: Windows 11 21H2
Detection rules: Use a custom detection script
Run script as 32-bit process on 64-bit clients: No
Enforce script signature check and run script silently: No
Detect The New Teams installation script
When deploying win32apps we need to detect if app is already installed or if installation is needed. A check is also done after installation to determine if app was installed. Teams installations can be detected by getting the appx packages installed. I have written 2 different detection script for New Teams for Intune deployments. One that detect only new Teams and one that detects if Old Classic Teams need to be cleaned up.
Detect both if old classic Teams is installed and new Teams is missing
$NewTeams = $null
$OldTeams = $null
$OldTeamsMachineWide = $null
$windowsAppsPath = "C:\Program Files\WindowsApps"
$NewTeamsSearch = "MSTeams_*_x64__*"
$NewTeams = Get-ChildItem -Path $windowsAppsPath -Directory -Filter $NewTeamsSearch -ErrorAction SilentlyContinue
$OldTeams = Get-AppxPackage "Teams*" -AllUsers
ForEach ( $Architecture in "SOFTWARE", "SOFTWARE\Wow6432Node" ) {
$UninstallKey = "HKLM:$Architecture\Microsoft\Windows\CurrentVersion\Uninstall"
if (Test-path $UninstallKey) {
$OldTeamsMachineWide = Get-ChildItem -Path $UninstallKey | Get-ItemProperty | Where-Object {$_.DisplayName -match "Teams Machine-Wide Installer" } |Select-Object PSChildName -ExpandProperty PSChildName
}
}
If ($OldTeamsMachineWide) {Write-Host "Old Teams Machine wide installer found";exit 1} #teams machine wide installer is installed will be uninstalled
elseif ($OldTeams) {Write-Host "Old Teams found";exit 1} #old teams is installed will be uninstalled
elseif ($NewTeams) {Write-Host "New Teams found";exit 0} #new teams is installed all is good
else {Write-Host "Failed detection of Teams";exit 1}
Detect only if new Teams is installed
$NewTeams = $null
$windowsAppsPath = "C:\Program Files\WindowsApps"
$NewTeamsSearch = "MSTeams_*_x64__*"
$NewTeams = Get-ChildItem -Path $windowsAppsPath -Directory -Filter $NewTeamsSearch -ErrorAction SilentlyContinue
if ($NewTeams ) {Write-Host "New Teams found";exit 0}
else {Write-Host "New Teams not found";exit 1}
The New Teams installation (and cleanup classic Teams) script
This is the script I´m using right now, and it seems stable where implemented, also available on my Github:
<#PSScriptInfo
.SYNOPSIS
Script for Intune
.DESCRIPTION
This script will remove the old Classic Teams and install the New Teams.
.EXAMPLE
.\Intune-NewTeamsInstaller.ps1
Will uninstall the old Classic Teams and install the New Teams.
.NOTES
Written by Mr-Tbone (Tbone Granheden) Coligo AB
torbjorn.granheden@coligo.se
.VERSION
1.0
.RELEASENOTES
1.0 2024-02-19 Initial Build
.AUTHOR
Tbone Granheden
@MrTbone_se
.COMPANYNAME
Coligo AB
.GUID
00000000-0000-0000-0000-000000000011
.COPYRIGHT
Feel free to use this, But would be grateful if My name is mentioned in Notes
.CHANGELOG
1.0.2402.1 - Initial Version
#>
#region ---------------------------------------------------[Set script requirements]-----------------------------------------------
#
#Requires -Version 4.0
#Requires -RunAsAdministrator
#endregion
#region ---------------------------------------------------[Script Parameters]-----------------------------------------------
#endregion
#region ---------------------------------------------------[Modifiable Parameters and defaults]------------------------------------
[string]$NewTeamsInstaller = ".\teamsbootstrapper.exe" #Path to the new Teams installer
[string]$NewTeamsInstallerArgs = "-p" #Arguments for the new Teams installer
[bool]$RemovePersonalTeams = $true #Remove Personal Teams to avoid confution between Corporate and Personal Teams
[string]$TaskName = "Intune-NewTeamsInstaller"#TaskName for the eventlog
[string]$CorpDataPath = "C:\ProgramData\MrTbone" #Path for the logfiles
#endregion
#region ---------------------------------------------------[Set global script settings]--------------------------------------------
Set-StrictMode -Version Latest
#endregion
#region ---------------------------------------------------[Static Variables]------------------------------------------------------
#Log File Info
[string]$logpath = "$($CorpDataPath)\logs"
[string]$LogFile = "$($logpath)\$($TaskName)$(Get-Date -Format 'yyyyMMdd')$(Get-Date -format 'HHmmss').log"
#endregion
#region ---------------------------------------------------[Import Modules and Extensions]-----------------------------------------
#endregion
#region ---------------------------------------------------[Functions]------------------------------------------------------------
function Write-ToEventlog {
Param(
[string]$Logtext,
[string]$EventSource,
[int]$Global:EventId,
[validateset("Information", "Warning", "Error")]$Global:EventType = "Information"
)
Begin {}
Process {
if ([bool]($(whoami -user) -match "S-1-5-18")){
if (!([System.Diagnostics.EventLog]::SourceExists($EventSource))) {
New-EventLog -LogName 'Application' -Source $EventSource -ErrorAction ignore | Out-Null
}
}
Write-EventLog -LogName 'Application' -Source $EventSource -EntryType $Global:EventType -EventId $Global:EventId -Message $Logtext -ErrorAction ignore | Out-Null
}
End {}
}
function Uninstall-ProgramFromRegKey {
Param(
$AppName
)
Begin {}
Process {
ForEach ( $Architecture in "SOFTWARE", "SOFTWARE\Wow6432Node" ) {
$UninstallKey = "HKLM:$Architecture\Microsoft\Windows\CurrentVersion\Uninstall"
if (Test-path $UninstallKey) {
$UninstallInfo = Get-ChildItem -Path $UninstallKey | Get-ItemProperty | Where-Object { $_.PSObject.Properties.Name -contains 'DisplayName' -and $_.DisplayName -match $AppName } | Select-Object PSChildName -ExpandProperty PSChildName
If ( $UninstallInfo ) {
$UninstallInfo | ForEach-Object {
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Trying to Uninstall: $(( Get-ItemProperty "$UninstallKey\$_" ).DisplayName)"
$process = Start-Process -Wait -FilePath "MsiExec.exe" -ArgumentList "/X $_ /qn /norestart" -PassThru
if ($process.ExitCode -ne 0) {
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to Uninstall with exit code $($process.ExitCode)."
}
}
}
}
else{
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,No Uninstall keys found for $($AppName)"
}
}
}
End {}
}
function Uninstall-ProgramFromFolder {
Param(
[string]$Path,
[string]$uninstaller,
[string]$UninstallArgs
)
Begin {}
Process {
$uninstallerpath = Join-path $Path $uninstaller
if (test-path $uninstallerpath) {
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Trying to Uninstall $($uninstallerpath)"
$process = Start-Process -FilePath "$($uninstallerpath)" -ArgumentList "$($UninstallArgs)" -PassThru -Wait -ErrorAction STOP
if ($process.ExitCode -ne 0) {
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to Uninstall $($uninstallerpath) with exit code $($process.ExitCode)."
}
try {Remove-Item -Path $Path -Force -Recurse -ErrorAction SilentlyContinue
Write-Verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to remove $($Path)"}
catch {write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to remove $($Path)"}
}
else {
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,No program found with $($uninstallerpath)"
}
}
End {}
}
function Remove-DesktopShortcuts {
Param(
[string]$ShortcutName
)
Begin {}
Process {
$desktopPath = [System.Environment]::GetFolderPath("Desktop")
$desktopShortcuts = Get-ChildItem -Path $desktopPath -Filter "*.lnk" -Recurse
foreach ($shortcut in $desktopShortcuts) {
$shell = New-Object -ComObject WScript.Shell
$targetPath = $shell.CreateShortcut($shortcut.FullName).TargetPath
if ($targetPath -like "*$ShortcutName*") {
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Trying to Delete shortcut: $($shortcut.FullName)"
try{Remove-Item -Path $shortcut.FullName -Force
Write-Verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to remove $($shortcut.FullName)"}
catch{write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to remove $($shortcut.FullName)"}
}
}
}
End {}
}
function Remove-StartMenuShortcuts {
Param(
[string]$ShortcutName
)
Begin {}
Process {
$startMenuPath = [System.Environment]::GetFolderPath("StartMenu")
$startMenuShortcuts = Get-ChildItem -Path $startMenuPath -Filter "*.lnk" -Recurse
foreach ($shortcut in $startMenuShortcuts) {
$shell = New-Object -ComObject WScript.Shell
$targetPath = $shell.CreateShortcut($shortcut.FullName).TargetPath
if ($targetPath -like "*$ShortcutName*") {
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Trying to Delete shortcut: $($shortcut.FullName)"
try{Remove-Item -Path $shortcut.FullName -Force
Write-Verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to remove $($shortcut.FullName)"}
catch{write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to remove $($shortcut.FullName)"}
}
}
}
End {}
}
function Remove-RegistryKeys {
param (
[string]$registryPath,
[string[]]$keyNames
)
Begin {}
Process {
# Loop through each key
foreach ($keyName in $keyNames) {
# Check if the key exists
if (Test-Path -Path "$registryPath\$keyName") {
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Trying to remove $($registryPath)\$($keyName)"
try {Remove-ItemProperty -Path $registryPath -Name $keyName -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Write-Verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to remove $($registryPath)\$($keyName)"}
catch{write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to remove $($registryPath)\$($keyName)"}
}
}
}
End {}
}
#endregion
#region ---------------------------------------------------[[Script Execution]------------------------------------------------------
Start-Transcript -Path $LogFile
# Stop all Classic Teams processes
try{get-process "teams*" | stop-process -Force -ErrorAction SilentlyContinue
Write-Verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to stop all Teams processes"}
catch{write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to stop all Teams processes"}
#Remove Personal Teams to avoid confusion
if ($RemovePersonalTeams) {
# Uninstall Teams from the user profile
try{Get-AppxPackage "MicrosoftTeams*" -AllUsers | Remove-AppPackage -AllUsers -ErrorAction SilentlyContinue
Write-Verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to uninstall Personal Teams appx packages for all users"}
catch{write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to uninstall Personal Teams appx packages for all users"}
}
# Uninstall All Classic Teams appx packages
try{Get-AppxPackage "Teams*" -AllUsers | Remove-AppPackage -AllUsers -ErrorAction SilentlyContinue
Write-Verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to uninstall all Classic Teams appx packages"}
catch{write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to uninstall all Classic Teams appx packages"}
# Uninstall Classic Teams Machine-Wide Installer app with an uninstall regkey
Uninstall-ProgramFromRegKey -AppName "Teams Machine-Wide Installer"
# Uninstall All Classic Teams apps with installation folder in program files
Uninstall-ProgramFromFolder -Path (Join-Path ${env:ProgramFiles(x86)} "Teams Installer") -uninstaller "teams.exe" -UninstallArgs "--uninstall"
Uninstall-ProgramFromFolder -Path (Join-Path ${env:ProgramFiles} "Teams Installer") -uninstaller "teams.exe" -UninstallArgs "--uninstall"
Uninstall-ProgramFromFolder -Path (Join-Path ${env:ProgramFiles(x86)} "Microsoft\Teams\current") -uninstaller "update.exe" -UninstallArgs "--uninstall -s"
Uninstall-ProgramFromFolder -Path (Join-Path ${env:ProgramFiles} "Microsoft\Teams\current") -uninstaller "update.exe" -UninstallArgs "--uninstall -s"
# Uninstall All Classic Teams apps with installation folder in personal profile
$userProfile = $env:USERPROFILE
$usersDirectory = Split-Path $userProfile
$userDirectories = Get-ChildItem -Path $usersDirectory -Directory
# Loop through each userprofile directory and uninstall
foreach ($userdirectory in $userDirectories) {
$username = Split-Path $userdirectory -Leaf
Uninstall-ProgramFromFolder -Path (Join-Path $userdirectory "appdata\local\Microsoft\Teams") -uninstaller "update.exe" -UninstallArgs "--uninstall -s"
Uninstall-ProgramFromFolder -Path (Join-Path $($env:ProgramData) "$($username)\Microsoft\Teams") -uninstaller "update.exe" -UninstallArgs "--uninstall -s"
}
#cleanup Classic Teams shortcuts from device
Remove-DesktopShortcuts -ShortcutName "*Teams*"
Remove-StartMenuShortcuts -ShortcutName "*Teams*"
# Remove Classic Teams from startup registry key
Remove-RegistryKeys -registryPath 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -keyNames 'Teams', 'TeamsMachineUninstallerLocalAppData', 'TeamsMachineUninstallerProgramData', 'com.squirrel.Teams.Teams', 'TeamsMachineInstaller'
Remove-RegistryKeys -registryPath 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run' -keyNames 'Teams', 'TeamsMachineUninstallerLocalAppData', 'TeamsMachineUninstallerProgramData', 'com.squirrel.Teams.Teams', 'TeamsMachineInstaller'
Remove-RegistryKeys -registryPath 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -keyNames 'Teams', 'TeamsMachineUninstallerLocalAppData', 'TeamsMachineUninstallerProgramData', 'com.squirrel.Teams.Teams', 'TeamsMachineInstaller'
Remove-RegistryKeys -registryPath 'HKCU:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run' -keyNames 'Teams', 'TeamsMachineUninstallerLocalAppData', 'TeamsMachineUninstallerProgramData', 'com.squirrel.Teams.Teams', 'TeamsMachineInstaller'
# Remove Classic Teams folders and icons
$ClassicTeamsShared = "$($env:ProgramData)\*\Microsoft\Teams"
try{Get-Item $ClassicTeamsShared | Remove-Item -Force -Recurse
Write-Verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to remove $($ClassicTeamsShared)"}
catch{write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to remove $($ClassicTeamsShared)"}
$ClassicTeamsPersonallocal = "$($usersDirectory)\*\AppData\Local\Microsoft\Teams"
try{Get-Item $ClassicTeamsPersonallocal | Remove-Item -Force -Recurse
Write-Verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to remove $($ClassicTeamsPersonallocal)"}
catch{write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to remove $($ClassicTeamsPersonallocal)"}
$ClassicTeamsPersonalroaming = "$($usersDirectory)\*\AppData\Roaming\Microsoft\Teams"
try{Get-Item $ClassicTeamsPersonalroaming | Remove-Item -Force -Recurse
Write-Verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to remove $($ClassicTeamsPersonalroaming)"}
catch{write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),error,Failed to remove $($ClassicTeamsPersonalroaming)"}
# Install New Teams
$process = Start-Process -FilePath "$($NewTeamsInstaller)" -ArgumentList "$($NewTeamsInstallerArgs)" -PassThru -Wait -ErrorAction STOP
if ($process.ExitCode -ne 0) {
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to Installation failed with $($NewTeamsInstaller) and exit code $($process.ExitCode)." -type Error
$Global:EventId=11
$Global:EventType="Error"
}
else{
write-verbose -verbose "$(Get-Date -Format 'yyyy-MM-dd'),$(Get-Date -format 'HH:mm:ss'),info,Success to Installation of $($NewTeamsInstaller) with exit code $($process.ExitCode)."
$Global:EventId=10
$Global:EventType="Information"
}
#stop transcript and write to eventlog
Stop-Transcript |out-null
$Transcript = ((Get-Content $LogFile -Raw) -split ([regex]::Escape("**********************")))[-3]
$EventText = "New Teams installer result: `n$($Transcript)"
Write-ToEventlog $EventText $TaskName $Global:EventId $Global:EventType
#endregion