Api/FlyCommon.ps1

<#
.SYNOPSIS
 
Check if the project name available
 
.DESCRIPTION
 
Check if the project name available
 
.PARAMETER ProjectName
The name of the project which you want to check
 
.OUTPUTS
 
Boolean, True if the project name is available
#>

function Resolve-FlyProjectName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${ProjectName}
    )

    Process {
        'Calling method: Resolve-FlyProjectName' | Write-Debug
        $notExist = Invoke-FlyCheckProjectExist -Name $ProjectName
        if (!$notExist) {
            throw ('The project "{0}" already exists. Configure a unique name for the project.' -f $ProjectName)
        }
    }
}

<#
.SYNOPSIS
 
Get the connection by name
 
.DESCRIPTION
 
Get the connection by name
 
.PARAMETER ConnectionName
The name of the connection
 
.OUTPUTS
 
ConnectionSummaryModel, the object model of the connection
#>

function Get-FlyConnectionByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${ConnectionName}
    )

    Process {
        'Calling method: Get-FlyConnectionByName' | Write-Debug
        $top = 200;
        $skip = 0;
        while ($true) {
            $connections = Get-FlyConnections -Search $ConnectionName -Top $top -Skip $skip
            $result = $connections.data | Where-Object { $_.Name -eq $ConnectionName } | Select-Object -First 1
            if ($null -ne $result) {
                return $result
            }
            if (!$connections.nextLink) {
                throw ('Failed to retrieve the connection: {0}. Log in to Fly to confirm the connection name.' -f $ConnectionName)
            }
            $skip = $skip + $top
        }
    }
}

<#
.SYNOPSIS
 
Get the policy by name
 
.DESCRIPTION
 
Get the policy by name
 
.PARAMETER PolicyName
The name of the policy
 
.OUTPUTS
 
PolicySummaryModel, the object model of the policy
#>

function Get-FlyPolicyByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${PolicyName},
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${PlatformType}
    )

    Process {
        'Calling method: Get-FlyPolicyByName' | Write-Debug
        $top = 200;
        $skip = 0;
        while ($true) {
            $policies = Get-FlyPolicies -MigrationModuleType $PlatformType  -Search $PolicyName -Top $top -Skip $skip
            $result = $policies.data | Where-Object { $_.Name -eq $PolicyName } | Select-Object -First 1
            if ($null -ne $result) {
                return $result
            }
            if (!$policies.nextLink) {
                throw ('Failed to retrieve the migration policy: {0}. Log in to Fly to confirm the policy name.' -f $PolicyName)
            }
            $skip = $skip + $top
        }
    }
}

<#
.SYNOPSIS
 
Get the tag by name
 
.DESCRIPTION
 
Get the tag by name
 
.PARAMETER TagName
The name of the tag
 
.OUTPUTS
 
TagSummaryModel, the object model of the tag
#>

function Get-FlyTagByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${TagName}
    )

    Process {
        'Calling method: Get-FlyTagByName' | Write-Debug
        $top = 200;
        $skip = 0;
        while ($true) {
            $tags = Get-FlyTags -Search $TagName -Top $top -Skip $skip
            $result = $tags.data | Where-Object { $_.Name -eq $TagName } | Select-Object -First 1
            if ($null -ne $result) {
                return $result
            }
            if (!$tags.nextLink) {
                throw ('Failed to retrieve the tag: {0}. Log in to Fly to confirm the tag name.' -f $TagName)
            }
            $skip = $skip + $top
        }
    }
}


<#
.SYNOPSIS
 
Get the project by name
 
.DESCRIPTION
 
Get the project by name
 
.PARAMETER ProjectName
The name of the project
 
.PARAMETER PlatformType
The platform of the project
 
.OUTPUTS
 
ProjectSummaryModel, the object model of the project
#>

function Get-FlyProjectByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${ProjectName},
        [Parameter(Mandatory = $false)]
        [String]
        [ValidateSet('Exchange', 'Teams', 'SharePoint', 'M365Group', 'TeamChat', 'OneDrive')]
        ${PlatformType}
    )

    Process {
        'Calling method: Get-FlyProjectByName' | Write-Debug
        $top = 200;
        $skip = 0;
        while ($true) {
            $projects = Get-FlyProjects -Search $ProjectName -Top $top -Skip $skip
            $result = $projects.data | Where-Object { $_.Name -eq $ProjectName } | Select-Object -First 1
            if ($null -ne $result) {
                if ($PlatformType -and ($PlatformType -ne [PlatformType].GetEnumName($result.sourcePlatform))) {
                    throw ('The type of the project "{0}" is not {1}.' -f $ProjectName, $PlatformType)
                }
                return $result
            }
            if (!$projects.nextLink) {
                throw ('Failed to retrieve the project: {0}. Log in to Fly to confirm the project name.' -f $ProjectName)
            }
            $skip = $skip + $top
        }
    }
}

<#
.SYNOPSIS
 
Get all project mappings by projectId
 
.DESCRIPTION
 
Get all project mappings by projectId
 
.PARAMETER ProjectName
The GUID of the project
 
.OUTPUTS
 
A list of the ProjectMappingSummaryModel
#>

function Get-FlyAllProjectMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId}
    )

    Process {
        'Calling method: Get-FlyAllProjectMappings' | Write-Debug
        $top = 2000;
        $skip = 0;
        $result = New-Object System.Collections.ArrayList;
        while ($true) {
            $mappings = Get-FlyProjectMappings -ProjectId $ProjectId -Top $top -Skip $skip
            if ($null -ne $mappings.data) {
                $result.AddRange($mappings.data)
            }
            if (!$mappings.nextLink) {
                break;
            }
            $skip = $skip + $top
        }
        return $result
    }
}

