Nearly a year has passed since the preview launch of the New Microsoft Teams and, although not exactly all the features seem to be perfectly stable, Microsoft decided long ago to automatically deploy this new version in place of the dear, old, established Teams Classic.
Now, in principle, I see this automatic update as absolutely welcome, according to Microsoft’s own words:

Users who have installed a different version of Teams will have their version replaced with the version being provisioned.

Happy with the automatic update then?
The upgrade to the New Teams, hear this, does not totally wipe out old installations! (to date at least)

As we know, during its lifecycle, Teams Classic installation has had various and imaginative incarnations: we range in fact from user-based installation in AppData or even ProgramData to the machine-wide installer, and we certainly want to get rid of any remnants of these old installations!
To this we add that if you are used to working with GPO and MSI distributions you will have to do without this time: New Teams is not provided as an MSI package.

All that being said, I thought it would therefore be useful to write myself a PowerShell script to have more control and automation over both the New Microsoft Teams deployment process and the removal of Teams Classic in all its variants.

Uninstall Teams Classic via PowerShell

We know that Teams Classic may have been installed either at the user profile level or machine-wide, so we must take this into account for complete removal.

User level removal

We check the user registry and run the QuietUninstallString, this allows us to go right in removing Teams Classic without the use of default paths.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Specify the registry path
$reg = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Teams"
# Get Teams uninstall information for each user
$userSids = Get-WmiObject Win32_UserProfile | Where-Object { $_.Special -eq $false } | Select-Object -ExpandProperty SID
foreach ($userSid in $userSids) {
    $userReg= "Registry::HKEY_USERS\$userSid\$reg"
    $teamsUninstallInfo = Get-ItemProperty -LiteralPath $userReg -ErrorAction SilentlyContinue
    # Display the Teams uninstall information for each user
    if ($teamsUninstallInfo) {
        $sid = New-Object System.Security.Principal.SecurityIdentifier($userSid)
        # Use Translate to find user from sid
        $objUser = $sid.Translate([System.Security.Principal.NTAccount])
        if ($teamsUninstallInfo.QuietUninstallString) {
            Start-Process -FilePath "cmd" -ArgumentList "/c", $teamsUninstallInfo.QuietUninstallString -Wait
        }
        # Cleanup registry
        if (Test-Path -path $userReg) {
            Remove-Item $userReg -Recurse -Force
        }
    }
}

Machine-wide removal

We use the known guid from Teams and also here we go to take advantage of the registry to uninstall via msiexec.
Compared to many other sources found on the web this method is much faster because it avoids using Get-WmiObject -Class Win32_Product to get the list of installed programs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Known Guid
$msiPkg32Guid = "{39AF0813-FA7B-4860-ADBE-93B9B214B914}"
$msiPkg64Guid = "{731F6BAA-A986-45A4-8936-7C3AAAAA760B}"
$uninstallReg64 = Get-Item -Path HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { $_.DisplayName -match 'Teams Machine-Wide Installer' }
$uninstallReg32 = Get-Item -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\* -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { $_.DisplayName -match 'Teams Machine-Wide Installer' }
if ($uninstallReg64) {
    $msiExecUninstallArgs = "/X $msiPkg64Guid /quiet"
} elseif ($uninstallReg32) {
    $msiExecUninstallArgs = "/X $msiPkg32Guid /quiet"
} else {
    return
}
$p = Start-Process "msiexec.exe" -ArgumentList $msiExecUninstallArgs -Wait -PassThru -WindowStyle Hidden

Install New Teams via PowerShell

Let’s start by saying that the officially supported methods for installation can be found here https://learn.microsoft.com/en-us/microsoftteams/new-teams-bulk-install-client and discover that we will necessarily have to use teamsbootstrapper.exe for our deployment, so first let’s download it via a helper function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function DownloadFile {
    param(
        [Parameter(Mandatory=$true)]
        [string]$url,
        [Parameter(Mandatory=$true)]
        [string]$fileName,
        [string]$path = [System.Environment]::GetEnvironmentVariable('TEMP','Machine')
    )
    # Construct WebClient object
    $webClient = New-Object -TypeName System.Net.WebClient
    $file = $null
    # Create path if it doesn't exist
    if (-not(Test-Path -Path $path)) {
        New-Item -Path $path -ItemType Directory -Force | Out-Null
    }
    # Download
    try {
        $outputPath = Join-Path -Path $path -ChildPath $fileName
        $webClient.DownloadFile($url, $outputPath)
        $file = $outputPath
    }
    catch {}
    # Dispose of the WebClient object
    $webClient.Dispose()
    return $file
}
$BootstrapperPath = DownloadFile "https://go.microsoft.com/fwlink/?linkid=2243204&clcid=0x409" "bootstrapper.exe"

