Public/Functions/Support/VoiceConfig/Set-TeamsPhoneNumber.ps1
# Module: TeamsFunctions # Function: Teams User Voice Configuration # Author: David Eberhardt # Updated: 13-FEB-2022 # Status: Live #TODO Monitor Set-CsPhoneNumberAssignment, remove deprecated use once testing has completed (with v22.3) #TODO For Later: When adding -Location, catch "Contact your operator to change the emergency address for this number." - OperatorConnect numbers cannot be assigned a location - display this error (warning?) instead? function Set-TeamsPhoneNumber { <# .SYNOPSIS Applies a Phone Number to a User Object or Resource Account .DESCRIPTION Applies a Microsoft Calling Plans Number OR a Direct Routing Number to a User or Resource Account .PARAMETER UserPrincipalName Required for Parameterset UserPrincipalName. UserPrincipalName of the Object to be assigned the PhoneNumber. This can be a UPN of a User Account (CsOnlineUser Object) or a Resource Account (CsOnlineApplicationInstance Object) .PARAMETER Object Required for Parameterset Object. CsOnlineUser Object passed to the function to reduce query time. This can be a UPN of a User Account (CsOnlineUser Object) or a Resource Account (CsOnlineApplicationInstance Object) .PARAMETER PhoneNumber A Microsoft Calling Plans Number or a Direct Routing Number Requires the Account to be licensed. Able to enable PhoneSystem and the Account for Enterprise Voice Required format is E.164 or LineUri, starting with a '+' and 10-15 digits long. .PARAMETER Force Suppresses confirmation prompt unless -Confirm is used explicitly Scavenges Phone Number from all accounts the PhoneNumber is currently assigned to including the current User .EXAMPLE Set-TeamsPhoneNumber -UserPrincipalName John@domain.com -PhoneNumber +15551234567 Applies the Phone Number +1 (555) 1234-567 to the Account John@domain.com .INPUTS System.String .OUTPUTS System.Void - If called directly Boolean - If called by another CmdLet .NOTES Simple helper function to assign a Phone Number to any User or Resource Account Returns boolean result and less communication if called by another function Can be used providing either the UserPrincipalName or the already queried CsOnlineUser Object .COMPONENT VoiceConfiguration .FUNCTIONALITY Enables a User for Enterprise Voice in order to apply a valid Voice Configuration .LINK https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/Set-TeamsPhoneNumber.md .LINK https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/about_VoiceConfiguration.md .LINK https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/about_UserManagement.md .LINK https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/about_Supporting_Functions.md .LINK https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/ #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'UserPrincipalName')] [OutputType([Boolean])] param( [Parameter(Mandatory, Position = 0, ParameterSetName = 'Object', ValueFromPipeline)] [Object[]]$Object, [Parameter(Mandatory, Position = 0, ParameterSetName = 'UserPrincipalName', ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('ObjectId', 'Identity')] [string[]]$UserPrincipalName, [Parameter(Mandatory, Position = 1, HelpMessage = 'Telephone Number to assign')] [AllowNull()] [AllowEmptyString()] [Alias('Tel', 'Number', 'TelephoneNumber')] [string]$PhoneNumber, [Parameter(HelpMessage = 'Suppresses confirmation prompt unless -Confirm is used explicitly')] [switch]$Force ) #param begin { Show-FunctionStatus -Level Live Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand)" # Asserting MicrosoftTeams Connection if ( -not (Assert-MicrosoftTeamsConnection) ) { break } # Setting Preference Variables according to Upstream settings if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') } if (-not $PSBoundParameters.ContainsKey('Confirm')) { $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference') } if (-not $PSBoundParameters.ContainsKey('WhatIf')) { $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference') } if (-not $PSBoundParameters.ContainsKey('Debug')) { $DebugPreference = $PSCmdlet.SessionState.PSVariable.GetValue('DebugPreference') } else { $DebugPreference = 'Continue' } if ( $PSBoundParameters.ContainsKey('InformationAction')) { $InformationPreference = $PSCmdlet.SessionState.PSVariable.GetValue('InformationAction') } else { $InformationPreference = 'Continue' } if ( $PSBoundParameters.ContainsKey('ErrorAction')) { $InformationPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ErrorAction') } else { $ErrorActionPreference = 'Stop' } $Stack = Get-PSCallStack $Called = ($stack.length -ge 3) if ( [String]::IsNullOrEmpty($PhoneNumber) ) { $PhoneNumber = $null } else { If ($PhoneNumber -notmatch '^(tel:\+|\+)?([0-9]?[-\s]?(\(?[0-9]{3}\)?)[-\s]?([0-9]{3}[-\s]?[0-9]{4})|[0-9]{8,15})((;ext=)([0-9]{3,8}))?$') { throw [System.Management.Automation.ValidationMetadataException] 'Not a valid phone number. Must be 8 to 15 digits long' } } # Preparing Splatting Object $parameters = $null $Parameters = @{ 'PhoneNumber' = $PhoneNumber 'Called' = $Called 'Force' = $Force 'ErrorAction' = $ErrorActionPreference } #region Worker Functions function SetNumber { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory)] [string]$UserPrincipalName, [Parameter(Mandatory)] [AllowNull()] [AllowEmptyString()] [string]$PhoneNumber, [Parameter(Mandatory)] [boolean]$PhoneNumberIsMSNumber, [Parameter(Mandatory)] [ValidateSet('User', 'ApplicationEndpoint')] [string]$UserType ) #param if ( $null -eq $PhoneNumber -or $PhoneNumber -eq '' ) { $E164Number = $LineUri = $null } else { $E164Number = Format-StringForUse $PhoneNumber -As E164 $LineUri = Format-StringForUse $PhoneNumber -As LineUri } try { #Using Set-CsPhoneNumberAssignment, but if failed, fall back to old functionality #TEST Number as an OperatorConnect Number <# Replaced with below Get-TeamsPhoneNumber $CsPhoneNumberAssignmentParams = @{ 'Identity' = $UserPrincipalName 'PhoneNumber' = $E164Number 'PhoneNumberType' = if ($PhoneNumberIsMSNumber) { 'CallingPlan' } else { 'DirectRouting' } } #> $PhoneNumberDetails = Get-TeamsPhoneNumber -PhoneNumber $PhoneNumber $CsPhoneNumberAssignmentParams = @{ 'Identity' = $UserPrincipalName 'PhoneNumber' = $E164Number 'PhoneNumberType' = $PhoneNumberDetails.PhoneNumberType } if ($PSBoundParameters.ContainsKey('Debug')) { " Function: $($MyInvocation.MyCommand.Name) - CsPhoneNumberAssignmentParams:", ($CsPhoneNumberAssignmentParams | Format-Table -AutoSize | Out-String).Trim() | Write-Debug } if ( [String]::IsNullOrEmpty($PhoneNumber) ) { # If phonenumber is empty, this needs to call Remove-CsPhoneNumberAssignment # Removal of all Numbers required if ($PSCmdlet.ShouldProcess("$UserPrincipalName", 'Remove-CsPhoneNumberAssignment -RemoveAll')) { Remove-CsPhoneNumberAssignment -Identity $UserPrincipalName -RemoveAll -ErrorAction STOP } } else { # Direct Routing Number if ($PSCmdlet.ShouldProcess("$UserPrincipalName", "Set-CsPhoneNumberAssignment -PhoneNumber $E164Number")) { Set-CsPhoneNumberAssignment @CsPhoneNumberAssignmentParams -ErrorAction STOP } } } catch { #Writing error of Set-CsPhoneNumberAssignment to debug stream Write-Warning -Message "Assignment with 'Set-CsPhoneNumberAssignment' did not work, trying legacy method!" " Function: $($MyInvocation.MyCommand.Name) - Set-CsPhoneNumberAssignment threw exception:", ($($_.Exception.Message) | Out-String).Trim() | Write-Debug #Using Set-CsUser and Set-CsOnline(Voice)ApplicationInstance to set the object switch ( $UserType ) { 'User' { if ($PhoneNumberIsMSNumber) { # Calling Plan Number if ($PSCmdlet.ShouldProcess("$UserPrincipalName", "Set-CsUser -Telephonenumber $E164Number")) { Set-CsUser -Identity "$UserPrincipalName" -TelephoneNumber $E164Number -ErrorAction STOP } } else { # Direct Routing Number try { if ($PSCmdlet.ShouldProcess("$UserPrincipalName", "Set-CsUser -OnPremLineURI $LineUri")) { Set-CsUser -Identity "$UserPrincipalName" -OnPremLineURI $LineUri -ErrorAction STOP } } catch { Write-Verbose -Message "Enablement with 'Set-CsUser -OnPremLineUri' did not work, trying 'Set-CsUser -LineUri'!" -Verbose if ($PSCmdlet.ShouldProcess("$UserPrincipalName", "Set-CsUser -LineURI $LineUri")) { Set-CsUser -Identity "$UserPrincipalName" -LineURI $LineUri -ErrorAction STOP } } } } 'ApplicationEndpoint' { if ($PhoneNumberIsMSNumber) { # Calling Plan Number (VoiceApplicationInstance) if ($PSCmdlet.ShouldProcess("$UserPrincipalName", "Set-CsOnlineVoiceApplicationInstance -Telephonenumber $E164Number")) { $null = (Set-CsOnlineVoiceApplicationInstance -Identity "$UserPrincipalName" -TelephoneNumber $E164Number -ErrorAction STOP) } } else { # Direct Routing Number (ApplicationInstance) if ($PSCmdlet.ShouldProcess("$UserPrincipalName", "Set-CsOnlineApplicationInstance -OnPremPhoneNumber $E164Number")) { $null = (Set-CsOnlineApplicationInstance -Identity "$UserPrincipalName" -OnpremPhoneNumber $E164Number -Force -ErrorAction STOP) } } } } } } function SetPhoneNumber ($UserObject, $UserLicense, $PhoneNumber, $Called, $Force) { Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)" $Id = $($UserObject.UserPrincipalName) #region Validating Object # Object Location (OnPrem VS Online) if ( $UserObject.InterpretedUserType -match 'OnPrem' ) { $Message = "'$Id' is not hosted in Teams!" if ($Called) { Write-Warning -Message $Message #return $false } else { Write-Warning -Message $Message #Deactivated as Object is able to be used/enabled even if in Islands mode and Object in Skype! #throw [System.InvalidOperationException]::New("$Message") } } #Determining Object Type $UserType = switch -regex ( $UserObject.InterpretedUserType ) { 'User' { 'User' } 'ApplicationInstance' { 'ApplicationEndpoint' } Default { $false } } if ( -not $UserType ) { $Message = "Object '$Id' is not a User or an ApplicationEndpoint!" if ($Called) { Write-Warning -Message $Message return $false } else { throw [System.InvalidOperationException]::New("$Message") } } #endregion #region Validating License if ( -not $UserLicense.PhoneSystem -and -not $UserLicense.PhoneSystemVirtualUser ) { $Message = "'$Id' Enterprise Voice Status: User is not licensed correctly (PhoneSystem required)!" if ($Called) { Write-Warning -Message $Message return $false } else { throw [System.InvalidOperationException]::New("$Message") } return $(if ($Called) { $false }) } if ( -not [string]$UserLicense.PhoneSystemStatus.contains('Success') ) { Write-Information "TRYING: '$Id' - Phone System: Not enabled, trying to enable" Set-AzureAdUserLicenseServicePlan -UserPrincipalName $UserObject.UserPrincipalName -Enable MCOEV $i = 0 $iMax = 60 Write-Information "INFO: User '$Id' - Phone System: Enabled; Waiting for AzureAd to write object ($iMax s)" $StatusID1 = 'Azure Active Directory is propagating Object. Please wait' $CurrentOperationID1 = 'Waiting for Get-AzureAdUserLicense to return a Result' Write-Verbose -Message "$StatusID1 - $CurrentOperationID1" do { if ($i -gt $iMax) { Write-Error -Message "Could not find Object in AzureAD in the last $iMax Seconds" -Category ObjectNotFound -RecommendedAction 'Please verify Object has been created (UserPrincipalName); Continue with Set-TeamsResourceAccount' return } Write-Progress -Id 1 -ParentId 0 -Activity $ActivityID1 -Status $StatusID1 -CurrentOperation $CurrentOperationID1 -SecondsRemaining $($iMax - $i) -PercentComplete (($i * 100) / $iMax) Start-Sleep -Milliseconds 1000 $i++ $UserLicense = Get-AzureAdUserLicense "$Id" } while ( -not [string]$UserLicense.PhoneSystemStatus.contains('Success') ) } #endregion #region Enterprise Voice if ( $UserObject.EnterpriseVoiceEnabled ) { $EVenabled = $true } else { Write-Information "TRYING: '$Id' - Enterprise Voice: Not enabled, trying to enable" $EVenabled = Enable-TeamsUserForEnterpriseVoice -UserPrincipalName $UserObject.UserPrincipalName } if ( -not $EVenabled ) { $Message = "'$Id' Enterprise Voice: User could not be enabled for Enterprise Voice!" if ($Called) { Write-Warning -Message $Message return $false } else { throw [System.InvalidOperationException]::New("$Message") } } #endregion #region Validating Phone Number # Querying CurrentPhoneNumber try { $CurrentPhoneNumber = $CsUser.LineUri Write-Verbose -Message "Object '$Id' - Phone Number assigned currently: $CurrentPhoneNumber" } catch { $CurrentPhoneNumber = $null Write-Verbose -Message "Object '$Id' - Phone Number assigned currently: NONE" } if ( [String]::IsNullOrEmpty($PhoneNumber) ) { if ($CurrentPhoneNumber) { Write-Warning -Message "Object '$Id' - PhoneNumber is NULL or Empty. The Existing Number '$CurrentPhoneNumber' will be removed" } else { Write-Verbose -Message "Object '$Id' - PhoneNumber is NULL or Empty, but no Number is currently assigned. No Action taken" } $PhoneNumber = $null $PhoneNumberIsMSNumber = $false } else { #Number Type Write-Verbose -Message "Object '$Id' - Parsing Online Telephone Numbers (validating Number against Microsoft Calling Plan Numbers)" $MSNumber = ((Format-StringForUse -InputString "$PhoneNumber" -SpecialChars 'tel:+') -split ';')[0] $CsOnlineTelephoneNumber = Get-CsOnlineTelephoneNumber -TelephoneNumber $MSNumber -WarningAction SilentlyContinue $PhoneNumberIsMSNumber = if ( $null -ne $CsOnlineTelephoneNumber ) { $true } else { $false } Write-Verbose -Message "Provisioning for $(if ( $PhoneNumberIsMSNumber ) { 'Calling Plans' } else { 'Direct Routing'})" # Previous assignments if ( $PhoneNumber ) { $UserWithThisNumber = Find-TeamsUserVoiceConfig -PhoneNumber $PhoneNumber -WarningAction SilentlyContinue $UserWithThisNumberExceptSelf = $UserWithThisNumber | Where-Object UserPrincipalName -NE $UserObject.UserPrincipalName } else { $UserWithThisNumber = $UserWithThisNumberExceptSelf = $null } if ( $UserWithThisNumberExceptSelf ) { if ($Force) { Write-Warning -Message "Object '$Id' - Number '$PhoneNumber' is currently assigned to User '$($UserWithThisNumber.UserPrincipalName)'. This assignment will be removed!" } else { Write-Error -Message "Object '$Id' - Number '$PhoneNumber' is already assigned to another Object: '$($UserWithThisNumber.UserPrincipalName)'" -Category NotImplemented -RecommendedAction 'Please specify a different Number or use -Force to re-assign' -ErrorAction Stop } } } #endregion #region ACTION # Scavenging Phone Number if ( $Force ) { Write-Warning -Message 'Parameter Force - Scavenging Phone Number from all Objects where number is assigned. Validate carefully' foreach ($UserWTN in $UserWithThisNumberExceptSelf) { Write-Verbose -Message "Object '$($UserWTN.UserPrincipalName)' - Scavenging Phone Number" try { if ( $UserWtn.UserPrincipalName -and -not $UserWtn.InterpretedVoiceConfigType ) { $UserWTNObject = Get-TeamsUserVoiceConfig -UserPrincipalName "$($UserWtn.UserPrincipalName)" -WarningAction SilentlyContinue $UserWTNPhoneNumberIsMSNumber = $($UserWTNObject.InterpretedVoiceConfigType -eq 'CallingPlans') $UserWTNUserType = $UserWTNObject.ObjectType } else { $UserWTNPhoneNumberIsMSNumber = $($UserWtn.InterpretedVoiceConfigType -eq 'CallingPlans') $UserWTNUserType = $UserWTN.ObjectType } $SetNumberParams = @{ 'UserPrincipalName' = $($UserWTN.UserPrincipalName) 'PhoneNumber' = $null 'PhoneNumberIsMSNumber' = $UserWTNPhoneNumberIsMSNumber 'UserType' = $UserWTNUserType 'ErrorAction' = $ErrorActionPreference } SetNumber @SetNumberParams Write-Information "INFO: '$($UserWTN.UserPrincipalName)' - Phone Number '$PhoneNumber' removed" -InformationAction Continue if ($Called) { return $true } } catch { $Message = "'$Id' - Error scavenging Phone Number: $($_.Exception.Message)" if ($Called) { Write-Warning -Message $Message return $false } else { throw $_ } } } } #Removing Phone Number if ( $Force -or ([String]::IsNullOrEmpty($PhoneNumber)) ) { Write-Verbose -Message "Object '$Id' - Removing Phone Number" try { $SetNumberParams = @{ 'UserPrincipalName' = $Id 'PhoneNumber' = $null 'PhoneNumberIsMSNumber' = $PhoneNumberIsMSNumber 'UserType' = $UserType 'ErrorAction' = $ErrorActionPreference } SetNumber @SetNumberParams if ($Called) { return $true } } catch { $Message = "'$Id' - Error removing Phone Number: $($_.Exception.Message)" if ($Called) { Write-Warning -Message $Message return $false } else { throw $_ } } } #Setting Phone Number if ( -not ([String]::IsNullOrEmpty($PhoneNumber)) ) { Write-Verbose -Message "Object '$Id' - Applying Phone Number" try { $SetNumberParams = @{ 'UserPrincipalName' = $Id 'PhoneNumber' = $PhoneNumber 'PhoneNumberIsMSNumber' = $PhoneNumberIsMSNumber 'UserType' = $UserType 'ErrorAction' = $ErrorActionPreference } SetNumber @SetNumberParams if ($Called) { return $true } } catch { $Message = "'$Id' - Error applying Phone Number: $($_.Exception.Message)" if ($Called) { Write-Warning -Message $Message return $false } else { throw $_ } } } #endregion # Output if ($Called) { Write-Output $true } } #endregion } #begin process { Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)" switch ($PSCmdlet.ParameterSetName) { 'UserprincipalName' { foreach ($User in $UserPrincipalName) { Write-Verbose -Message "[PROCESS] Processing '$User'" try { #NOTE Call placed without the Identity Switch to make remoting call and receive object in tested format (v2.5.0 and higher) #$CsUser = Get-CsOnlineUser -Identity "$User" -WarningAction SilentlyContinue -ErrorAction Stop $CsUser = Get-CsOnlineUser "$User" -WarningAction SilentlyContinue -ErrorAction Stop $UserLicense = Get-AzureAdUserLicense "$User" } catch { Write-Error "'$User' not found" -Category ObjectNotFound continue } SetPhoneNumber -UserObject $CsUser -UserLicense $UserLicense @Parameters } } 'Object' { foreach ($O in $Object) { Write-Verbose -Message "[PROCESS] Processing provided CsOnlineUser Object for '$($O.UserPrincipalName)'" $UserLicense = Get-AzureAdUserLicense "$($O.UserPrincipalName)" SetPhoneNumber -UserObject $O -UserLicense $UserLicense @Parameters } } } } #process end { Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)" } #end } #Set-TeamsPhoneNumber |