function Get-FlySharePointMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlySharePointMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source URL')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination URL')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyExchangeMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlyExchangeMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyTeamsMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlyTeamsMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source Team email address')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination Team email address')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyM365GroupMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlyM365GroupMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source Group email address')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination Group email address')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyTeamChatMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlyTeamChatMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source user')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination user')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyOneDriveMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlyOneDriveMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source user')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination user')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyMappingSettingByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $false)]
        [String]
        ${MappingName},
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${Type},
        [Parameter(Mandatory = $false)]
        [PSCustomObject[]]
        ${MigrationModuleTypes},
        [Parameter(Mandatory = $false)]
        [Boolean]
        ${NeedValidation} = $true
    )

    Process {
        'Calling method: Get-FlyMappingSettingByName' | Write-Debug
        if ($null -ne $MappingName -and $MappingName -ne '' -and $NeedValidation) {
            $top = 200;
            $skip = 0;
            while ($true) {
                $mappings = Get-FlyMappingSummaries -Type $Type -MigrationModuleTypes $MigrationModuleTypes -Search $MappingName -Top $top -Skip $skip
                $result = $mappings.data | Where-Object { $_.Name -eq $MappingName } | Select-Object -First 1
                if ($null -ne $result) {
                    return $result.Id
                }
                if (!$mappings.nextLink) {
                    throw ('Failed to retrieve the mapping name: {0}. Log in to Fly to confirm the {1} mapping name.' -f $MappingName, $Type)
                }
                $skip = $skip + $top
            }
        }
        else {
            if ($null -ne $MappingName -and $MappingName -ne '') {
                Write-Host ('Skip verify {0} mapping name: {1}, it will not be saved since custom mapping is not selected.' -f $Type, $MappingName) -ForegroundColor Green
            }
            return [Guid]::Empty 
        }
    }
}


function Get-FlyMappingSettingById {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [Guid]
        ${MappingId},
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${Type},
        [Parameter(Mandatory = $false)]
        [PSCustomObject[]]
        ${MigrationModuleTypes}
    )

    Process {
        'Calling method: Get-FlyMappingSettingById' | Write-Debug
        if ($null -eq $MappingId -or $MappingId -eq [Guid]::Empty) {
            return ''
        }
        $top = 200;
        $skip = 0;
        while ($true) {
            $mappings = Get-FlyMappingSummaries -Type $Type -MigrationModuleTypes $MigrationModuleTypes -Top $top -Skip $skip
            $result = $mappings.data | Where-Object { $_.Id -eq $MappingId } | Select-Object -First 1
            if ($null -ne $result) {
                return $result.Name
            }
            if (!$mappings.nextLink) {
                throw ('Failed to retrieve the mapping name. Log in to Fly to confirm the {0} mapping id: {1}.' -f $Type, $MappingId)
            }
            $skip = $skip + $top
        }
    }
}

function Get-FlyEmailTemplateByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${TemplateName},
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${Type}
    )

    Process {
        'Calling method: Get-FlyEmailTemplateByName' | Write-Debug
        $top = 200;
        $skip = 0;
        while ($true) {
            $templates = Get-FlyEmailTemplateSummaries -Search $TemplateName -Top $top -Skip $skip
            $result = $templates.data | Where-Object { $_.Name -eq $TemplateName -and $_.EmailType -eq $Type } | Select-Object -First 1
            if ($null -ne $result) {
                return $result
            }
            if (!$templates.nextLink) {
                throw ('Failed to retrieve the email template: {0}. Log in to Fly to confirm the email template name.' -f $TemplateName)
            }
            $skip = $skip + $top
        }
    }
}

function Get-FlyEmailTemplateById {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [Guid]
        ${TemplateId}
    )

    Process {
        'Calling method: Get-FlyEmailTemplateById' | Write-Debug
        if ($null -eq $TemplateId -or $TemplateId -eq [Guid]::Empty) {
            return '';
        }
        $top = 200;
        $skip = 0;
        while ($true) {
            $templates = Get-FlyEmailTemplateSummaries -Top $top -Skip $skip
            $result = $templates.data | Where-Object { $_.Id -eq $TemplateId } | Select-Object -First 1
            if ($null -ne $result) {
                return $result.Name
            }
            if (!$templates.nextLink) {
                throw ('Failed to retrieve the email template name. Log in to Fly to confirm the email template id: {0}.' -f $TemplateId)
            }
            $skip = $skip + $top
        }
    }
}

function Get-FlyFilterPolicyByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${FilterPolicyName},
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${MigrationModuleType}
    )

    Process {
        'Calling method: Get-FlyFilterPolicyByName' | Write-Debug
        $top = 200;
        $skip = 0;
        while ($true) {
            $filterPolicies = Get-FlyFilterSummaries -MigrationModuleType $MigrationModuleType -Search $FilterPolicyName -Top $top -Skip $skip
            $result = $filterPolicies.data | Where-Object { $_.Name -eq $FilterPolicyName } | Select-Object -First 1
            if ($null -ne $result) {
                return $result
            }
            if (!$filterPolicies.nextLink) {
                throw ('Failed to retrieve the filter policy: {0}. Log in to Fly to confirm the filter policy name.' -f $FilterPolicyName)
            }
            $skip = $skip + $top
        }
    }
}

