Private/ConvertTo-TcsMailNickname.ps1

<#
.SYNOPSIS
    Builds a valid Microsoft Entra ID mail nickname.

.DESCRIPTION
    Private helper. Returns Prefix + Text + Suffix as a mail nickname that Microsoft Entra ID accepts:
      - accents are removed from letters (the text is decomposed, Unicode FormD, and the combining
        marks are dropped, so an A with an umlaut becomes a plain A);
      - characters Entra ID does not allow in a mail nickname are removed: anything outside printable
        ASCII, and @ ( ) \ [ ] " ; : < > ,
      - when nothing is left of the text, a stable 8-character hash of HashSource is used instead;
      - when the result is longer than 64 characters, the text is shortened and a hash of HashSource
        is added before the suffix, so different long names still get different nicknames.
    Prefix and Suffix must already be valid nickname characters.

.PARAMETER Text
    The variable part of the nickname, for example an application name.

.PARAMETER Prefix
    Fixed text before the variable part, for example 'Intune-AG-'.

.PARAMETER Suffix
    Fixed text after the variable part, for example '-Available'. It is always kept.

.PARAMETER HashSource
    The value the hash is calculated from, normally the full group display name.
#>

function ConvertTo-TcsMailNickname {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]
        $Text,

        [string]
        $Prefix = '',

        [string]
        $Suffix = '',

        [Parameter(Mandatory = $true)]
        [string]
        $HashSource
    )

    $maxLength = 64
    $decomposed = $Text.Normalize([System.Text.NormalizationForm]::FormD)
    $builder = New-Object -TypeName System.Text.StringBuilder
    foreach ($character in $decomposed.ToCharArray()) {
        if ([System.Globalization.CharUnicodeInfo]::GetUnicodeCategory($character) -ne [System.Globalization.UnicodeCategory]::NonSpacingMark) {
            $null = $builder.Append($character)
        }
    }
    $core = $builder.ToString() -replace '[^\x21-\x7E]|[@()\\\[\]";:<>,]', ''

    $sha = [System.Security.Cryptography.SHA256]::Create()
    try {
        $hashBytes = $sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($HashSource))
    }
    finally {
        $sha.Dispose()
    }
    $hash = -join ($hashBytes[0..3] | ForEach-Object { $_.ToString('x2') })

    if (-not $core) {
        $core = $hash
    }

    $nickname = $Prefix + $core + $Suffix
    if ($nickname.Length -gt $maxLength) {
        $room = $maxLength - $Prefix.Length - $Suffix.Length - $hash.Length - 1
        $nickname = $Prefix + $core.Substring(0, [Math]::Max($room, 0)).TrimEnd('-', '.') + '-' + $hash + $Suffix
    }
    $nickname
}