Then we go through the installation via bootstrapper.exe, we can let it download Teams directly from the Internet or provide a local or UNC path if we prefer to save time and bandwidth.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function InstallTeams {
    param(
        [Parameter(Mandatory=$true)]
        [string]$bootstrapperPath,
        [string]$teamsPackagePath = ''
    )
    try {
        # Using the teamsbootstrapper.exe -p command always guarantees the latest Teams client is installed.
        # Use -o with path to Teams's MSIX package minimizing the amount of bandwidth used for the initial installation.
        # The MSIX can exist in a local path or UNC.
        if ($teamsPackagePath -ne '') {
            $arg = "-o $teamsPackagePath"
        }
        $r = & $bootstrapperPath -p $arg
        $resultObj = try { $r | ConvertFrom-Json } catch { $null }
        if ($resultObj -eq $null -or $resultObj.success -eq $false) {
            throw ''
        }
        return $true
    }
    catch {
        return $false
    }
}
InstallTeams -BootstrapperPath $BootstrapperPath

Teams-Posh, the ultimate script for removing and installing Teams.

All that remains is to put together all the pieces seen so far and throw down the complete script with the proper logging.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
<#
.SYNOPSIS
Teams-Posh installs or uninstalls Microsoft Teams.

.DESCRIPTION
This script allows for the installation or uninstallation of Microsoft Teams. 
When installing, it downloads the bootstrapper and Teams package if not provided, 
and then installs Microsoft Teams.
When uninstalling, it removes the installed Microsoft Teams application, 
this includes Teams Classic uninstallation querying related registry keys thus 
avoiding use of very slow call to "Get-WmiObject -Class Win32_Product".

.PARAMETER Action
Specifies the action to perform. Valid values are 'Install' or 'Uninstall'.

.PARAMETER BootstrapperPath
Specifies the path to the bootstrapper executable. 
If not provided, it will be downloaded by Microsoft website.

.PARAMETER TeamsPackagePath
Specifies the path to the Microsoft Teams package. 
If not provided (required for installation), it will be downloaded.

.EXAMPLE
.\Teams-Posh.ps1 -Action Install
Installs Microsoft Teams.

.EXAMPLE
.\Teams-Posh.ps1 -Action Uninstall
Uninstalls Microsoft Teams.

.NOTES
Author:[lestoilfante](https://github.com/lestoilfante)
#>


param (
    [Parameter(Mandatory=$true)]
    [ValidateSet('Install', 'Uninstall')]
    [string]$Action,
    [string]$BootstrapperPath = '',
    [string]$TeamsPackagePath = ''
)

function Teams-Posh {
    # Check running with elevated privileges
    if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
        Log "This script requires elevation. Please run as administrator"
        exit 1
    }

    if ($BootstrapperPath -eq '') {
        $BootstrapperPath = DownloadFile "https://go.microsoft.com/fwlink/?linkid=2243204&clcid=0x409" "bootstrapper.exe"
        if ($BootstrapperPath -eq $null) { exit 1 }
    }

    if ($Action -eq 'Install') {
        $install = InstallTeams -BootstrapperPath $BootstrapperPath -TeamsPackagePath $TeamsPackagePath
        if ($install -eq $true) { exit 0 }
        exit 1
    }

    if ($Action -eq 'Uninstall') {
        RemoveTeamsClassicWide
        RemoveTeamsClassic
        RemoveTeams $BootstrapperPath
        exit 0
    }
}


function InstallTeams {
    param(
        [Parameter(Mandatory=$true)]
        [string]$bootstrapperPath,
        [string]$teamsPackagePath = ''
    )
    try {
        # Using the teamsbootstrapper.exe -p command always guarantees the latest Teams client is installed.
        # Use -o with path to Teams's MSIX package minimizing the amount of bandwidth used for the initial installation.
        # The MSIX can exist in a local path or UNC.
        if ($teamsPackagePath -ne '') {
            $arg = "-o $teamsPackagePath"
        } else { Log 'Downloading Teams' }
        $r = & $bootstrapperPath -p $arg
        $resultObj = try { $r | ConvertFrom-Json } catch { $null }
        if ($resultObj -eq $null -or $resultObj.success -eq $false) {
            throw ''
        }
        Log 'Teams installation done'
        return $true
    }
    catch {
        Log 'ERROR: Teams installation failed'
        return $false
    }
}