function Get-FlyFilterPolicyById {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [Guid]
        ${PolicyId},
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${MigrationModuleType}
    )

    Process {
        'Calling method: Get-FlyFilterPolicyById' | Write-Debug
        if ($null -eq $PolicyId -or $PolicyId -eq [Guid]::Empty) {
            return '';
        }
        $top = 200;
        $skip = 0;
        while ($true) {
            $filterPolicies = Get-FlyFilterSummaries -MigrationModuleType $MigrationModuleType -Top $top -Skip $skip
            $result = $filterPolicies.data | Where-Object { $_.Id -eq $PolicyId } | Select-Object -First 1
            if ($null -ne $result) {
                return $result.Name
            }
            if (!$filterPolicies.nextLink) {
                throw ('Failed to retrieve the filter policy name. Log in to Fly to confirm the filter policy id: {0}.' -f $PolicyId)
            }
            $skip = $skip + $top
        }
    }
}

function Initialize-FlyNotificationCreationModel {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [Notification] $NotificationSettings,
        [Parameter(Mandatory = $false)]
        [PSCustomObject]$MigrationModuleType
    )

    Process {
        'Calling method: Initialize-FlyNotificationCreationModel' | Write-Debug
        if ( $null -eq $NotificationSettings ) {
            return [PSCustomObject]@{
                "sendProjectMigration"  = $false
                "sendIndividualMapping" = $false
            }
        }
        else {

            $individualEmailTemplateId = if ($NotificationSettings.SendIndividualMapping) {
                (Get-FlyEmailTemplateByName -TemplateName $NotificationSettings.IndividualLevelEmailTemplateName -Type ([EmailTemplateType]::MappingNotification)).Id
            }
            else {
                [Guid]::Empty
            }
            $projectMigrationEmailTemplateId = if ($NotificationSettings.SendProjectMigration) {
                (Get-FlyEmailTemplateByName -TemplateName $NotificationSettings.ProjectLevelEmailTemplateName  -Type ([EmailTemplateType]::ProjectNotification)).Id
            }
            else {
                [Guid]::Empty
            }
            $startTime = if ($NotificationSettings.SendProjectMigration) {
                Convert-DateTimeToTicks -DateTime $NotificationSettings.StartTime
            }
            else {
                0
            }
            $endTime = if ($NotificationSettings.SendProjectMigration -and $NotificationSettings.IsEnableEndTime) {
                Convert-DateTimeToTicks -DateTime $NotificationSettings.EndTime
            }
            else {
                0
            }
            $projectLevelEmailRecipients = $null
            if ($NotificationSettings.SendProjectMigration -and $null -ne $NotificationSettings.ProjectLevelEmailRecipients) { $projectLevelEmailRecipients = @($NotificationSettings.ProjectLevelEmailRecipients) }
            $additionalRecipients = $null
            if ($NotificationSettings.SendIndividualMapping -and $null -ne $NotificationSettings.AdditionalRecipients) { $additionalRecipients = @($NotificationSettings.AdditionalRecipients) }
            $notificationModel = [PSCustomObject]@{
                "sendProjectMigration"            = $NotificationSettings.SendProjectMigration
                "frequency"                       = if ($NotificationSettings.SendProjectMigration) { $NotificationSettings.FrequencyDays }else { 1 }
                "startTime"                       = $startTime
                "isEnableEndTime"                 = if ($NotificationSettings.SendProjectMigration) { $NotificationSettings.IsEnableEndTime } else { $false }
                "endTime"                         = $endTime
                "migrationEmailRecipients"        = $projectLevelEmailRecipients
                "projectMigrationEmailTemplateId" = $projectMigrationEmailTemplateId 
                "sendIndividualMapping"           = $NotificationSettings.SendIndividualMapping
                "mappingCondition"                = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.IndividualMappingLevel } else { [InformMappingCondition]::OnlyFailedMappings }
                "sendToSourceEmail"               = if ($NotificationSettings.SendIndividualMapping -and $MigrationModuleType -eq 'Exchange') { $NotificationSettings.SendToSourceEmail }else { $false }
                "sendToDestinationEmail"          = if ($NotificationSettings.SendIndividualMapping -and $MigrationModuleType -eq 'Exchange') { $NotificationSettings.SendToDestinationEmail } else { $false }  
                "sendToMe"                        = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.SendToPersonRunMigration } else { $false }
                "sendToAdditionalRecipients"      = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.SendToAdditionalRecipients } else { $false }  
                "additionalRecipients"            = $additionalRecipients
                "emailTemplateId"                 = $individualEmailTemplateId
            }   
            return $notificationModel    
        }
    }
}


