Graphimo.psm1

function Join-UriQuery { 
    <#
    .SYNOPSIS
    Provides ability to join two Url paths together including advanced querying
 
    .DESCRIPTION
    Provides ability to join two Url paths together including advanced querying which is useful for RestAPI/GraphApi calls
 
    .PARAMETER BaseUri
    Primary Url to merge
 
    .PARAMETER RelativeOrAbsoluteUri
    Additional path to merge with primary url (optional)
 
    .PARAMETER QueryParameter
    Parameters and their values in form of hashtable
 
    .PARAMETER EscapeUriString
    If set, will escape the url string
 
    .EXAMPLE
    Join-UriQuery -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' -QueryParameter @{
        page = 1
        per_page = 20
        search = 'SearchString'
    }
 
    .EXAMPLE
    Join-UriQuery -BaseUri 'https://evotec.xyz/wp-json/wp/v2/posts' -QueryParameter @{
        page = 1
        per_page = 20
        search = 'SearchString'
    }
 
    .EXAMPLE
    Join-UriQuery -BaseUri 'https://evotec.xyz' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts'
 
    .NOTES
    General notes
    #>

    [alias('Join-UrlQuery')]
    [CmdletBinding()]
    param (
        [parameter(Mandatory)][uri] $BaseUri,
        [parameter(Mandatory = $false)][uri] $RelativeOrAbsoluteUri,
        [Parameter()][System.Collections.IDictionary] $QueryParameter,
        [alias('EscapeUrlString')][switch] $EscapeUriString
    )
    begin {
        Add-Type -AssemblyName System.Web
    }
    process {

        if ($BaseUri -and $RelativeOrAbsoluteUri) {
            $Url = Join-Uri -BaseUri $BaseUri -RelativeOrAbsoluteUri $RelativeOrAbsoluteUri
        }
        else {
            $Url = $BaseUri
        }

        if ($QueryParameter) {
            $Collection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty)
            foreach ($key in $QueryParameter.Keys) {
                $Collection.Add($key, $QueryParameter.$key)
            }
        }

        $uriRequest = [System.UriBuilder] $Url
        if ($Collection) {
            $uriRequest.Query = $Collection.ToString()
        }
        if (-not $EscapeUriString) {
            $uriRequest.Uri.AbsoluteUri
        }
        else {
            [System.Uri]::EscapeUriString($uriRequest.Uri.AbsoluteUri)
        }
    }
}
function Remove-EmptyValue { 
    <#
    .SYNOPSIS
    Removes empty values from a hashtable recursively.
 
    .DESCRIPTION
    This function removes empty values from a given hashtable. It can be used to clean up a hashtable by removing keys with null, empty string, empty array, or empty dictionary values. The function supports recursive removal of empty values.
 
    .PARAMETER Hashtable
    The hashtable from which empty values will be removed.
 
    .PARAMETER ExcludeParameter
    An array of keys to exclude from the removal process.
 
    .PARAMETER Recursive
    Indicates whether to recursively remove empty values from nested hashtables.
 
    .PARAMETER Rerun
    Specifies the number of times to rerun the removal process recursively.
 
    .PARAMETER DoNotRemoveNull
    If specified, null values will not be removed.
 
    .PARAMETER DoNotRemoveEmpty
    If specified, empty string values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyArray
    If specified, empty array values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyDictionary
    If specified, empty dictionary values will not be removed.
 
    .EXAMPLE
    $hashtable = @{
        'Key1' = '';
        'Key2' = $null;
        'Key3' = @();
        'Key4' = @{}
    }
    Remove-EmptyValue -Hashtable $hashtable -Recursive
 
    Description
    -----------
    This example removes empty values from the $hashtable recursively.
 
    .EXAMPLE
    $SplatDictionary = [ordered] @{
        Test = $NotExistingParameter
        Test1 = 'Existing Entry'
        Test2 = $null
        Test3 = ''
        Test5 = 0
        Test6 = 6
        Test7 = @{}
    }
 
    Remove-EmptyValue -Splat $SplatDictionary -Recursive -ExcludeParameter 'Test7'
 
    $SplatDictionary
 
    #>

    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param(
        [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary
    )
    foreach ($Key in [string[]] $Hashtable.Keys) {
        if ($Key -notin $ExcludeParameter) {
            if ($Recursive) {
                if ($Hashtable[$Key] -is [System.Collections.IDictionary]) {
                    if ($Hashtable[$Key].Count -eq 0) {
                        if (-not $DoNotRemoveEmptyDictionary) {
                            $Hashtable.Remove($Key)
                        }
                    }
                    else {
                        Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive -ExcludeParameter $ExcludeParameter -DoNotRemoveNull:$DoNotRemoveNull -DoNotRemoveEmpty:$DoNotRemoveEmpty -DoNotRemoveEmptyArray:$DoNotRemoveEmptyArray -DoNotRemoveEmptyDictionary:$DoNotRemoveEmptyDictionary
                    }
                }
                else {
                    if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                        $Hashtable.Remove($Key)
                    }
                    elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                        $Hashtable.Remove($Key)
                    }
                    elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                        $Hashtable.Remove($Key)
                    }
                }
            }
            else {
                if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                    $Hashtable.Remove($Key)
                }
                elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                    $Hashtable.Remove($Key)
                }
                elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                    $Hashtable.Remove($Key)
                }
            }
        }
    }
    if ($Rerun) {
        for ($i = 0; $i -lt $Rerun; $i++) {
            Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive -ExcludeParameter $ExcludeParameter -DoNotRemoveNull:$DoNotRemoveNull -DoNotRemoveEmpty:$DoNotRemoveEmpty -DoNotRemoveEmptyArray:$DoNotRemoveEmptyArray -DoNotRemoveEmptyDictionary:$DoNotRemoveEmptyDictionary
        }
    }
}
function Join-Uri { 
    <#
    .SYNOPSIS
    Provides ability to join two Url paths together
 
    .DESCRIPTION
    Provides ability to join two Url paths together
 
    .PARAMETER BaseUri
    Primary Url to merge
 
    .PARAMETER RelativeOrAbsoluteUri
    Additional path to merge with primary url
 
    .EXAMPLE
    Join-Uri 'https://evotec.xyz/' '/wp-json/wp/v2/posts'
 
    .EXAMPLE
    Join-Uri 'https://evotec.xyz/' 'wp-json/wp/v2/posts'
 
    .EXAMPLE
    Join-Uri -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts'
 
    .EXAMPLE
    Join-Uri -BaseUri 'https://evotec.xyz/test/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts'
 
    .NOTES
    General notes
    #>

    [alias('Join-Url')]
    [cmdletBinding()]
    param(
        [parameter(Mandatory)][uri] $BaseUri,
        [parameter(Mandatory)][uri] $RelativeOrAbsoluteUri
    )

    return ($BaseUri.OriginalString.TrimEnd('/') + "/" + $RelativeOrAbsoluteUri.OriginalString.TrimStart('/'))
}
function Connect-MsalToken {
    [cmdletBinding()]
    param(
        [System.Collections.IDictionary] $MsalTokenSplat,
        [System.Collections.IDictionary] $Authorization,
        [int] $ExpiresTimeout = 30,
        [switch] $ForceRefesh
    )
    if (-not $Script:AuthorizationCache) {
        $Script:AuthorizationCache = [ordered] @{}
    }
    if ($Authorization.Splat) {
        $ApplicationID = $Authorization.Splat.ClientId
        if ($Script:AuthorizationCache[$ApplicationID] -and -not $ForceRefesh) {
            if ($Script:AuthorizationCache[$ApplicationID].MsalToken.ExpiresOn.UtcDateTime -gt ([datetime]::UtcNow).AddSeconds($ExpiresTimeout)) {
                Write-Verbose "Connect-MsalToken - Using cache for $ApplicationID"
                return $Script:AuthorizationCache[$ApplicationID]
            }
        }
        $Splat = $Authorization.Splat
        try {
            $MsalToken = Get-MsalToken @Splat -ErrorAction Stop
        }
        catch {
            Write-Warning -Message "Connect-MsalToken - Couldn't execute Get-MsalToken. Error: $($_.Exception.Message)"
            return
        }
        $Script:AuthorizationCache[$ApplicationID] = [ordered] @{
            'MsalToken' = $MsalToken
            'Splat'     = $Authorization.Splat
        }
        $Script:AuthorizationCache[$ApplicationID]
    }
    else {
        Write-Warning -Message "Connect-MsalToken - Using old authorization format without Splatting. No refresh of tokens will be available."
    }
}
function Convert-GraphInternalUser {
    <#
    .SYNOPSIS
    Converts user returned by graph with onPremisesExtensionAttributes to new simplified object
 
    .DESCRIPTION
    Converts user returned by graph with onPremisesExtensionAttributes to new simplified object
 
    .PARAMETER InputObject
    The object to convert
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [parameter(Mandatory, ValueFromPipeline)][PSCustomObject[]] $InputObject
    )
    process {
        $InputObject | ForEach-Object {
            $NewObject = [ordered] @{}
            $Object = $_
            foreach ($Property in $Object.PSObject.Properties.Name) {
                if ($Property -eq 'onPremisesExtensionAttributes') {
                    $OnPrem = $Object.onPremisesExtensionAttributes
                    if ($null -ne $OnPrem) {
                        if ($OnPrem -is [System.Collections.IDictionary]) {
                            foreach ($Key in $OnPrem.Keys) {
                                $NewObject[$Key] = $OnPrem[$Key]
                            }
                        }
                        else {
                            foreach ($ExtensionAttribute in $OnPrem.PSObject.Properties.Name) {
                                $NewObject[$ExtensionAttribute] = $OnPrem.$ExtensionAttribute
                            }
                        }
                    }

                    $NewObject['onPremisesExtensionAttributes'] = $OnPrem
                }
                else {
                    $NewObject[$Property] = $Object.$Property
                }
            }
            [PSCustomObject] $NewObject
        }
    }
}