function RemoveTeamsClassicWide {
    # Known Guid
    $msiPkg32Guid = "{39AF0813-FA7B-4860-ADBE-93B9B214B914}"
    $msiPkg64Guid = "{731F6BAA-A986-45A4-8936-7C3AAAAA760B}"
    $uninstallReg64 = Get-Item -Path HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { $_.DisplayName -match 'Teams Machine-Wide Installer' }
    $uninstallReg32 = Get-Item -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\* -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { $_.DisplayName -match 'Teams Machine-Wide Installer' }
    if ($uninstallReg64) {
        $msiExecUninstallArgs = "/X $msiPkg64Guid /quiet"
        Log "Teams Classic Machine-Wide Installer x64 found."
    } elseif ($uninstallReg32) {
        $msiExecUninstallArgs = "/X $msiPkg32Guid /quiet"
        Log "Teams Machine-Wide Installer x86 found."
    } else {
        return
    }
    $p = Start-Process "msiexec.exe" -ArgumentList $msiExecUninstallArgs -Wait -PassThru -WindowStyle Hidden
    if ($p.ExitCode -eq 0) {
        Log "Teams Classic Machine-Wide uninstalled."
    } else {
        Log "ERROR: Teams Classic Machine-Wide uninstall failed with exit code $($p.ExitCode)"
    }
}

function RemoveTeamsClassic {
    # Specify the registry path
    $reg = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Teams"
    # Get Teams uninstall information for each user
    $userSids = Get-WmiObject Win32_UserProfile | Where-Object { $_.Special -eq $false } | Select-Object -ExpandProperty SID
    foreach ($userSid in $userSids) {
        $userReg= "Registry::HKEY_USERS\$userSid\$reg"
        $teamsUninstallInfo = Get-ItemProperty -LiteralPath $userReg -ErrorAction SilentlyContinue
        # Display the Teams uninstall information for each user
        if ($teamsUninstallInfo) {
            $sid = New-Object System.Security.Principal.SecurityIdentifier($userSid)
            # Use Translate to find user from sid
            $objUser = $sid.Translate([System.Security.Principal.NTAccount])
            if ($teamsUninstallInfo.QuietUninstallString) {
                Start-Process -FilePath "cmd" -ArgumentList "/c", $teamsUninstallInfo.QuietUninstallString -Wait
                Log "Teams Classic Removed for user $($objUser.Value)"
            }
            # Cleanup registry
            if (Test-Path -path $userReg) {
                Remove-Item $userReg -Recurse -Force
            }
        }
    }
}

function RemoveTeams {
    param(
        [string]$bootstrapper = ''
    )
    try{
        $appx = Get-AppxPackage -AllUsers | Where-Object { $PSItem.Name -eq "MSTeams" }
        if ($appx) {
            Log "Teams $($appx.Version) package found"
            $appx | Remove-AppxPackage -AllUsers
        } else { Log "No Teams package found" }
        if($bootstrapper -ne '') {
            Log "Deprovisioning Teams using $bootstrapper"
            $r = & $bootstrapper -x
            $resultObj = try { $r | ConvertFrom-Json } catch { $null }
            if ($resultObj -eq $null) {
                throw ''
            }
            Log "Deprovisioning Teams using $bootstrapper done"
        }
    }
    catch {
        Log "ERROR: Teams package remove error"
    }
}

function DownloadFile {
    param(
        [Parameter(Mandatory=$true)]
        [string]$url,
        [Parameter(Mandatory=$true)]
        [string]$fileName,
        [string]$path = [System.Environment]::GetEnvironmentVariable('TEMP','Machine')
    )
    # Construct WebClient object
    $webClient = New-Object -TypeName System.Net.WebClient
    $file = $null
    # Create path if it doesn't exist
    if (-not(Test-Path -Path $path)) {
        New-Item -Path $path -ItemType Directory -Force | Out-Null
    }
    # Download
    try {
        Log "Download of $fileName start"
        $outputPath = Join-Path -Path $path -ChildPath $fileName
        $webClient.DownloadFile($url, $outputPath)
        Log "Download of $fileName done"
        $file = $outputPath
    }
    catch {
        Log "ERROR: Download of $fileName failed"
    }
    # Dispose of the WebClient object
    $webClient.Dispose()
    return $file
}

function Log {
	param (
		[string]$Text
	)
	$timestamp = "{0:yyyy-MM-dd HH:mm:ss}" -f [DateTime]::Now
	Write-Information -MessageData "$timestamp `- $($Text)" -InformationAction Continue
}

Teams-Posh