function Resolve-FlyNotificationCreationModel {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [PSCustomObject]$NotificationSettings,
        [Parameter(Mandatory = $false)]
        [PSCustomObject]$MigrationModuleType
    )

    Process {
        'Calling method: Resolve-FlyNotificationCreationModel' | Write-Debug
        if ( $null -eq $NotificationSettings ) {
            throw 'Notification object should not be null.'
        }
        $notificationProperties = ("sendProjectMigration", "frequency", "startTime", "isEnableEndTime", "endTime", "migrationEmailRecipients", "projectMigrationEmailTemplateId", "sendIndividualMapping", "mappingCondition", "sendToSourceEmail", "sendToDestinationEmail", "sendToMe", "sendToAdditionalRecipients", "additionalRecipients", "emailTemplateId")
        if ($MigrationModuleType -ne 'Exchange') {
            $notificationProperties = $notificationProperties | Where-Object { $_ -ne "sendToSourceEmail" -and $_ -ne "sendToDestinationEmail" }
        }
        Confirm-FlyObjectPropertyExistence -Object $NotificationSettings -Properties $notificationProperties
                
        $individualEmailTemplateId = if ($NotificationSettings.SendIndividualMapping) {
            (Get-FlyEmailTemplateByName -TemplateName $NotificationSettings.EmailTemplateId -Type ([EmailTemplateType]::MappingNotification)).Id
        }
        else {
            [Guid]::Empty
        }
        $projectMigrationEmailTemplateId = if ($NotificationSettings.SendProjectMigration) {
            (Get-FlyEmailTemplateByName -TemplateName $NotificationSettings.ProjectMigrationEmailTemplateId  -Type ([EmailTemplateType]::ProjectNotification)).Id
        }
        else {
            [Guid]::Empty
        }
        $startTime = if ($NotificationSettings.SendProjectMigration -and (Confirm-FlyDateTimeProperty -DateTimeValue $NotificationSettings.StartTime -PropertyName 'StartTime' -Format 'MM/dd/yyyy HH:mm')) {
            Convert-DateTimeToTicks -DateTime $NotificationSettings.StartTime
        }
        else {
            0
        }
        $endTime = if ($NotificationSettings.SendProjectMigration -and (Confirm-FlyDateTimeProperty -DateTimeValue $NotificationSettings.EndTime -PropertyName 'EndTime' -Format 'MM/dd/yyyy HH:mm')) {
            Convert-DateTimeToTicks -DateTime $NotificationSettings.EndTime
        }
        else {
            0
        }

        $projectLevelEmailRecipients = $null
        if ($NotificationSettings.SendProjectMigration -and $null -ne $NotificationSettings.MigrationEmailRecipients) { $projectLevelEmailRecipients = @($NotificationSettings.MigrationEmailRecipients) }
        $additionalRecipients = $null
        if ($NotificationSettings.SendIndividualMapping -and $null -ne $NotificationSettings.AdditionalRecipients) { $additionalRecipients = @($NotificationSettings.AdditionalRecipients) }

        $notificationModel = [PSCustomObject]@{
            "sendProjectMigration"            = $NotificationSettings.SendProjectMigration
            "frequency"                       = if ($NotificationSettings.SendProjectMigration) { $NotificationSettings.Frequency } else { 1 }
            "startTime"                       = $startTime
            "isEnableEndTime"                 = if ($NotificationSettings.SendProjectMigration) { $NotificationSettings.IsEnableEndTime } else { $false }
            "endTime"                         = $endTime
            "migrationEmailRecipients"        = $projectLevelEmailRecipients
            "projectMigrationEmailTemplateId" = $projectMigrationEmailTemplateId 
            "sendIndividualMapping"           = $NotificationSettings.SendIndividualMapping
            "mappingCondition"                = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.MappingCondition } else { [InformMappingCondition]::OnlyFailedMappings }
            "sendToSourceEmail"               = if ($NotificationSettings.SendIndividualMapping -and $MigrationModuleType -eq 'Exchange') { $NotificationSettings.SendToSourceEmail }else { $false }
            "sendToDestinationEmail"          = if ($NotificationSettings.SendIndividualMapping -and $MigrationModuleType -eq 'Exchange') { $NotificationSettings.SendToDestinationEmail } else { $false }  
            "sendToMe"                        = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.SendToMe } else { $false }
            "sendToAdditionalRecipients"      = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.SendToAdditionalRecipients } else { $false }  
            "additionalRecipients"            = $additionalRecipients
            "emailTemplateId"                 = $individualEmailTemplateId
        }   
        return $notificationModel    
    } 
}



function Initialize-FlyUserMappingRules {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [UserMappingRuleProperty[]] $UserMappingRuleSettings,
        [Parameter(Mandatory = $false)]
        [PSCustomObject]$MigrationModuleType
    )

    Process {
        'Calling method: Initialize-FlyUserMappingRules' | Write-Debug
        $all = [UserMappingRuleProperty]::UPN, [UserMappingRuleProperty]::DisplayName, [UserMappingRuleProperty]::Email

        if ($null -eq $UserMappingRuleSettings -or $UserMappingRuleSettings.Count -eq 0 ) {
            return @(
                [PSCustomObject]@{ enabled = $false; sequence = 0; property = $all[0] }
                [PSCustomObject]@{ enabled = $false; sequence = 1; property = $all[1] }
                [PSCustomObject]@{ enabled = $false; sequence = 2; property = $all[2] }
            )
        }
        elseif ($UserMappingRuleSettings.Count -gt 3) {
            throw ('UserMappingRules cannot contain more than 3 rules.')
        }
        elseif ($MigrationModuleType -eq 'TeamChat' -and $UserMappingRuleSettings -contains [UserMappingRuleProperty]::Email) {
            throw ('TeamChat UserMappingRules are not supported Email property.')  
        }
        else {
            $orderedUnique = $UserMappingRuleSettings | Select-Object -Unique
            if ( $orderedUnique.Count -ne $UserMappingRuleSettings.Count ) {
                throw ('UserMappingRules cannot contain duplicate rules.')
            }
            $result = @()
            $seq = 0
            foreach ($name in $UserMappingRuleSettings) {
                $result += [PSCustomObject]@{
                    enabled  = $true
                    sequence = $seq
                    property = $name
                }
                $seq++
            }
            foreach ($p in $all) {
                if (-not ($UserMappingRuleSettings -contains $p)) {
                    $result += [PSCustomObject]@{
                        enabled  = $false
                        sequence = $seq
                        property = $p
                    }
                    $seq++
                }
            } 
            return $result    
        }
    }
}