function Invoke-InternalGraphimo {
    [CmdletBinding()]
    param(
        [Array] $OutputQuery,
        [int] $First,
        [int] $CurrentCount,
        [string] $CountVariable,
        [switch] $MgGraph
    )
    if ($OutputQuery.value) {
        $FoundUsers = $OutputQuery.value
    }
    if ($CountVariable) {
        Set-Variable -Name $CountVariable -Value $OutputQuery.'@odata.count' -Scope Global
    }
    if ($First) {
        if ($CurrentCount) {
            $First = $First - $CurrentCount
        }
        if ($FoundUsers.Count -eq $First) {
        }
        elseif ($FoundUsers.Count -gt $First) {
            $FoundUsers = $FoundUsers | Select-Object -First $First
        }
        else {
            $First = $First - $FoundUsers.Count
        }
    }
    if ($MgGraph -or $Script:MgGraphAuthenticated -eq $true) {
        foreach ($Object in $FoundUsers) {
            [PSCustomObject] $Object
        }
    }
    else {
        $FoundUsers
    }
}
function Add-GraphGroup {
    [CmdletBinding()]
    param(
        [alias('Authorization')][System.Collections.IDictionary] $Headers,
        [string] $DisplayName,
        [string] $Name,
        [string] $Description,
        [string] $MailNickname,
        [switch] $SecurityEnabled,
        [switch] $MailEnabled,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    $URI = "/groups"
    $BaseUri = 'https://graph.microsoft.com/v1.0'
    $Body = [ordered]@{
        groupTypes = @()
    }

    if ($PSBoundParameters.ContainsKey('DisplayName')) {
        $Body['displayName'] = $DisplayName
    }
    if ($PSBoundParameters.ContainsKey('Name')) {
        $Body['name'] = $Name
    }
    if ($PSBoundParameters.ContainsKey('Description')) {
        $Body['description'] = $Description
    }
    if ($PSBoundParameters.ContainsKey('MailNickname')) {
        $Body['mailNickname'] = $MailNickname
    }
    if ($PSBoundParameters.ContainsKey('SecurityEnabled')) {
        $Body['securityEnabled'] = $SecurityEnabled.IsPresent
    }
    if ($PSBoundParameters.ContainsKey('MailEnabled')) {
        $Body['mailEnabled'] = $MailEnabled.IsPresent
    }
    if ($Body.Count -gt 0) {
        Invoke-Graphimo -Uri $URI -Method POST -Headers $Headers -Body $Body -BaseUri $BaseUri -MgGraph:$MgGraph.IsPresent
    }
}
function Add-GraphGroupMember {
    [cmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Headers')]
    param(
        [parameter()][alias('Authorization')][System.Collections.IDictionary] $Headers,
        [parameter(Mandatory)][alias('GroupID')][string] $ID,
        [parameter(Mandatory)][string] $MemberID,
        [parameter()][switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    $URI = "/groups/$ID/members/`$ref"

    $Body = @{
        "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$MemberID"
    }

    Invoke-Graphimo -Uri $URI -Method POST -Headers $Headers -Body $Body -MgGraph:$MgGraph.IsPresent
}
function Add-GraphUser {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [parameter()][alias('Authorization')][System.Collections.IDictionary] $Headers,
        [Parameter(Mandatory)][string] $UserPrincipalName,
        [string] $Name,
        [parameter(Mandatory)][alias('AccountEnabled')][bool] $Enabled,
        [alias('FirstName')][string] $GivenName,
        [alias('LastName')][string] $Surname,
        [alias('Title')][string] $JobTitle,
        [string] $EmployeeId,
        [string] $City,
        [Parameter(Mandatory)][string] $MailNickname,
        [alias('EmailAddress')][string] $Mail,
        [string] $Country,
        [string] $Department,
        [string] $PostalCode,
        [alias('Fax')][string] $FaxNumber,
        [string] $State,
        [string] $StreetAddress,
        [alias('OfficePhone')][string] $BusinessPhones,
        [alias('Mobile')][string] $MobilePhone,
        [string] $OfficeLocation,
        [string] $CompanyName,
        [Parameter(Mandatory)][string] $DisplayName,
        [switch] $ShowInAddressList,
        [switch] $DoNotForceChangePasswordNextSignIn,
        [string] $EmployeeType,
        [Parameter(Mandatory)][string] $Password,
        [alias('HireDate')][DateTime] $StartDate,
        [alias('CustomProperty')][System.Collections.IDictionary] $CustomProperties,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    $URI = "/users"
    $Body = [ordered]@{}

    if ($PSBoundParameters.ContainsKey('StartDate')) {

        $Body['employeeHireDate'] = $StartDate
    }
    if ($PSBoundParameters.ContainsKey('UserPrincipalName')) {
        $Body['userPrincipalName'] = $UserPrincipalName
    }
    if ($PSBoundParameters.ContainsKey('JobTitle')) {
        $Body['jobTitle'] = $JobTitle
    }
    if ($PSBoundParameters.ContainsKey('EmployeeId')) {
        $Body['employeeId'] = $EmployeeId
    }
    if ($PSBoundParameters.ContainsKey('MailNickname')) {
        $Body['mailNickname'] = $MailNickname
    }
    if ($PSBoundParameters.ContainsKey('Mail')) {
        $Body['mail'] = $Mail
    }
    if ($PSBoundParameters.ContainsKey('FaxNumber')) {
        $Body['faxNumber'] = $FaxNumber
    }
    if ($PSBoundParameters.ContainsKey('givenName')) {
        $Body['givenName'] = $givenName
    }
    if ($PSBoundParameters.ContainsKey('Surname')) {
        $Body['surname'] = $Surname
    }
    if ($PSBoundParameters.ContainsKey('City')) {
        $Body['city'] = $City
    }
    if ($PSBoundParameters.ContainsKey('Country')) {
        $Body['country'] = $Country
    }
    if ($PSBoundParameters.ContainsKey('Department')) {
        $Body['department'] = $Department
    }
    if ($PSBoundParameters.ContainsKey('PostalCode')) {
        $Body['postalCode'] = $PostalCode
    }
    if ($PSBoundParameters.ContainsKey('State')) {
        $Body['state'] = $State
    }
    if ($PSBoundParameters.ContainsKey('StreetAddress')) {
        $Body['streetAddress'] = $StreetAddress
    }
    if ($PSBoundParameters.ContainsKey('businessPhones')) {
        $Body['businessPhones'] = @($businessPhones)
    }
    if ($PSBoundParameters.ContainsKey('mobilePhone')) {
        $Body['mobilePhone'] = $mobilePhone
    }
    if ($PSBoundParameters.ContainsKey('OfficeLocation')) {
        $Body['officeLocation'] = $OfficeLocation
    }
    if ($PSBoundParameters.ContainsKey('CompanyName')) {
        $Body['companyName'] = $CompanyName
    }
    if ($PSBoundParameters.ContainsKey('DisplayName')) {
        $Body['displayName'] = $DisplayName
    }
    if ($PSBoundParameters.ContainsKey('ShowInAddressList')) {
        $Body['showInAddressList'] = $ShowInAddressList.IsPresent
    }
    if ($PSBoundParameters.ContainsKey('Enabled')) {
        $Body['accountEnabled'] = $Enabled
    }
    if ($PSBoundParameters.ContainsKey('EmployeeType')) {
        $Body['employeeType'] = $EmployeeType
        $BaseUri = 'https://graph.microsoft.com/beta'
    }
    else {
        $BaseUri = 'https://graph.microsoft.com/v1.0'
    }
    $Body['passwordProfile'] = @{
        forceChangePasswordNextSignIn = -not $DoNotForceChangePasswordNextSignIn.IsPresent
        password                      = $Password
    }

    foreach ($Property in $CustomProperties.Keys) {
        $Body[$Property] = $CustomProperties[$Property]
    }

    if ($Body.Count -gt 0) {
        Invoke-Graphimo -Uri $URI -Method POST -Headers $Headers -Body $Body -BaseUri $BaseUri -MgGraph:$MgGraph.IsPresent
    }
}
function Connect-Graphimo {
    [cmdletBinding(DefaultParameterSetName = 'ClearText')]
    param(
        [parameter(Mandatory, ParameterSetName = 'Encrypted')]
        [parameter(Mandatory, ParameterSetName = 'ClearText')][string][alias('ClientID')] $ApplicationID,
        [parameter(Mandatory, ParameterSetName = 'ClearText')][string][alias('ClientSecret')] $ApplicationKey,
        [parameter(Mandatory, ParameterSetName = 'Encrypted')][string][alias('ClientSecretEncrypted')] $ApplicationKeyEncrypted,
        [parameter(Mandatory, ParameterSetName = 'Credential')][PSCredential] $Credential,

        [parameter(Mandatory, ParameterSetName = 'Encrypted')]
        [parameter(Mandatory, ParameterSetName = 'ClearText')]
        [parameter(Mandatory, ParameterSetName = 'Credential')]
        [string] $TenantDomain,

        [parameter(ParameterSetName = 'Encrypted')]
        [parameter(ParameterSetName = 'ClearText')]
        [parameter(ParameterSetName = 'Credential')]
        [ValidateSet("https://manage.office.com", "https://graph.microsoft.com", "https://graph.microsoft.com/beta", 'https://graph.microsoft.com/.default')] $Resource = 'https://graph.microsoft.com/.default',
        [int] $ExpiresTimeout = 30,
        [switch] $ForceRefesh,

        [parameter(Mandatory, ParameterSetName = 'MsalToken')][System.Collections.IDictionary] $MsalToken,
        [parameter(Mandatory, ParameterSetName = 'MsGraphConfiguration')]
        [System.Collections.IDictionary] $MsGraphConfiguration,

        [parameter(Mandatory, ParameterSetName = 'MsGraphConfigurationSwitch')]
        [switch] $MgGraph
    )

    if (-not $Script:AuthorizationCache) {
        $Script:AuthorizationCache = [ordered] @{}
    }

    if ($null -ne $MsGraphConfiguration -or $MgGraph.IsPresent) {
        if ($MgGraph.IsPresent) {

            $Script:MgGraphAuthenticated = $true
            return
        }
        else {
            if (-not $MsGraphConfiguration.ErrorAction) {
                $MsGraphConfiguration.ErrorAction = 'Stop'
            }
            try {
                Connect-MgGraph @MsGraphConfiguration
                $Script:MgGraphAuthenticated = $true
                return
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    throw
                }
                $Script:MgGraphAuthenticated = $null
                $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " "
                Write-Warning -Message "Connect-Graphimo - Error: $ErrorMessage"
                return $false
            }
        }
    }

    if ($Credential) {
        $RestSplat = @{
            ErrorAction = 'Stop'
            Method      = 'POST'
            Body        = @{
                grant_type    = "client_credentials"
                client_id     = $Credential.UserName
                client_secret = $Credential.GetNetworkCredential().Password
            }
        }
    }
    elseif ($ApplicationKey -or $ApplicationKeyEncrypted) {
        if ($ApplicationKeyEncrypted) {
            try {
                $ApplicationKeyTemp = $ApplicationKeyEncrypted | ConvertTo-SecureString -ErrorAction Stop
            }
            catch {
                $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " "
                Write-Warning -Message "Connect-Graphimo - Error: $ErrorMessage"
                return
            }
            $ApplicationKey = [System.Net.NetworkCredential]::new([string]::Empty, $ApplicationKeyTemp).Password
        }
        $RestSplat = @{
            ErrorAction = 'Stop'
            Method      = 'POST'
            Body        = @{
                grant_type    = "client_credentials"
                client_id     = $ApplicationID
                client_secret = $ApplicationKey
            }
        }
    }
    elseif ($MsalToken) {
        $Authorization = @{
            Splat = $MsalToken
        }
        return Connect-MsalToken -Authorization $Authorization -ExpiresTimeout $ExpiresTimeout -ForceRefesh:$ForceRefesh
    }

    if ($Script:AuthorizationCache[$ApplicationID] -and -not $ForceRefesh) {
        if ($Script:AuthorizationCache[$ApplicationID].ExpiresOn -gt [datetime]::UtcNow) {
            Write-Verbose "Connect-Graphimo - Using cache for $ApplicationID"
            return $Script:AuthorizationCache[$ApplicationID]
        }
    }

    if ($Resource -in 'https://graph.microsoft.com/.default', "https://graph.microsoft.com/beta") {

        $RestSplat['Body']['scope'] = $Resource
        $RestSplat['Uri'] = "https://login.microsoftonline.com/$($TenantDomain)/oauth2/v2.0/token"
    }
    else {

        $RestSplat['Body']['resource'] = $Resource
        $RestSplat['Uri'] = "https://login.microsoftonline.com/$($TenantDomain)/oauth2/token"
    }
    Write-Verbose "Connect-Graphimo - EndPoint $($RestSplat['Uri'])"
    try {
        $Authorization = Invoke-RestMethod @RestSplat -Verbose:$false
        $Key = [ordered] @{
            'Authorization' = "$($Authorization.token_type) $($Authorization.access_token)"
            'Extended'      = $Authorization
            'Error'         = ''
            'ExpiresOn'     = ([datetime]::UtcNow).AddSeconds($Authorization.expires_in - $ExpiresTimeout)
            'Splat'         = [ordered] @{
                ApplicationID  = $RestSplat['Body']['client_id']
                ApplicationKey = $RestSplat['Body']['client_secret']
                TenantDomain   = $TenantDomain
                Resource       = $Resource
            }
        }
        $Script:AuthorizationCache[$ApplicationID] = $Key
    }
    catch {
        $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " "
        Write-Warning -Message "Connect-Graphimo - Error: $ErrorMessage"
        $Key = [ordered] @{
            'Authorization' = $Null
            'Extended'      = $Null
            'Error'         = $ErrorMessage
            'ExpiresOn'     = $null
            'Splat'         = [ordered] @{
                ApplicationID  = $RestSplat['Body']['client_id']
                ApplicationKey = $RestSplat['Body']['client_secret']
                TenantDomain   = $TenantDomain
                Resource       = $Resource
            }
        }
    }
    $Key
}
function Get-GraphApplication {
    [cmdletBinding()]
    param(
        [parameter()][alias('Authorization')][System.Collections.IDictionary] $Headers,
        [string] $ID,
        [string] $DisplayName,
        [string[]] $Property,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    if ($ID) {

        $RelativeURI = "/applications/$ID"
        $QueryParameter = @{
            '$Select' = $Property -join ','
        }
    }
    elseif ($DisplayName) {
        $RelativeURI = '/applications'
        $QueryParameter = @{
            '$Select'  = $Property -join ','
            '$filter'  = "displayName eq '$DisplayName'"
            '$orderby' = $OrderBy
        }
    }
    else {

        $RelativeURI = '/applications'
        $QueryParameter = @{
            '$Select'  = $Property -join ','
            '$filter'  = $Filter
            '$orderby' = $OrderBy
        }
    }
    Remove-EmptyValue -Hashtable $QueryParameter
    Invoke-Graphimo -Uri $RelativeURI -Method GET -Headers $Headers -QueryParameter $QueryParameter -MgGraph:$MgGraph.IsPresent
}
function Get-GraphContact {
    [cmdletBinding()]
    param(
        [parameter()][alias('Authorization')][System.Collections.IDictionary] $Headers,
        [string] $Id,
        [string[]] $Property,
        [string] $Filter,
        [string] $OrderBy,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    if ($ID) {

        $RelativeURI = "/contacts/$ID"
        $QueryParameter = @{
            '$Select' = $Property -join ','
        }
    }
    else {

        $RelativeURI = '/contacts'
        $QueryParameter = @{
            '$Select'  = $Property -join ','
            '$filter'  = $Filter
            '$orderby' = $OrderBy
        }
    }
    Remove-EmptyValue -Hashtable $QueryParameter
    Invoke-Graphimo -Uri $RelativeURI -Method GET -Headers $Headers -QueryParameter $QueryParameter -MgGraph:$MgGraph.IsPresent
}
function Get-GraphGroup {
    [cmdletBinding()]
    param(
        [alias('Authorization')][System.Collections.IDictionary] $Headers,
        [string] $Id,
        [string[]] $Property,
        [string] $Filter,
        [string] $OrderBy,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    if ($ID) {

        $RelativeURI = "/groups/$ID"
        $QueryParameter = @{
            '$Select' = $Property -join ','
        }
    }
    else {

        $RelativeURI = '/groups'
        $QueryParameter = @{
            '$Select'  = $Property -join ','
            '$filter'  = $Filter
            '$orderby' = $OrderBy
        }
    }
    Remove-EmptyValue -Hashtable $QueryParameter
    Invoke-Graphimo -Uri $RelativeURI -Method GET -Headers $Headers -QueryParameter $QueryParameter -MgGraph:$MgGraph.IsPresent
}
function Get-GraphGroupMember {
    [cmdletBinding()]
    param(
        [alias('Authorization')][System.Collections.IDictionary] $Headers,
        [parameter(Mandatory)][string] $Id,
        [string] $Search,
        [string[]] $Property,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    if ($ID) {

        $RelativeURI = "/groups/$ID/members"
        $QueryParameter = @{
            '$Select' = $Property -join ','
            '$Search' = $Search
        }
    }
    Remove-EmptyValue -Hashtable $QueryParameter
    $invokeGraphimoSplat = @{
        Uri            = $RelativeURI
        Method         = 'GET'
        Headers        = $Headers
        QueryParameter = $QueryParameter
        MgGraph        = $MgGraph.IsPresent
    }
    if ($QueryParameter.'$Search') {

        $invokeGraphimoSplat['ConsistencyLevel'] = 'eventual'
    }

    Invoke-Graphimo @invokeGraphimoSplat
}
function Get-GraphUser {
    [alias('Get-GraphUsers')]
    [cmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [parameter(ParameterSetName = 'Default')]
        [parameter(ParameterSetName = 'EmailAddress')]
        [parameter(ParameterSetName = 'UserPrincipalName')]
        [parameter(ParameterSetName = 'Filter')]
        [parameter(ParameterSetName = 'Id')]
        [alias('Authorization')][System.Collections.IDictionary] $Headers,

        [parameter(ParameterSetName = 'Id')][string] $Id,
        [parameter(ParameterSetName = 'UserPrincipalName')][string] $UserPrincipalName,
        [alias('Mail')][parameter(ParameterSetName = 'EmailAddress')][string] $EmailAddress,

        [string[]] $Property,

        [parameter(ParameterSetName = 'Filter')][string] $Filter,
        [string] $OrderBy,
        [switch] $IncludeManager,
        [int] $First,
        [string] $CountVariable,
        [string] $ConsistencyLevel,

        [switch] $DebugRequest,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    if ($Property -contains 'EmployeeType' -or $Property -contains 'onPremisesExtensionAttributes') {
        $BaseURI = 'https://graph.microsoft.com/beta'
    }
    else {
        $BaseURI = 'https://graph.microsoft.com/v1.0'
    }
    $NewProperties = foreach ($P in $Property) {
        if ($P -like "extensionAttribute*") {
            'onPremisesExtensionAttributes'
        }
        else {
            $P
        }
    }
    $Property = $NewProperties | Select-Object -Unique

    if ($UserPrincipalName) {
        $RelativeURI = '/users'
        $QueryParameter = [ordered]@{
            '$Select' = $Property -join ','
            '$filter' = "userPrincipalName eq '$UserPrincipalName'"
        }
    }
    elseif ($EmailAddress) {
        $RelativeURI = '/users'
        $QueryParameter = [ordered]@{
            '$Select' = $Property -join ','
            '$filter' = "mail eq '$EmailAddress'"
        }
    }
    elseif ($ID) {

        $RelativeURI = "/users"
        $QueryParameter = [ordered]@{
            '$filter' = "id eq '$ID'"
            '$Select' = $Property -join ','
        }
    }
    else {

        $RelativeURI = '/users'
        $QueryParameter = [ordered]@{
            '$Select'  = $Property -join ','
            '$filter'  = $Filter
            '$orderby' = $OrderBy
        }
    }
    if ($IncludeManager) {
        $QueryParameter['$expand'] = 'manager'
    }
    if ($CountVariable) {
        $QueryParameter['$count'] = 'true'
    }

    Remove-EmptyValue -Hashtable $QueryParameter

    if ($DebugRequest) {
        Write-Verbose ("Get-GraphUser request: BaseUri={0} Uri={1} Select={2} Filter={3} Expand={4} OrderBy={5} Count={6} ConsistencyLevel={7}" -f $BaseURI, $RelativeURI, $QueryParameter['$Select'], $QueryParameter['$filter'], $QueryParameter['$expand'], $QueryParameter['$orderby'], $QueryParameter['$count'], $ConsistencyLevel)
    }

    $invokeGraphimoSplat = @{
        Uri              = $RelativeURI
        Method           = 'GET'
        QueryParameter   = $QueryParameter
        BaseUri          = $BaseURI
        First            = $First
        CountVariable    = $CountVariable
        ConsistencyLevel = $ConsistencyLevel
        MgGraph          = $MgGraph.IsPresent
    }
    if ($Headers) {
        $invokeGraphimoSplat['Headers'] = $Headers
    }

    if ($Property -contains 'onPremisesExtensionAttributes') {
        $OutputData = Invoke-Graphimo @invokeGraphimoSplat
        if ($OutputData) {
            $OutputData | Convert-GraphInternalUser
        }
    }
    else {
        Invoke-Graphimo @invokeGraphimoSplat
    }
}

function Import-GraphGuest {
    [cmdletBinding()]
    param(
        [parameter()][alias('Authorization')][System.Collections.IDictionary] $Headers,
        [string] $Name,
        [Parameter(Mandatory)][string] $EmailAddress,
        [switch] $SendInvitationMessage,
        [string] $InviteRedirectUrl = "https://portal.office.com",
        [switch] $ResetRedemption,
        [string] $InvitedUserID,
        [ValidateSet('Member', 'Guest')][string] $UserType,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    $URI = '/invitations'
    $body = [ordered]@{
        'invitedUserDisplayName'  = $Name
        'invitedUserEmailAddress' = $EmailAddress
        'inviteRedirectUrl'       = $InviteRedirectUrl
        'sendInvitationMessage'   = $SendInvitationMessage.IsPresent
        'resetRedemption'         = $ResetRedemption.IsPresent
    }
    if ($UserType) {
        $Body['invitedUserType'] = $UserType
    }
    if ($InvitedUserID) {
        $Body['invitedUser'] = @{
            'id' = $InvitedUserID
        }
    }
    Invoke-Graphimo -Uri $URI -Method POST -Headers $Headers -Body $Body -MgGraph:$MgGraph.IsPresent
}
function Invoke-Graphimo {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [alias('PrimaryUri')][uri] $BaseUri = 'https://graph.microsoft.com/v1.0',
        [uri] $Uri,
        [parameter()][alias('Authorization')][System.Collections.IDictionary] $Headers,
        [validateset('GET', 'DELETE', 'POST', 'PATCH', 'PUT')][string] $Method = 'GET',
        [string] $ContentType = "application/json; charset=UTF-8",
        [System.Collections.IDictionary] $Body,
        [System.Collections.IDictionary] $QueryParameter,
        [switch] $FullUri,
        [string] $CountVariable,
        [int] $First,
        [string] $ConsistencyLevel,
        [switch] $MgGraph
    )
    if ($MgGraph -or $Script:MgGraphAuthenticated -eq $true) {
    }
    elseif ($Headers.MsalToken) {
        if ($Headers.Splat) {
            $Splat = $Headers.Splat
            $Headers = Connect-MsalToken -Authorization $Headers
        }
    }
    else {
        if (-not $Headers) {
            Write-Warning "No headers provided. Skipping."
            return
        }

        if ($Headers.Splat) {
            $Splat = $Headers.Splat
            $Headers = Connect-Graphimo @Splat
        }
    }

    if ($Headers.Error) {
        Write-Warning "Invoke-Graphimo - Authorization error. Skipping."
        return
    }
    if ($Headers.MsalToken) {
        $RestSplat = @{
            Headers     = @{
                Authorization = $Headers.MsalToken.TokenType + ' ' + $Headers.MsalToken.AccessToken
            }
            Method      = $Method
            ContentType = $ContentType
        }
    }
    else {
        $RestSplat = @{
            Headers     = $Headers
            Method      = $Method
            ContentType = $ContentType
        }
    }
    if ($Body) {
        $RestSplat['Body'] = $Body | ConvertTo-Json -Depth 5
    }
    if ($FullUri) {
        $RestSplat.Uri = $Uri
    }
    else {
        $RestSplat.Uri = Join-UriQuery -BaseUri $BaseUri -RelativeOrAbsoluteUri $Uri -QueryParameter $QueryParameter
    }
    if ($RestSplat['Body']) {
        $WhatIfInformation = "Invoking [$Method] " + [System.Environment]::NewLine + $RestSplat['Body'] + [System.Environment]::NewLine
    }
    else {
        $WhatIfInformation = "Invoking [$Method] / [ConsistencyLevel: $($ConsistencyLevel)] "
    }
    if ($ConsistencyLevel) {
        if (-not $RestSplat.Headers) {
            $RestSplat.Headers = [ordered]@{}
        }
        $RestSplat.Headers['ConsistencyLevel'] = $ConsistencyLevel
    }
    Remove-EmptyValue -Hashtable $RestSplat
    try {
        if ($Method -eq 'GET') {
            Write-Verbose "Invoke-Graphimo - $($WhatIfInformation)over URI $($RestSplat.Uri)"
            if ($MgGraph -or $Script:MgGraphAuthenticated -eq $true) {
                $OutputQuery = Invoke-MgGraphRequest @RestSplat -Verbose:$false
            }
            else {
                $OutputQuery = Invoke-RestMethod @RestSplat -Verbose:$false
            }
            $Count = 1
            [Array] $FoundUsers = Invoke-InternalGraphimo -OutputQuery $OutputQuery -First $First -CountVariable $CountVariable -MgGraph:$MgGraph.IsPresent
            $CurrentCount = $FoundUsers.Count
            if ($First -gt 0 -and $CurrentCount -ge $First) {
                return $FoundUsers
            }
            else {
                $FoundUsers
            }
            if ($OutputQuery.'@odata.nextLink') {
                do {
                    $RestSplat.Uri = $OutputQuery.'@odata.nextLink'
                    Write-Verbose "Invoke-Graphimo - $($WhatIfInformation)NextLink (Page $Count/Current Count: $($CurrentCount))) over URI $($RestSplat.Uri)"
                    if ($MgGraph -or $Script:MgGraphAuthenticated -eq $true) {
                        $OutputQuery = Invoke-MgGraphRequest @RestSplat -Verbose:$false
                    }
                    else {
                        $OutputQuery = Invoke-RestMethod @RestSplat -Verbose:$false
                    }
                    [Array] $FoundUsers = Invoke-InternalGraphimo -OutputQuery $OutputQuery -First $First -CurrentCount $CurrentCount -CountVariable $CountVariable -MgGraph:$MgGraph.IsPresent
                    $FoundUsers
                    $Count++
                    $CurrentCount = $CurrentCount + $FoundUsers.Count
                } until (($First -gt 0 -and $CurrentCount -ge $First) -or $null -eq $OutputQuery.'@odata.nextLink')
            }
        }
        else {
            Write-Verbose "Invoke-Graphimo - $($WhatIfInformation)over URI $($RestSplat.Uri)"
            if ($PSCmdlet.ShouldProcess($($RestSplat.Uri), $WhatIfInformation)) {
                if ($MgGraph -or $Script:MgGraphAuthenticated -eq $true) {
                    $OutputQuery = Invoke-MgGraphRequest @RestSplat -Verbose:$false
                }
                else {
                    $OutputQuery = Invoke-RestMethod @RestSplat -Verbose:$false
                }
                if ($Method -in 'POST') {
                    $OutputQuery
                }
                else {
                    return $true
                }
            }
        }
    }
    catch {
        $RestError = $_.ErrorDetails.Message
        if ($RestError) {
            try {
                $ErrorMessage = ConvertFrom-Json -InputObject $RestError
                Write-Warning -Message "Invoke-Graphimo - Error: $($_.Exception.Message) $($ErrorMessage.error.message)"
            }
            catch {
                Write-Warning -Message "Invoke-Graphimo - Error: $($_.Exception.Message)"
            }
        }
        else {
            Write-Warning -Message "Invoke-Graphimo - Error: $($_.Exception.Message)"
        }
        if ($Method -notin 'GET', 'POST') {
            return $false
        }
    }
}
function Remove-GraphGroupMember {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(ParameterSetName = 'All')]
        [parameter(ParameterSetName = 'PerID')]
        [parameter(ParameterSetName = 'BySearch')]
        [alias('Authorization')][System.Collections.IDictionary] $Headers,
        [parameter(Mandatory, ParameterSetName = 'All')]
        [parameter(Mandatory, ParameterSetName = 'PerID')]
        [parameter(Mandatory, ParameterSetName = 'BySearch')]
        [alias('GroupID')][string] $ID,

        [parameter(Mandatory, ParameterSetName = 'PerID')][string] $MemberID,

        [parameter(Mandatory, ParameterSetName = 'BySearch')][string] $Search,

        [parameter(ParameterSetName = 'All')][switch] $All,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    if ($All) {

        $Users = Get-GraphGroupMember -Id $ID -Headers $Headers -Verbose -Property id, displayName -MgGraph:$MgGraph.IsPresent
        foreach ($User in $Users) {
            $URI = "/groups/$ID/members/$($User.id)/`$ref"
            Invoke-Graphimo -Uri $URI -Method DELETE -Headers $Headers
        }
    }
    elseif ($Search) {
        $Users = Get-GraphGroupMember -Id $ID -Headers $Headers -Verbose -Property id, displayName -Search $Search -MgGraph:$MgGraph.IsPresent
        foreach ($User in $Users) {
            $URI = "/groups/$ID/members/$($User.id)/`$ref"
            Invoke-Graphimo -Uri $URI -Method DELETE -Headers $Headers -MgGraph:$MgGraph.IsPresent
        }
    }
    else {

        $URI = "/groups/$ID/members/$MemberID/`$ref"
        Invoke-Graphimo -Uri $URI -Method DELETE -Headers $Headers -MgGraph:$MgGraph.IsPresent
    }
}
function Remove-GraphManager {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [parameter()][alias('Authorization')][System.Collections.IDictionary] $Headers,
        [alias('UserID')][string] $ID,
        [string] $UserPrincipalName,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    if ($ID) {
        $URI = "/users/$ID/manager/`$ref"
    }
    else {
        $URI = "/users/$UserPrincipalName/manager/`$ref"
    }
    Invoke-Graphimo -Uri $URI -Method DELETE -Headers $Headers -MgGraph:$MgGraph.IsPresent
}
function Remove-GraphUser {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [parameter()][alias('Authorization')][System.Collections.IDictionary] $Headers,
        [string] $UserPrincipalName,
        [alias('UserID')][string] $ID,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    if ($ID) {
        $URI = "/users/$ID"
    }
    else {
        $URI = "/users/$UserPrincipalName"
    }
    Invoke-Graphimo -Uri $URI -Method DELETE -Headers $Headers -MgGraph:$MgGraph.IsPresent
}
function Set-GraphManager {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [parameter()][alias('Authorization')][System.Collections.IDictionary] $Headers,
        [alias('UserID')][string] $ID,
        [string] $UserPrincipalName,
        [string] $Name,
        [string] $ManagerID,
        [string] $ManagerDisplayName,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    if ($ID) {
        $URI = "/users/$ID/manager/`$ref"
    }
    else {
        $URI = "/users/$UserPrincipalName/manager/`$ref"
    }
    $Body = [ordered]@{
        "@odata.id" = "https://graph.microsoft.com/v1.0/users/$ManagerID"
    }
    Invoke-Graphimo -Uri $URI -Method PUT -Headers $Headers -Body $Body -MgGraph:$MgGraph.IsPresent
}
function Set-GraphUser {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [parameter()][alias('Authorization')][System.Collections.IDictionary] $Headers,
        [alias('UserID')][string] $ID,
        [string] $SearchUserPrincipalName,
        [string] $UserPrincipalName,
        [string] $Name,
        [alias('AccountEnabled')][nullable[bool]] $Enabled,
        [alias('FirstName')][string] $GivenName,
        [alias('LastName')][string] $Surname,
        [alias('Title')][string] $JobTitle,
        [string] $EmployeeId,
        [string] $City,
        [string] $MailNickname,
        [alias('EmailAddress')][string] $Mail,
        [string] $Country,
        [string] $Department,
        [string] $PostalCode,
        [alias('Fax')][string] $FaxNumber,
        [string] $State,
        [string] $StreetAddress,
        [alias('OfficePhone')][string] $BusinessPhones,
        [alias('Mobile')][string] $MobilePhone,
        [string] $OfficeLocation,
        [string] $CompanyName,
        [string] $DisplayName,
        [string] $EmployeeType,
        [switch] $ShowInAddressList,
        [alias('HireDate')][DateTime] $StartDate,
        [alias('CustomProperty')][System.Collections.IDictionary] $CustomProperties,
        [string] $ExtensionAttribute1,
        [string] $ExtensionAttribute2,
        [string] $ExtensionAttribute3,
        [string] $ExtensionAttribute4,
        [string] $ExtensionAttribute5,
        [string] $ExtensionAttribute6,
        [string] $ExtensionAttribute7,
        [string] $ExtensionAttribute8,
        [string] $ExtensionAttribute9,
        [string] $ExtensionAttribute10,
        [string] $ExtensionAttribute11,
        [string] $ExtensionAttribute12,
        [string] $ExtensionAttribute13,
        [string] $ExtensionAttribute14,
        [string] $ExtensionAttribute15,
        [System.Collections.IDictionary] $OnPremisesExtensionAttributes,
        [string] $UserType,
        [switch] $MgGraph
    )

    if (-not $MgGraph -and -not $Headers -and $Script:MgGraphAuthenticated -ne $true) {
        Write-Warning -Message "No headers or MgGraph switch provided. Skipping."
        return
    }

    $Body = [ordered]@{}

    if ($PSBoundParameters.ContainsKey('StartDate')) {

        $Body['employeeHireDate'] = $StartDate
    }
    if ($PSBoundParameters.ContainsKey('JobTitle')) {
        $Body['jobTitle'] = $JobTitle
    }
    if ($PSBoundParameters.ContainsKey('EmployeeId')) {
        $Body['employeeId'] = $EmployeeId
    }
    if ($PSBoundParameters.ContainsKey('UserPrincipalName')) {
        $Body['userPrincipalName'] = $UserPrincipalName
    }
    if ($PSBoundParameters.ContainsKey('MailNickname')) {
        $Body['mailNickname'] = $MailNickname
    }
    if ($PSBoundParameters.ContainsKey('Mail')) {
        $Body['mail'] = $Mail
    }
    if ($PSBoundParameters.ContainsKey('FaxNumber')) {
        $Body['faxNumber'] = $FaxNumber
    }
    if ($PSBoundParameters.ContainsKey('givenName')) {
        $Body['givenName'] = $givenName
    }
    if ($PSBoundParameters.ContainsKey('Surname')) {
        $Body['surname'] = $Surname
    }
    if ($PSBoundParameters.ContainsKey('City')) {
        $Body['city'] = $City
    }
    if ($PSBoundParameters.ContainsKey('Country')) {
        $Body['country'] = $Country
    }
    if ($PSBoundParameters.ContainsKey('Department')) {
        $Body['department'] = $Department
    }
    if ($PSBoundParameters.ContainsKey('PostalCode')) {
        $Body['postalCode'] = $PostalCode
    }
    if ($PSBoundParameters.ContainsKey('State')) {
        $Body['state'] = $State
    }
    if ($PSBoundParameters.ContainsKey('StreetAddress')) {
        $Body['streetAddress'] = $StreetAddress
    }
    if ($PSBoundParameters.ContainsKey('businessPhones')) {
        $Body['businessPhones'] = @($businessPhones)
    }
    if ($PSBoundParameters.ContainsKey('mobilePhone')) {
        $Body['mobilePhone'] = $mobilePhone
    }
    if ($PSBoundParameters.ContainsKey('OfficeLocation')) {
        $Body['officeLocation'] = $OfficeLocation
    }
    if ($PSBoundParameters.ContainsKey('CompanyName')) {
        $Body['companyName'] = $CompanyName
    }
    if ($PSBoundParameters.ContainsKey('DisplayName')) {
        $Body['displayName'] = $DisplayName
    }
    if ($PSBoundParameters.ContainsKey('ShowInAddressList')) {
        $Body['showInAddressList'] = $ShowInAddressList.IsPresent
    }
    if ($PSBoundParameters.ContainsKey('Enabled')) {
        $Body['accountEnabled'] = $Enabled
    }
    if ($PSBoundParameters.ContainsKey('EmployeeType')) {
        $Body['employeeType'] = $EmployeeType
        $BaseUri = 'https://graph.microsoft.com/beta'
    }
    else {
        $BaseUri = 'https://graph.microsoft.com/v1.0'
    }
    foreach ($Property in $CustomProperties.Keys) {
        $Body[$Property] = $CustomProperties[$Property]
    }

    $Body['onPremisesExtensionAttributes'] = [ordered] @{}
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute1')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute1'] = $ExtensionAttribute1
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute2')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute2'] = $ExtensionAttribute2
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute3')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute3'] = $ExtensionAttribute3
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute4')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute4'] = $ExtensionAttribute4
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute5')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute5'] = $ExtensionAttribute5
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute6')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute6'] = $ExtensionAttribute6
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute7')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute7'] = $ExtensionAttribute7
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute8')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute8'] = $ExtensionAttribute8
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute9')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute9'] = $ExtensionAttribute9
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute10')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute10'] = $ExtensionAttribute10
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute11')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute11'] = $ExtensionAttribute11
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute12')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute12'] = $ExtensionAttribute12
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute13')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute13'] = $ExtensionAttribute13
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute14')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute14'] = $ExtensionAttribute14
    }
    if ($PSBoundParameters.ContainsKey('ExtensionAttribute15')) {
        $Body['onPremisesExtensionAttributes']['extensionAttribute15'] = $ExtensionAttribute15
    }
    if ($PSBoundParameters.ContainsKey("UserType")) {
        $Body['userType'] = $UserType
    }
    if ($ID) {
        $URI = "/users/$ID"
    }
    else {
        $URI = "/users/$SearchUserPrincipalName"
    }
    if ($Body['onPremisesExtensionAttributes'].Count -eq 0) {
        $Body.Remove('onPremisesExtensionAttributes')
    }
    if ($Body.Count -gt 0) {
        $UriEncoded = [System.Web.HttpUtility]::UrlEncode($Uri)
        Invoke-Graphimo -Uri $UriEncoded -Method PATCH -Headers $Headers -Body $Body -BaseUri $BaseUri -MgGraph:$MgGraph.IsPresent
    }
    else {
        Write-Warning -Message "Set-GraphUser - No changes were made to the user, as no field to change."
    }
}