function Resolve-FlyUserMappingRules {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [PSCustomObject] $UserMappingRuleSettings,
        [Parameter(Mandatory = $false)]
        [PSCustomObject]$MigrationModuleType   
    )

    Process {
        'Calling method: Resolve-FlyUserMappingRules' | Write-Debug 
        if ($null -eq $UserMappingRuleSettings -or -not ($UserMappingRuleSettings -is [System.Collections.IEnumerable] -and $UserMappingRuleSettings -isnot [string])) {
            throw ('UserMappingRules must be an array of objects.')
        }
        elseif ($MigrationModuleType -eq 'TeamChat' -and $UserMappingRuleSettings.Count -ne 2) {
            throw ('TeamChat UserMappingRules must contain exactly 2 rules.')
        }
        elseif ($MigrationModuleType -ne 'TeamChat' -and $UserMappingRuleSettings.Count -ne 3) {
            throw ('UserMappingRules must contain exactly 3 rules.')
        }
        $required = 'enabled', 'sequence', 'property'
        $sequences = @()
        $properties = @()
        foreach ($item in $UserMappingRuleSettings) {
            Confirm-FlyObjectPropertyExistence -Object $item -Properties $required
            if ($item.enabled -isnot [bool]) {
                throw 'UserMappingRules: enabled must be true or false.'
            }
            try { $seq = [int]$item.sequence } catch { throw 'UserMappingRules: sequence must be 0 or 1 or 2.' }
            try { $prop = [int]$item.property } catch { throw 'UserMappingRules: property must be 0 or 1 or 2.' }
            $sequences += $seq
            $properties += $prop
        }
        if ($MigrationModuleType -eq 'TeamChat') {
            if ((@(0, 1) -join ',') -ne (($sequences | Sort-Object) -join ',')) {
                throw 'UserMappingRules: sequence must be unique values 0, 1.'
            }
            if ((@(0, 1) -join ',') -ne (($properties | Sort-Object) -join ',')) {
                throw 'UserMappingRules: property must be unique values 0, 1.'
            }
        }
        else {
            if ((@(0, 1, 2) -join ',') -ne (($sequences | Sort-Object) -join ',')) {
                throw 'UserMappingRules: sequence must be unique values 0, 1, 2.'
            }
            if ((@(0, 1, 2) -join ',') -ne (($properties | Sort-Object) -join ',')) {
                throw 'UserMappingRules: property must be unique values 0, 1, 2.'
            }
        }
        $sorted = $UserMappingRuleSettings | Sort-Object { [int]$_.sequence }
        $seenDisabled = $false
        foreach ($r in $sorted) {
            if (-not $r.enabled) { $seenDisabled = $true }
            elseif ($seenDisabled -and $r.enabled) {
                throw 'UserMappingRules: sequence number of enabled property must be less than sequence number of disabled property.'
            }
        }
        $disabledProps = $sorted | Where-Object { -not $_.enabled } | ForEach-Object { [int]$_.property }
        if ($disabledProps.Count -gt 1) {
            for ($i = 1; $i -lt $disabledProps.Count; $i++) {
                if ($disabledProps[$i] -lt $disabledProps[$i - 1]) {
                    throw ('For disabled property of UserMappingRules: sequence number of property: {0} must be less than sequence number of property: {1}.' -f $disabledProps[$i], $disabledProps[$i - 1])
                }
            }
        }
        return $sorted
    }
}

function Confirm-FlyObjectPropertyExistence {
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${Object},
        [Parameter(Mandatory = $true)]
        [string[]]
        ${Properties}
    )
    Process {
        'Calling method: Confirm-FlyObjectPropertyExistence' | Write-Debug
        foreach ($propertyKey in $Properties) {
            $prop = $Object.PSObject.Properties[$propertyKey]
            if (-not $prop) {
                throw ('{0} property cannot be empty.' -f $propertyKey)
            }
        }
    }
}

function Confirm-FlyDateTimeProperty {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${DateTimeValue},
        [Parameter(Mandatory = $true)]
        [String]
        ${PropertyName},
        [Parameter(Mandatory = $false)]
        [String]
        ${Format}
    )
    Process {
        'Calling method: Confirm-FlyDateTimeProperty' | Write-Debug

        if ($null -eq $DateTimeValue) { return $false }
        if ($DateTimeValue -is [String] -and [String]::IsNullOrWhiteSpace($DateTimeValue)) { return $false }
        
        $result = $false  
        if ($DateTimeValue -is [DateTime]) {
            $result = $true
        }
        elseif ($DateTimeValue -is [String]) { 
            $parsedDt = [DateTime]::MinValue
            if ([DateTime]::TryParse($DateTimeValue, [ref]$parsedDt)) { 
                $result = $true 
            }
            else {
                throw "Property $PropertyName is not a valid DateTime: $DateTimeValue"
            }           
        }
        else {
            throw "Error: Property $PropertyName is not a valid DateTime: $DateTimeValue"
        }
        
        if ($null -ne $Format -and $Format -ne '') {
            $culture = [System.Globalization.CultureInfo]::InvariantCulture
            try {
                $dt = [DateTime]::ParseExact($DateTimeValue, $Format, $culture, [System.Globalization.DateTimeStyles]::None)
                if (($dt.ToString($Format, $culture) -ne $DateTimeValue)) {
                    throw "Property $PropertyName should be $Format format."
                }
            }
            catch { 
                throw "Error: Property $PropertyName should be $Format format."
            }
        }
        return $result
    }
}

function Convert-TicksToDateTime {
    [CmdletBinding()]
    [OutputType([String])]
    Param(
        [Parameter(Mandatory = $true)]
        [Int64]$Ticks,
        [Parameter(Mandatory = $true)]
        [Boolean]$ShowHourFormat
    )
    Process {
        'Calling method: Convert-TicksToDateTime' | Write-Debug
        if ($Ticks -lt 0 -or $Ticks -gt [DateTime]::MaxValue.Ticks) {
            throw "Ticks out of range: $Ticks"
        }
        if ($Ticks -eq 0 ) {
            return ''
        }
        $utc = [DateTime]::SpecifyKind([DateTime]::new($Ticks), [DateTimeKind]::Utc)
        if ($ShowHourFormat) {
            return $utc.ToLocalTime().ToString('MM/dd/yyyy HH:mm') 
        }
        else {
            return $utc.ToLocalTime().ToString('MM/dd/yyyy')
        } 
    }
}

function Convert-DateTimeToTicks {
    [CmdletBinding()]
    [OutputType([Int64])]
    Param(
        [Parameter(Mandatory = $true)]
        [DateTime]$DateTime
    )
    Process {
        'Calling method: Convert-DateTimeToTicks' | Write-Debug
        $utc = if ($DateTime.Kind -eq [DateTimeKind]::Utc) {
            $DateTime 
        } 
        else { 
            $DateTime.ToUniversalTime() 
        }
        return $utc.Ticks
    }
}

function Get-FlyPolicyFromJson {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${Path}
    )
    Process {
        'Calling method: Get-FlyPolicyFromJson' | Write-Debug
        $isJson = Confirm-FileExtension -Path $Path -AllowedExtensions 'json'
        if (-not $isJson) {
            throw 'The file is not a JSON file.'
        }
        $result = Get-Content -Path $Path -Raw -Encoding UTF8
        if ([String]::IsNullOrWhiteSpace($result) -or $result -eq '{}') { 
            throw 'JSON file is empty.'
        }
        $model = $result | ConvertFrom-Json
        if ($null -eq $model) { throw "Object parsed is null." }

        if ($model -isnot [PSCustomObject]) {
            throw "The content is not a JSON object."
        }
        return $model
    }
}

function Get-FlyMappingsFromCsv {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${Path}
    )
    Process {
        'Calling method: Get-FlyMappingsFromCsv' | Write-Debug
        $isCsv = Confirm-FileExtension -Path $Path -AllowedExtensions 'csv'
        if (-not $isCsv) {
            throw 'The file is not a CSV file.'
        }
        $result = Import-Csv -Path $Path
        if ($null -eq $result) {
            throw 'There is no data configured in the CSV file.'
        }
        return $result
    }
}

function Confirm-FileExtension {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${Path},
        [Parameter(Mandatory = $true)]
        [String[]]
        ${AllowedExtensions}
    )
    Process {
        'Calling method: Confirm-FileExtension' | Write-Debug
        $extension = ''
        if ($Path) {
            $split = (Split-Path -Path $Path -Leaf).Split(".")
            if ($split -and $split.Count -gt 1) {
                $extension = $split[1]
            }
        }
        if ($AllowedExtensions -contains $extension) {
            return $true
        }
        else {
            return $false
        }
    }
}

function Confirm-PSCustomObjectProperties {
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${Object},
        [Parameter(Mandatory = $true)]
        [string[]]
        ${Properties}
    )
    Process {
        'Calling method: Confirm-PSCustomObjectProperties' | Write-Debug
        foreach ($property in $Properties) {
            if (($null -eq $Object.$property) -or ('' -eq $Object.$property)) {
                throw ('{0} cannot be empty.' -f $property) 
            }
        }
    }
}

function Convert-FlyMappingStatus {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${Mapping}
    )
    Process {
        'Convert-FlyMappingStatus' | Write-Debug
        $progress = $Mapping.JobProgress
        $finalStatus = @([ProjectMappingItemStageStatus]::Successful, [ProjectMappingItemStageStatus]::Exceptioned, [ProjectMappingItemStageStatus]::Failed)
        if ($finalStatus -contains $Mapping.StageStatus) {
            $progress = 100
        }
        return [PSCustomObject]@{
            'Source'           = $Mapping.SourceIdentity
            'Destination'      = $Mapping.DestinationIdentity
            'Stage'            = Get-FlyMappingStage $Mapping.Stage
            'Stage status'     = Get-FlyMappingStageStatus $Mapping
            'Job progress (%)' = $progress
        }
    }
}

function Get-FlyMappingStage {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [ProjectMappingItemStage]
        ${Stage}
    )
    Process {
        'Calling method: Get-FlyMappingStage' | Write-Debug
        switch ($Stage) {
            ([ProjectMappingItemStage]::CreateMapping) { 'New mapping' }
            ([ProjectMappingItemStage]::RunVerification) { 'Mapping verification' }
            ([ProjectMappingItemStage]::RunAssessment) { 'Scan source data' }
            ([ProjectMappingItemStage]::RunDataMigration) { 'Migration' }
            ([ProjectMappingItemStage]::EmailForwarding) { 'Email forwarding' }
            ([ProjectMappingItemStage]::KeepX500EmailAddress) { 'Add X500 email address to destination mailboxes' }
            Default { [ProjectMappingItemStage].GetEnumName($Stage) }
        }
    }
}