Export-ModuleMember -Function @('Add-GraphGroup', 'Add-GraphGroupMember', 'Add-GraphUser', 'Connect-Graphimo', 'Get-GraphApplication', 'Get-GraphContact', 'Get-GraphGroup', 'Get-GraphGroupMember', 'Get-GraphUser', 'Import-GraphGuest', 'Invoke-Graphimo', 'Remove-GraphGroupMember', 'Remove-GraphManager', 'Remove-GraphUser', 'Set-GraphManager', 'Set-GraphUser') -Alias @('Get-GraphUsers')
# SIG # Begin signature block
# MIIt6AYJKoZIhvcNAQcCoIIt2TCCLdUCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAz7nZoTu8nwOPy
# f/wtcWFa4DDfZU5y5PcYTL1FbE3gRKCCJuUwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqGSIb3DQEBDAUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1
# c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0CiteLdd1TlZG7GIQvUz
# jOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS1WcaUk8OoVf8iJnB
# kcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZMswEgJfMQ04uy+wjw
# iuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBCMo/7xuoc82VokaJN
# TIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3/hoGlMJ8yOobMubK
# wvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26jq22YBoMbt2pnLdK9
# RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5OBDm6s6R9b7T+2+T
# YTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo7AqnsAMrkbI2eb73
# rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzUtw1T0PhH5nUwjeww
# k3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwmKwH0iU1Z23jPgUo+
# QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NPTLuqDQhTQXxYPUez
# +rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/BAgwBgEB/wIBADAd
# BgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0jBBgwFoAU7NfjgtJx
# XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF
# BwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
# aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo
# dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy
# bDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkqhkiG9w0BAQwFAAOC
# AgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDKr2wwJxMSnpBEn0v9
# nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvdaqpDP9ZNq4+sg0dVQ
# eYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+lP92OT2e1JnPSt0o
# 618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7abrs2nfvlIVNaw8rp
# avGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNSy0bYxDQcoqVLjc1v
# djcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeKiwmhywvpMRr/Lhlc
# OXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYbKyP/j7XqiHtwa+ag
# uv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8QxAwIZDwzj64ojDzL
# j4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVmzGwOysWGw/YmMwwH
# S6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCnHwZ05/LWUpD9r4VI
# IflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4wgga0MIIEnKADAgEC
# AhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVT
# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
# b20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcw
# MDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E
# aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l
# U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0ZodLRRF51NrY0NlLWZ
# loMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi6wuim5bap+0lgloM
# 2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNgxVBdJkf77S2uPoCj
# 7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQ
# Sku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJm/s80FiocSk1VYLZ
# lDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+
# 8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1ZlAeSpQl92QOMeRx
# ykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9MmeOreGPRdtBx3yG
# OP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7Yk/ehb//Wx+5kMqI
# MRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm
# 1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6X5uAiynM7Bu2ayBj
# UwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU729T
# SunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4c
# D08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUF
# BwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEG
# CCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5k
# aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAX
# MAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIBABfO+xaA
# HP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQ
# M2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt
# 6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0F8HABBgr0UdqirZ7
# bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmS
# Nq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKfZxAvBAKqMVuqte69
# M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzEwlvzZiiyfTPjLbnF
# RsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmM
# Thi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOXgpIUsWTjd6xpR6oa
# Qf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx
# 9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3
# /BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN
# 8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQg
# VGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAw
# MDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp
# Z2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBU
# aW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
# ADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx
# +wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvN
# Zh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlL
# nh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmn
# cOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjPKHW5KqCvpSduSwhw
# UmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL
# 4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7wJNdoRORVbPR1VVnD
# uSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCy
# FG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOgrY7rlRyTlaCCfw7a
# SUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K096V1hE0yZIXe+gi
# AwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGj
# ggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zyMe39/dfzkXFjGVBD
# z2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8E
# BAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGF
# MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUH
# MAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH
# NFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBW
# MFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVk
# RzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkw
# FzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3x
# HCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh
# 8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/ML9lFfim8/9yJmZS
# e2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu+WUqW4daIqToXFE/
# JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4obEMnxYOX8VBRKe1u
# NnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq
# 8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasnM9AWcIQfVjnzrvwi
# CZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol/DJgddJ35XTxfUlQ
# +8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1
# R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3ocCVccAvlKV9jEnstr
# niLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcBZU8atufk+EMF/cWu
# iC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB18wggVHoAMCAQICEAfCUnQo
# FKLWq/4k6hfl3S4wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNV
# BAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0
# IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMTAeFw0yMzA0MTYw
# MDAwMDBaFw0yNjA3MDYyMzU5NTlaMGcxCzAJBgNVBAYTAlBMMRIwEAYDVQQHDAlN
# aWtvxYLDs3cxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVWT1RFQzEhMB8G
# A1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAlJoHlzELSGimkpCr2wLfBhWSdcsDh/EsMZU7rODHMq1p
# lTq0QVUUAPAKRfRWnqG8JpGcb5MUExSxypvvJJ8KJhFLJXGvAqkjiNGMBC7+RME1
# RIdAvw2nob8aOrZJjTxff0j9Sgt3NJdbzvjO73TVRikCEK4cauxBtInswWTgIrpD
# XRlV0WDi5+O1d6i+T8Bv6LtmpSf74nyA2nfNahW/kJFIdNiaNuEjI1nSg8rXazF4
# tNt+QjeEa1vvII30Sfnyio4DCJm7nHgrIvSL9Wuum1HPWpwHpjm0+JheVP8kAYAL
# gKN/o1QfMIlHfO5FEDtMyQhfL6tmK1Ts/DiZjF/IICLBBFGdwmSg9IVXN3Zu3Fkg
# MPPxTcxjT5QGiMc11/ang9BIGgi0ZCLQN7d3kFviAF8kv/WZ56RVKA70BmyvkOP2
# z9Im/fFy30KcVRkbtHAldDYO+wyJERfiMkdT3MFQKvjs1VN7ynqNub/657Ylwpgs
# YluKB2DtvHkkP3iAHJ4ovt7igzWayNeT+1cQ65FCHOhbYkrzocHNwM2PrxH4r1JB
# SkasL0kq+Hwq65JO89kHu9mcJcNhA0VR8stH1FRjvUDLoehN0cJyS/eoqdGpXJoS
# gARqCKkltOZ13QlG5F5oTwk0+Z2kA7mdVJAF22T0oSo2z8M3Vz9m/CPZ0PPVUoEC
# AwEAAaOCAgMwggH/MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0G
# A1UdDgQWBBR68WolWbgyccRJNeWy6DLhSOdt9zAOBgNVHQ8BAf8EBAMCB4AwEwYD
# VR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2Ny
# bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0
# MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIw
# MjFDQTEuY3JsMD4GA1UdIAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0
# dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBlAYIKwYBBQUHAQEEgYcwgYQwJAYI
# KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBcBggrBgEFBQcwAoZQ
# aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29k
# ZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQwCQYDVR0TBAIwADANBgkq
# hkiG9w0BAQsFAAOCAgEAtxHh11D4aXt9Stgy+Nx34eqpLwR8kdUZQ/ZVSJJXEQke
# dGR86FrOhAZUxcqIb5KXJVQrkXUFt97Uur7SjzrnKQw7+MLAPus5CWCPHx6Lluk6
# mtVuO2Eq3OQDkoSHCffjaTWyjRood3aEpXIqNplCgl+SP2a8yQZEKSdJGIWv6VEk
# 9gmxNya6CX9r0FhlIiPidy3YjzR5oTtZfs2kJEsb9HFQxEzH0BmSikVREmehYOtW
# 9HY70EseddDHW8bSjI70t2bQMrap0B5NYqT/kYPjOZRR60pFJZ6Rmvn957kIcQ2+
# zfRPIVFXr8QC06xYn4PM4bJVUR+fw3/wsZTClwu6Kd9PwMkLDkMR1tbjcd7RtQzI
# Is6cAWrK8YesGu4mgPi6dO6tSPdni4a2G7cN8QtrzSBnTHTe53e+sjCI3WJwJ+69
# /MMLWidymA9EE5e+xAfLv+XArN0oWXQ3coOCuzaCZfIhB626raKABzjC4iaYi9ov
# WJ/JEDAev0OkTDtyFDy7snAfaOgzYsEB3+ibeaFuz9PZOTccQRJpLMcDW5mbzUOu
# WZ93sVACqhvsd9RIM+SGeFP4z80WpRJRCKUtK4K1YPEfKRDoXfeZhM6eVhEShcl4
# Xupwem0mB7/HJSwFdIjJLt9PK6X4zIkJKktyy831CeTh6rSikDTC8c/c9fOVArMx
# ggZZMIIGVQIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg
# UlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEAfCUnQoFKLWq/4k6hfl3S4wDQYJYIZI
# AWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0B
# CQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAv
# BgkqhkiG9w0BCQQxIgQgXxCnsVvnVi0g92M0b4HAgBWkxc43PGHGJIg4ZrgRLbww
# DQYJKoZIhvcNAQEBBQAEggIAOCzRO8LGEsDL0LcMFdaJoR4siH2ovEbsMrPAcOYB
# Sm/33Y1QYEEb3XYHxw7IDvxQ26xxLWixxxOgcrRzDsSE13FrwZ9Qnxq377+wwRqt
# 6AvGgvbtQa+umke8rfej9n+55I6q1lixMcQ9kyNk+7m6Q9h4v0+agiHCkzHYaWR0
# Ih+DI66Q/JBzyrPj6Y+0G+KAOOoBwCg1afR0CelhS8t3JECXgh7bh32AI6ND+RZE
# b65tuW43US9eMnFk6+u+0eLF2RXJ5FruYrk4aZwKeUHPpstAp0B5gH+gAYNYi1PP
# z7wof4D1WgE6F1QPPJiMkDrQ8fhWT6sZwbO1KJvjFJ3hdWsyDClMHLWtJNwl7wQw
# IazByljwXWEriNF4rGLhkOuGiqQGbSmjFTGTcVC3OPFB3c1ISC1C8Z3V2Q8d0e4W
# gGjo1iyqg7+f356Y3Ya48+rYfq7YRXBceZao+iUN40TKzaCCVY/itF0eb7xtxe+f
# PbLiYWZcMyyoauljv17EWGWWRKefNLfrnP91RqVkrX6tG8mzb2rIO3hPY1EEFr8l
# djZyPbon8QXge5gg2pRYl6aXlLeDeDlrCuRe+AQnFmJmiBkW6RDZEn6WkreBkHMy
# IuPIEZRBefBE/5ujxwB8TLcHindMuwh20Oz9Ob8FwY2vmAGuQeiDa8tWi2Y/APdd
# 3MGhggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYDVQQGEwJV
# UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRy
# dXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0ExAhAK
# gO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsG
# CSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwMjAzMTYxMzEyWjAvBgkqhkiG
# 9w0BCQQxIgQgSRAftYfV829do17ZTmxtf2lYYTzQbbiohLRybTKC4R4wDQYJKoZI
# hvcNAQEBBQAEggIAvB1AnrX5qigXYm/fxogVC2wkZjJKZ4Y9Bu3SXkjeuWQmWt4T
# ExwNMA+aHx+wqZwDn2VHrFPHVuQ/Wbe6oedX20gAEFtgEqj+bwa04GgqCoCJqx4F
# PoKLWaa7aBR21tPdLrRv2F1LuexnKBm1Ti561S7KSCXP+o6R7zDmLSBCPUMORC3p
# cHgYwCz/PH+XoymPHAvg3ZF704Oai8UQV0sWJqbBTpI7aVFHHdJV7SZ8VJDvmhx8
# aUfIP8DZJygGjnSE1MSmmcj3JYN/anuz4105DkC45pK4NLMAvV8xkozKoUtxIlSM
# KpZepcGl6oc0JCyqOc2K6/LPwJL6tL5zgVNHDzJo2LtlRjCoDYZavpRTfapNrBbd
# mcOZ7H2y3/TKxdZB9Y9KfQptByoHfz697EA6EX5IEWbKFJQ1ZRpl/74onatoxWhs
# ip2XFiMo9KQBZVY85M2ksl+7tVynXkUrrcgAhTOD+ARLDitBMwNB+E1mjLN8vCAz
# vgVK6VsaP6qm8pfrXa77q8IkQMh1GLVQs5kce7nwNvlZixGeKftNHgX7W/RCn13z
# uwOtFIHAaYEV5/pabI8+E10MXHOhF2FEQD6+kgVuOcAe+aYrZ7aNzgjZ8W7HxdNj
# rAH1pKSJKmKxiapZI2dm7BcJ/giOjZ9+PxIpRU9wg+BoxkryEHiXCa7Q8ps=
# SIG # End signature block