function Get-FlyMappingStageStatus {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [PSCustomObject]
        ${Mapping}
    )
    Process {
        'Calling method: Get-FlyMappingStageStatus' | Write-Debug
        if ($Mapping.StageStatus -eq [ProjectMappingItemStageStatus]::Waiting -and $Mapping.ScheduleTime -gt 0) {
            return 'Scheduled'
        }
        elseif ($Mapping.StageStatus -eq 9) {
            return 'Finished'
        }
        else {
            switch ([ProjectMappingItemStageStatus]$Mapping.StageStatus) {
                ([ProjectMappingItemStageStatus]::NotStart) { 'Not started' }
                ([ProjectMappingItemStageStatus]::Waiting) { 'In queue' }
                ([ProjectMappingItemStageStatus]::Queued) { 'In queue with priority' }
                ([ProjectMappingItemStageStatus]::InProgress) { 'In progress' }
                ([ProjectMappingItemStageStatus]::Successful) { 'Finished' }
                ([ProjectMappingItemStageStatus]::Exceptioned) { 'Exceptions' }
                ([ProjectMappingItemStageStatus]::Failed) { 'Failed' }
                ([ProjectMappingItemStageStatus]::Stopped) { 'Stopped' }
                Default { [ProjectMappingItemStageStatus].GetEnumName($Mapping.StageStatus) }
            }
        }
        
    }
}

<#
.SYNOPSIS
 
Convert the string value of object level to integer
 
.DESCRIPTION
 
Convert the string value of object level to integer
 
.PARAMETER Level
The string value of object level
 
.OUTPUTS
 
The integer value of object level
#>

function Get-FlyDataType {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${Level}
    )
    Process {
        'Calling method: Get-FlyDataType' | Write-Debug
        switch ($Level) {
            'Site collection' { 600 }
            'Site' { 400 }
            'List' { 200 }
            'Folder' { 100 }
            'User mailbox' { 1001 }
            'Archive mailbox' { 1002 }
            'Distribution list' { 1007 }
            'Microsoft 365 Group mailbox' { 1003 }
            'Resource mailbox' { 1004 }
            'Shared mailbox' { 1005 }
            'Mail-enabled security group' { 1008 }
            'Microsoft 365 Group' { 1006 }
            Default { throw ('Invalid data type') }
        }
    }
}

<#
.SYNOPSIS
 
Convert the string value of method to SharePointMethodTypes, only support SharePoint object for now
 
.DESCRIPTION
 
Convert the string value of method to SharePointMethodTypes, only support SharePoint object for now
 
.PARAMETER Level
The string value of method
 
.OUTPUTS
 
SharePointMethodTypes, refer to SharePointMethodTypes.ps1
#>

function Get-FlySharePointMethod {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${Method}
    )
    Process {
        'Calling method: Get-FlySharePointMethod' | Write-Debug
        switch ($Method) {
            'Merge' { [SharePointMethodTypes]::Combine }
            'Attach' { [SharePointMethodTypes]::Attach }
            Default { throw ('Invalid method') }
        }
    }
}

<#
.SYNOPSIS
 
Convert the string value of type to TeamsChannelType
 
.DESCRIPTION
 
Convert the string value of type to TeamsChannelType
 
.PARAMETER Level
The string value of type
 
.OUTPUTS
 
TeamsChannelType, refer to TeamsChannelType.ps1
#>

function Get-FlyTeamsChannelType {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${Type}
    )
    Process {
        'Calling method: Get-FlyTeamsChannelType' | Write-Debug
        switch ($Type) {
            'Standard' { [TeamsChannelType]::Standard }
            'Private' { [TeamsChannelType]::Private }
            'Shared' { [TeamsChannelType]::Shared }
            Default { throw ('Invalid teams channel type') }
        }
    }
}
<#
.SYNOPSIS
 
Output the error details of ErrorRecord
 
.DESCRIPTION
 
Output the error details of ErrorRecord
 
.PARAMETER Level
The information of ErrorRecord
 
.OUTPUTS
 
#>

function ErrorDetail {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [System.Management.Automation.ErrorRecord]
        ${Error}
    )

    Process {
        'Calling method: ErrorDetail' | Write-Debug
        if ($Error.ErrorDetails.Message) {
            Write-Host $Error.ErrorDetails.Message -ForegroundColor Red
        }
        Write-Error $Error.Exception
    }
}

function Get-IdentityServiceToken {
    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Mandatory)]
        [string]$IdentityServiceUri,
        [Parameter(Mandatory)]
        [string]$Scope,
        [Parameter(Mandatory)]
        [string]$ClientId,
        [Parameter(Mandatory)]
        [Alias("Certificate", "Cert")]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$SigningCertificate
    )
    PROCESS {
        'Calling method: Get-IdentityServiceToken' | Write-Debug
        $encodedThumbprint = ConvertTo-Base64UrlEncodedString -Bytes $SigningCertificate.GetCertHash()
        $headerTable = [ordered]@{typ = "JWT"; alg = "RS256"; kid = $encodedThumbprint }
        $header = $headerTable | ConvertTo-Json -Compress | ConvertTo-Base64UrlEncodedString
        $now = Get-Date
        $currentEpochTime = Convert-DateTimeToEpoch -DateTime $now
        $notBefore = $currentEpochTime
        $futureEpochTime = Convert-DateTimeToEpoch -DateTime ($now.AddHours(1))
        $payloadTable = [ordered]@{sub = $ClientId; jti = ([System.Guid]::NewGuid()).ToString(); iss = $ClientId; aud = $IdentityServiceUri.TrimEnd('/') + "/connect/token"; nbf = $notBefore; exp = $futureEpochTime; iat = $currentEpochTime }
        $payload = $payloadTable | ConvertTo-Json -Compress | ConvertTo-Base64UrlEncodedString
        $jwtPlainText = "{0}.{1}" -f $header, $payload
        $jwtSig = New-JwtRsaSignature -JsonWebToken $jwtPlainText -SigningCertificate $SigningCertificate
        $ClientAssertion = "{0}.{1}" -f $jwtPlainText, $jwtSig
        $RequestUri = $IdentityServiceUri.TrimEnd('/') + "/connect/token"
        $Body = @{
            grant_type            = 'client_credentials'
            scope                 = $Scope
            client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
            client_assertion      = $ClientAssertion
        }
        $Response = Invoke-WebRequest -Uri $RequestUri -Method 'POST' -Body $Body -ErrorAction Stop -UseBasicParsing
        return (ConvertFrom-Json $Response).access_token
    }
}

function Get-IdentityServiceTokenByClientSecret {
    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Mandatory)]
        [string]$IdentityServiceUri,
        [Parameter(Mandatory)]
        [string]$Scope,
        [Parameter(Mandatory)]
        [string]$ClientId,
        [Parameter(Mandatory)]
        [string]$ClientSecret
    )
    PROCESS {
        'Calling method: Get-IdentityServiceTokenByClientSecret' | Write-Debug
        $RequestUri = $IdentityServiceUri.TrimEnd('/') + "/connect/token"
        $Body = @{
            grant_type    = 'client_credentials'
            scope         = $Scope
            client_id     = $ClientId
            client_secret = $ClientSecret
        }
        $Response = Invoke-WebRequest -Uri $RequestUri -Method 'POST' -Body $Body -ErrorAction Stop -UseBasicParsing
        return (ConvertFrom-Json $Response).access_token
    }
}

function New-JwtRsaSignature {
    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$SigningCertificate,
        [String]$JsonWebToken
    )
    PROCESS {
        'Calling method: New-JwtRsaSignature' | Write-Debug
        $rsa = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($SigningCertificate))
        if ($null -eq $rsa) {
            # Requiring the private key to be present
            throw "There's no private key in the supplied certificate." 
        }
        [byte[]]$message = [System.Text.Encoding]::UTF8.GetBytes($JsonWebToken)
        $sigBytes = $rsa.SignData($message, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1)
        return ConvertTo-Base64UrlEncodedString -Bytes $sigBytes
    }
}


function ConvertTo-Base64UrlEncodedString {
    [CmdletBinding()]
    [OutputType([string])]
    Param (
        [Parameter(Position = 0, ParameterSetName = "String", Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$InputString,

        [Parameter(Position = 1, ParameterSetName = "Byte Array", Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
        [byte[]]$Bytes
    )
    PROCESS {
        [string]$base64UrlEncodedString = ""

        if ($PSBoundParameters.ContainsKey("Bytes")) {
            $output = [Convert]::ToBase64String($Bytes)
            $output = $output.Split('=')[0] # Remove any trailing '='s
            $output = $output.Replace('+', '-') # 62nd char of encoding
            $output = $output.Replace('/', '_') # 63rd char of encoding
            $base64UrlEncodedString = $output

        }
        else {
            $encoder = [System.Text.UTF8Encoding]::new()
            [byte[]]$inputBytes = $encoder.GetBytes($InputString)
            $base64String = [Convert]::ToBase64String($inputBytes)
            [string]$base64UrlEncodedString = ""
            $base64UrlEncodedString = $base64String.Split('=')[0] # Remove any trailing '='s
            $base64UrlEncodedString = $base64UrlEncodedString.Replace('+', '-'); # 62nd char of encoding
            $base64UrlEncodedString = $base64UrlEncodedString.Replace('/', '_'); # 63rd char of encoding
        }
        return $base64UrlEncodedString
    }
}

function Convert-DateTimeToEpoch {
    [CmdletBinding()]
    [OutputType([System.Int64])]
    Param(
        [Parameter(Mandatory)]
        [DateTime]$DateTime
    )
    PROCESS {
        'Calling method: Convert-DateTimeToEpoch' | Write-Debug
        $dtut = $DateTime.ToUniversalTime()
        [TimeSpan]$ts = New-TimeSpan -Start  (Get-Date "01/01/1970") -End $dtut
        [Int64]$secondsSinceEpoch = [Math]::Floor($ts.TotalSeconds)
        return $secondsSinceEpoch
    }
}

function Convert-JWTtoken {
    [CmdletBinding()]
    [OutputType([System.Int64])]
    Param(
        [Parameter(Mandatory = $true)]
        [String]$token
    )

    PROCESS {
        'Calling method: Convert-JWTtoken' | Write-Debug
        #Validate as per https://tools.ietf.org/html/rfc7519
        #Access and ID tokens are fine, Refresh tokens will not work
        if (!$token.Contains(".") -or !$token.StartsWith("eyJ")) { Write-Error "Invalid token" -ErrorAction Stop }
 
        #Header
        $tokenheader = $token.Split(".")[0].Replace('-', '+').Replace('_', '/')
        #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0
        while ($tokenheader.Length % 4) { Write-Verbose "Invalid length for a Base-64 char array or string, adding ="; $tokenheader += "=" }

        #Payload
        $tokenPayload = $token.Split(".")[1].Replace('-', '+').Replace('_', '/')
        #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0
        while ($tokenPayload.Length % 4) { Write-Verbose "Invalid length for a Base-64 char array or string, adding ="; $tokenPayload += "=" }

        #Convert to Byte array
        $tokenByteArray = [System.Convert]::FromBase64String($tokenPayload)
        #Convert to string array
        $tokenArray = [System.Text.Encoding]::UTF8.GetString($tokenByteArray)

        #Convert from JSON to PSObject
        $result = $tokenArray | ConvertFrom-Json
        return $result
    }
}