HP.Repo.psm1

#
# Copyright 2018-2024 HP Development Company, L.P.
# All Rights Reserved.
#
# NOTICE: All information contained herein is, and remains the property of HP Development Company, L.P.
#
# The intellectual and technical concepts contained herein are proprietary to HP Development Company, L.P
# and may be covered by U.S. and Foreign Patents, patents in process, and are protected by
# trade secret or copyright law. Dissemination of this information or reproduction of this material
# is strictly forbidden unless prior written permission is obtained from HP Development Company, L.P.

using namespace HP.CMSLHelper

Set-StrictMode -Version 3.0
#requires -Modules "HP.Private","HP.Softpaq","HP.Sinks"

# CMSL is normally installed in C:\Program Files\WindowsPowerShell\Modules
# but if installed via PSGallery and via PS7, it is installed in a different location
if (Test-Path "$PSScriptRoot\..\HP.Private\HP.CMSLHelper.dll") {
  Add-Type -Path "$PSScriptRoot\..\HP.Private\HP.CMSLHelper.dll"
}
else{
  Add-Type -Path "$PSScriptRoot\..\..\HP.Private\1.8.0\HP.CMSLHelper.dll"
}

enum ErrorHandling
{
  Fail = 0
  LogAndContinue = 1
}

$REPOFILE = ".repository/repository.json"
$LOGFILE = ".repository/activity.log"

# print a bare error
function err
{
  [CmdletBinding()]
  param(
    [string]$str,
    [boolean]$withLog = $true
  )

  [console]::ForegroundColor = 'red'
  [console]::Error.WriteLine($str)
  [console]::ResetColor()

  if ($withLog) { Write-LogError -Message $str -Component "HP.Repo" -File $LOGFILE }
}

# convert a date object to an 8601 string
function ISO8601DateString
{
  [CmdletBinding()]
  param(
    [datetime]$Date
  )
  $Date.ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff",[System.Globalization.CultureInfo]::InvariantCulture)
}

# get current user name
function GetUserName ()
{
  [CmdletBinding()]
  param()

  try {
    [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
  }
  catch {
    return $env:username
  }
}

# check if a file exists
function FileExists
{
  [CmdletBinding()]
  param(
    [string]$File
  )
  Test-Path $File -PathType Leaf
}

# load a json object
function LoadJson
{
  [CmdletBinding()]
  param(
    [string]$File
  )

  try {
    $PS7Mark = "PS7Mark"
    $rawData = (Get-Content -Raw -Path $File) -replace '("DateLastModified": ")([^"]+)(")',('$1' + $PS7Mark + '$2' + $PS7Mark + '$3')
    [SoftpaqRepositoryFile]$result = $rawData | ConvertFrom-Json
    $result.DateLastModified = $result.DateLastModified -replace $PS7Mark,""
    return $result
  }
  catch
  {
    err ("Could not parse '$File' $($_.Exception.Message)")
    return $Null
  }
}

# load a repository definition file
function LoadRepository
{
  [CmdletBinding()]
  param()

  Write-Verbose "loading $REPOFILE"
  $inRepo = FileExists -File $REPOFILE
  if (-not $inRepo) {
    throw [System.Management.Automation.ItemNotFoundException]"Directory '$(Get-Location)' is not a repository."
  }

  $repo = LoadJson -File $REPOFILE
  if (-not $repo -eq $null)
  {
    err ("Could not initialize the repository: $($_.Exception.Message)")
    return $false,$null
  }

  if (-not $repo.Filters) { $repo.Filters = @() }

  if (-not $repo.settings) {
    $repo.settings = New-Object SoftpaqRepositoryFile+Configuration
  }

  if (-not $repo.settings.OnRemoteFileNotFound) {
    $repo.settings.OnRemoteFileNotFound = [ErrorHandling]::Fail
  }

  if (-not $repo.settings.ExclusiveLockMaxRetries) {
    $repo.settings.ExclusiveLockMaxRetries = 10
  }

  if (-not $repo.settings.OfflineCacheMode) {
    $repo.settings.OfflineCacheMode = "Disable"
  }

  if (-not $repo.settings.RepositoryReport) {
    $repo.settings.RepositoryReport = "CSV"
  }

  foreach ($filter in $repo.Filters)
  {
    if (-not $filter.characteristic)
    {
      $filter.characteristic = "*"
    }
    if (-not $filter.preferLTSC)
    {
      $filter.preferLTSC = $false
    }
  }

  if (-not $repo.Notifications) {
    $repo.Notifications = New-Object SoftpaqRepositoryFile+NotificationConfiguration
    $repo.Notifications.port = 25
    $repo.Notifications.tls = $false
    $repo.Notifications.UserName = ""
    $repo.Notifications.Password = ""
    $repo.Notifications.from = "softpaq-repo-sync@$($env:userdnsdomain)"
    $repo.Notifications.fromname = "Softpaq Repository Notification"
  }

  Write-Verbose "load success"
  return $true,$repo
}

# This function downloads SoftPaq CVA, if SoftPaq exe already exists, checks signature of SoftPaq exe. If redownload required, SoftPaq exe will be downloaded.
# Note that CVAs are always downloaded since there is no reliable way to check their consistency.
function DownloadSoftpaq
{
  [CmdletBinding()]
  param(
    $DownloadSoftpaqCmd,
    [int]$MaxRetries = 10
  )

  $download_file = $true
  $EXEname = "sp" + $DownloadSoftpaqCmd.number + ".exe"
  $CVAname = "sp" + $DownloadSoftpaqCmd.number + ".cva"

  # download the SoftPaq CVA. Existing CVAs are always overwritten.
  Write-Verbose ("Downloading CVA $($DownloadSoftpaqCmd.number)")
  Log (" sp$($DownloadSoftpaqCmd.number).cva - Downloading CVA file.")
  Get-SoftpaqMetadataFile @DownloadSoftpaqCmd -MaxRetries $MaxRetries -Overwrite "Yes"
  Log (" sp$($DownloadSoftpaqCmd.number).cva - Done downloading CVA file.")

  # check if the SoftPaq exe already exists
  if (FileExists -File $EXEname) {
    Write-Verbose "Checking signature for existing file $EXEname"
    if (Get-HPPrivateCheckSignature -File $EXEname -CVAfile $CVAname -Verbose:$VerbosePreference -Progress:(-not $DownloadSoftpaqCmd.Quiet)) {

      # existing SoftPaq exe passes signature check. No need to redownload
      $download_file = $false

      if (-not $DownloadSoftpaqCmd.Quiet) {
        Write-Host -ForegroundColor Magenta "File $EXEname already exists and passes signature check. It will not be redownloaded."
      }

      Log (" sp$($DownloadSoftpaqCmd.number).exe - Already exists. Will not redownload.")
    }
    else {
      # existing SoftPaq exe failed signature check. Need to delete it and redownload
      Write-Verbose ("Need to redownload file '$EXEname'")
    }
  }
  else {
    Write-Verbose ("Need to download file '$EXEname'")
  }

  # download the SoftPaq exe if needed
  if ($download_file -eq $true) {
    try {
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Downloading EXE file.")
      
      # download the SoftPaq exe
      Get-Softpaq @DownloadSoftpaqCmd -MaxRetries $MaxRetries -Overwrite yes

      # check newly downloaded SoftPaq exe signature
      if (-not (Get-HPPrivateCheckSignature -File $EXEname -CVAfile $CVAname -Verbose:$VerbosePreference -Progress:(-not $DownloadSoftpaqCmd.Quiet))) {

        # delete SoftPaq CVA and EXE since the EXE failed signature check
        Remove-Item -Path $EXEname -Force -Verbose:$VerbosePreference
        Remove-Item -Path $CVAName -Force -Verbose:$VerbosePreference

        $msg = "File $EXEname failed integrity check and has been deleted, will retry download next sync"
        if (-not $DownloadSoftpaqCmd.Quiet) {
          Write-Host -ForegroundColor Magenta $msg
        }
        Write-LogWarning -Message $msg -Component "HP.Repo" -File $LOGFILE
      }
      else {
        Log (" sp$($DownloadSoftpaqCmd.number).exe - Done downloading EXE file.")
      }
    }
    catch {
      Write-Host -ForegroundColor Magenta "File sp$($DownloadSoftpaqCmd.number).exe has invalid or missing signature and will be deleted."
      Log (" sp$($DownloadSoftpaqCmd.number).exe has invalid or missing signature and will be deleted.")
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Redownloading EXE file.")
      Get-Softpaq @DownloadSoftpaqCmd -maxRetries $maxRetries
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Done downloading EXE file.")
    }
  }
}

# write a repository definition file
function WriteRepositoryFile
{
  [CmdletBinding()]
  param($obj)

  $now = Get-Date
  $obj.DateLastModified = ISO8601DateString -Date $now
  $obj.ModifiedBy = GetUserName
  Write-Verbose "Writing repository file to $REPOFILE"
  $obj | ConvertTo-Json | Out-File -Force $REPOFILE
}

# check if a filter exists in a repo object
function FilterExists
{
  [CmdletBinding()]
  param($repo,$f)

  $c = getFilters $repo $f
  return ($null -ne $c)
}

# get a list of filters in a repo, matching exact parameters
function getFilters
{
  [CmdletBinding()]
  param($repo,$f)

  if ($repo.Filters.Count -eq 0) { return $null }
  $repo.Filters | Where-Object {
    $_.platform -eq $f.platform -and
    $_.OperatingSystem -eq $f.OperatingSystem -and
    $_.Category -eq $f.Category -and
    $_.ReleaseType -eq $f.ReleaseType -and
    $_.characteristic -eq $f.characteristic -and
    $_.preferLTSC -eq $f.preferLTSC
  }
}

# get a list of filters in a repo, considering empty parameters as wildcards
function GetFiltersWild
{
  [CmdletBinding()]
  param($repo,$f)

  if ($repo.Filters.Count -eq 0) { return $null }
  $repo.Filters | Where-Object {
    $_.platform -eq $f.platform -and
    (
      $_.OperatingSystem -eq $f.OperatingSystem -or
      $f.OperatingSystem -eq "*" -or
      ($f.OperatingSystem -eq "win10:*" -and $_.OperatingSystem.StartsWith("win10")) -or
      ($f.OperatingSystem -eq "win11:*" -and $_.OperatingSystem.StartsWith("win11"))
    ) -and
    ($_.Category -eq $f.Category -or $f.Category -eq "*") -and
    ($_.ReleaseType -eq $f.ReleaseType -or $f.ReleaseType -eq "*") -and
    ($_.characteristic -eq $f.characteristic -or $f.characteristic -eq "*") -and
    ($_.preferLTSC -eq $f.preferLTSC -or $null -eq $f.preferLTSC)
  }
}

# write a log entry to the .repository/activity.log
function Log
{
  [CmdletBinding()]
  param([string[]]$entryText)

  foreach ($line in $entryText)
  {
    if (-not $line) {
      $line = " "
    }
    Write-LogInfo -Message $line -Component "HP.Repo" -File $LOGFILE
  }

}

# touch a file (change its date if exists, or create it if it doesn't.
function TouchFile
{
  [CmdletBinding()]
  param([string]$File)

  if (Test-Path $File) { (Get-ChildItem $File).LastWriteTime = Get-Date }
  else { Write-Output $null > $File }
}


# remove all marks from the repository
function FlushMarks
{
  [CmdletBinding()]
  param()

  Write-Verbose "Removing all marks"
  Remove-Item ".repository\mark\*" -Include "*.mark"
}


# send a notification email
function Send
{
  [CmdletBinding()]
  param(
    $subject,
    $body,
    $html = $true
  )

  $n = Get-RepositoryNotificationConfiguration
  if ((-not $n) -or (-not $n.server)) {
    Write-Verbose ("Notifications are not configured")
    return
  }

  try {
    if ((-not $n.addresses) -or (-not $n.addresses.Count))
    {
      Write-Verbose ("Notifications have no recipients defined")
      return
    }
    Log ("Sending a notification email")

    $params = @{}
    $params.To = $n.addresses
    $params.SmtpServer = $n.server
    $params.port = $n.port
    $params.UseSsl = $n.tls
    $params.from = "$($n.fromname) <$($n.from)>"
    $params.Subject = $subject
    $params.Body = $body
    $params.BodyAsHtml = $html

    Write-Verbose ("server: $($params.SmtpServer)")
    Write-Verbose ("port: $($params.Port)")

    if ([string]::IsNullOrEmpty($n.UserName) -eq $false)
    {
      try {
        [SecureString]$read = $n.Password | ConvertTo-SecureString
        $params.Credential = New-Object System.Management.Automation.PSCredential ($n.UserName,$read)
        if (-not $params.Credential) {
          Log ("Could not build credential object from username and password")
          return;
        }
      }
      catch {
        err ("Failed to build credential object from username and password: $($_.Exception.Message)")
        return
      }
    }
    Send-MailMessage @params -ErrorAction Stop
  }
  catch {
    err ("Could not send email: $($_.Exception.Message)")
    return
  }
  Write-Verbose ("Send complete.")
}

<#
.SYNOPSIS
    Initializes a repository in the current directory
 
.DESCRIPTION
  This command initializes a directory to be used as a repository. This command creates a .repository folder in the current directory,
  which contains the definition of the .repository and all its settings.
 
  In order to un-initalize a directory, simple remove the .repository folder.
 
  After initializing a repository, you must add at least one filter to define the content that this repository will receive.
 
  If the directory already contains a repository, this command will fail.
 
.EXAMPLE
    Initialize-Repository
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryConfiguration)
 
.LINK
  [Set-RepositoryConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryConfiguration)
#>

function Initialize-Repository
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Initialize-Repository")]
  param()

  if (FileExists -File $REPOFILE) {
    err "This directory is already initialized as a repository."
    return
  }
  $now = Get-Date
  $newRepositoryFile = New-Object SoftpaqRepositoryFile

  $newRepositoryFile.settings = New-Object SoftpaqRepositoryFile+Configuration
  $newRepositoryFile.settings.OnRemoteFileNotFound = [ErrorHandling]::Fail
  $newRepositoryFile.settings.ExclusiveLockMaxRetries = 10
  $newRepositoryFile.settings.OfflineCacheMode = "Disable"
  $newRepositoryFile.settings.RepositoryReport = "CSV"

  $newRepositoryFile.DateCreated = ISO8601DateString -Date $now
  $newRepositoryFile.CreatedBy = GetUserName

  try {
    New-Item -ItemType directory -Path .repository | Out-Null
    WriteRepositoryFile -obj $newRepositoryFile
    New-Item -ItemType directory -Path ".repository/mark" | Out-Null
  }
  catch {
    err ("Could not initialize the repository: $($_.Exception.Message)")
    return
  }
  Log "Repository initialized successfully."
}

<#
.SYNOPSIS
  Adds a filter per specified platform to the current repository
 
.DESCRIPTION
  This command adds a filter to a repository that was previously initialized by the Initialize-Repository command. A repository can contain one or more filters, and filtering will be the based on all the filters defined. Please note that "*" means "current" for the -Os parameter but means "all" for the -Category, -ReleaseType, -Characteristic parameters.
 
  .PARAMETER Platform
  Specifies the platform using its platform ID to include in this repository. A platform ID, a 4-digit hexadecimal number, can be obtained by executing the Get-HPDeviceProductID command. This parameter is mandatory.
 
.PARAMETER Os
  Specifies the operating system to be included in this repository. The value must be one of 'win10' or 'win11'. If not specified, the current operating system will be assumed, which may not be what is intended.
 
.PARAMETER OsVer
  Specifies the target OS Version (e.g. '1809', '1903', '1909', '2004', '2009', '21H1', '21H2', '22H2', '23H2', '24H2', etc). Starting from the '21H1' release, 'xxHx' format is expected. If not specified, the current operating system version will be assumed, which may not be what is intended.
 
.PARAMETER Category
  Specifies the SoftPaq category to be included in this repository. The value must be one (or more) of the following values:
  - Bios
  - Firmware
  - Driver
  - Software
  - OS
  - Manageability
  - Diagnostic
  - Utility
  - Driverpack
  - Dock
  - UWPPack
 
  If not specified, all categories will be included.
 
.PARAMETER ReleaseType
  Specifies a release type for this command to filter based on. The value must be one (or more) of the following values:
  - Critical
  - Recommended
  - Routine
 
  If not specified, all release types are included.
 
.PARAMETER Characteristic
  Specifies the characteristic to be included in this repository. The value must be one of the following values:
  - SSM
  - DPB
  - UWP
   
  If this parameter is not specified, all characteristics are included.
 
.PARAMETER PreferLTSC
  If specified and if the data file exists, this command uses the Long-Term Servicing Branch/Long-Term Servicing Channel (LTSB/LTSC) Reference file for the specified platform ID. If the data file does not exist, this command uses the regular Reference file for the specified platform.
 
.EXAMPLE
  Add-RepositoryFilter -Platform 1234 -Os win10 -OsVer 2009
 
.EXAMPLE
  Add-RepositoryFilter -Platform 1234 -Os win10 -OsVer "21H1"
 
.EXAMPLE
  Add-RepositoryFilter -Platform 1234 -Os win10 -OsVer "21H1" -PreferLTSC
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
 
.LINK
  [Get-HPDeviceProductID](https://developers.hp.com/hp-client-management/doc/Get-HPDeviceProductID)
#>

function Add-RepositoryFilter
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter")]
  param(
    [ValidatePattern("^[a-fA-F0-9]{4}$")]
    [Parameter(Position = 0,Mandatory = $true)]
    [string]$Platform,

    [ValidateSet("win7","win8","win8.1","win81","win10","win11","*")] # keep in sync with the SoftPaq module
    [Parameter(Position = 1)] $Os = "*", # counterintuitively, "*" for this Os parameter means "current"
    [string[]]

    [ValidateSet("1809","1903","1909","2004","2009","21H1","21H2","22H2", "23H2", "24H2")] # keep in sync with the SoftPaq module
    [Parameter(Position = 1)]
    [string]$OsVer,

    [ValidateSet("Bios","Firmware","Driver","Software","Os","Manageability","Diagnostic","Utility","Driverpack","Dock","UWPPack","*")] # keep in sync with the SoftPaq module
    [Parameter(Position = 2)]
    [string[]]$Category = "*",

    [ValidateSet("Critical","Recommended","Routine","*")] # keep in sync with the SoftPaq module
    [Parameter(Position = 3)]
    [string[]]$ReleaseType = "*",

    [ValidateSet("SSM","DPB","UWP","*")] # keep in sync with the SoftPaq module
    [Parameter(Position = 4)]
    [string[]]$Characteristic = "*",

    [Parameter(Position = 5, Mandatory = $false)]
    [switch]$PreferLTSC
  )

  $c = LoadRepository
  try {
    if ($c[0] -eq $false) { return }
    $repo = $c[1]

    $newFilter = New-Object SoftpaqRepositoryFile+SoftpaqRepositoryFilter
    $newFilter.platform = $Platform

    $newFilter.OperatingSystem = $Os
    if (-not $OsVer)
    {
      $OsVer = GetCurrentOSVer
    }
    if ($OsVer) { $OsVer = $OsVer.ToLower() }
    if ($Os -eq "win10") { $newFilter.OperatingSystem = "win10:$OsVer" }
    elseif ($Os -eq "win11") { $newFilter.OperatingSystem = "win11:$OsVer" }

    $newFilter.Category = $Category
    $newFilter.ReleaseType = $ReleaseType
    $newFilter.characteristic = $Characteristic
    $newFilter.preferLTSC = $PreferLTSC.IsPresent

    # silently ignore if the filter is already in the repo
    $exists = filterExists $repo $newFilter
    if (!$exists) {
      $repo.Filters += $newFilter
      WriteRepositoryFile -obj $repo
      if ($OsVer -and $Os -ne '*') { Log "Added filter $Platform {{ os='$Os', osver='$OsVer', category='$Category', release='$ReleaseType', characteristic='$Characteristic', preferLTSC='$($PreferLTSC.IsPresent)' }}" }
      else { Log "Added filter $Platform {{ os='$Os', category='$Category', release='$ReleaseType', characteristic='$Characteristic', preferLTSC='$($PreferLTSC.IsPresent)' }}" }
    }
    else
    {
      Write-Verbose "Silently ignoring this filter since exact match is already in the repository"
    }
    Write-Verbose "Repository filter added."
  }
  catch
  {
    err ("Could not add filter to the repository: $($_.Exception.Message)")
  }
}


<#
.SYNOPSIS
  Removes one or more previously added filters per specified platform from the current repository
 
.DESCRIPTION
  This command removes one or more previously added filters per specified platform from the current repository. Please note that "*" means "current" for the -Os parameter but means "all" for the -Category, -ReleaseType, -Characteristic parameters.
 
.PARAMETER Platform
  Specifies the platform to be removed from this repository. This is a 4-digit hex number that can be obtained via the Get-HPDeviceProductID command. This parameter is mandatory.
 
.PARAMETER Os
 Specifies an OS for this command to be removed from this repository. The value must be 'win10' or 'win11'. If not specified, the current operating system will be assumed, which may not be what is intended.
 
.PARAMETER OsVer
  Specifies the target OS Version (e.g. '1809', '1903', '1909', '2004', '2009', '21H1', '21H2', '22H2', '23H2', '24H2', etc). Starting from the '21H1' release, 'xxHx' format is expected. If the parameter is not specified, the current operating system version will be assumed, which may not be what is intended.
 
.PARAMETER Category
  Specifies the SoftPaq category to be removed from this repository. The value must be one (or more) of the following values:
  - Bios
  - Firmware
  - Driver
  - Software
  - OS
  - Manageability
  - Diagnostic
  - Utility
  - Driverpack
  - Dock
  - UWPPack
 
  If not specified, all categories will be removed.
 
.PARAMETER ReleaseType
  Specifies the release type for this command to remove from this repository. If not specified, all release types will be removed. The value must be one (or more) of the following values:
  - Critical
  - Recommended
  - Routine
   
  If this parameter is not specified, all release types will be removed.
 
.PARAMETER Characteristic
  Specifies the characteristic to be removed from this repository. The value must be one of the following values:
  - SSM
  - DPB
  - UWP
   
  If this parameter is not specified, all characteristics are included. If not specified, all characteristics will be removed.
 
.PARAMETER PreferLTSC
  If specified, this command uses the Long-Term Servicing Branch/Long-Term Servicing Channel (LTSB/LTSC) Reference file for the specified platform. If not specified, all preferences will be matched.
 
.PARAMETER Yes
  If specified, this command will delete the filter without asking for confirmation. If not specified, this command will ask for confirmation before deleting a filter.
 
.EXAMPLE
  Remove-RepositoryFilter -Platform 1234
 
.EXAMPLE
  Remove-RepositoryFilter -Platform 1234 -Os win10 -OsVer "21H1"
 
.EXAMPLE
  Remove-RepositoryFilter -Platform 1234 -Os win10 -OsVer "21H1" -PreferLTSC $True
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Get-HPDeviceProductID](https://developers.hp.com/hp-client-management/doc/Get-HPDeviceProductID)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
#>

function Remove-RepositoryFilter
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter")]
  param(
    [ValidatePattern("^[a-fA-F0-9]{4}$")]
    [Parameter(Position = 0,Mandatory = $true)]
    [string]$Platform,

    [ValidateSet("win7","win8","win8.1","win81","win10","win11","*")] # keep in sync with the SoftPaq module
    [string[]]
    [Parameter(Position = 1)]
    $Os = "*", # counterintuitively, "*" for this Os parameter means "current"

    [ValidateSet("1809","1903","1909","2004","2009","21H1","21H2","22H2", "23H2", "24H2")] # keep in sync with the SoftPaq module
    [Parameter(Position = 1)]
    [string]$OsVer,

    [ValidateSet("Bios","Firmware","Driver","Software","Os","Manageability","Diagnostic","Utility","Driverpack","Dock","UWPPack","*")] # keep in sync with the SoftPaq module
    [string[]]
    [Parameter(Position = 2)]
    $Category = "*",

    [ValidateSet("Critical","Recommended","Routine","*")] # keep in sync with the SoftPaq module
    [string[]]
    [Parameter(Position = 3)]
    $ReleaseType = "*",

    [Parameter(Position = 4,Mandatory = $false)]
    [switch]$Yes = $false,

    [ValidateSet("SSM","DPB","UWP","*")] # keep in sync with the SoftPaq module
    [string[]]
    [Parameter(Position = 5)]
    $Characteristic = "*",

    [Parameter(Position = 5, Mandatory = $false)]
    [nullable[boolean]]$PreferLTSC = $null
  )

  $c = LoadRepository
  try {
    if ($c[0] -eq $false) { return }

    $newFilter = New-Object SoftpaqRepositoryFile+SoftpaqRepositoryFilter
    $newFilter.platform = $Platform
    $newFilter.OperatingSystem = $Os

    if ($Os -eq "win10") {
      if ($OsVer) { $newFilter.OperatingSystem = "win10:$OsVer" }
      else { $newFilter.OperatingSystem = "win10:*" }
    }
    elseif ($Os -eq "win11") {
      if ($OsVer) { $newFilter.OperatingSystem = "win11:$OsVer" }
      else { $newFilter.OperatingSystem = "win11:*" }
    }

    $newFilter.Category = $Category
    $newFilter.ReleaseType = $ReleaseType
    $newFilter.characteristic = $Characteristic
    $newFilter.preferLTSC = $PreferLTSC

    $todelete = getFiltersWild $c[1] $newFilter
    if (-not $todelete) {
      Write-Verbose ("No matching filter to delete")
      return
    }

    if (-not $Yes.IsPresent) {
      Write-Host "The following filters will be deleted:" -ForegroundColor Cyan
      $todelete | ConvertTo-Json -Depth 2 | Write-Host -ForegroundColor Cyan
      $answer = Read-Host "Enter 'y' to continue: "
      if ($answer -ne "y") {
        Write-Host 'Aborted.'
        return }
    }

    $c[1].Filters = $c[1].Filters | Where-Object { $todelete -notcontains $_ }
    WriteRepositoryFile -obj $c[1]

    foreach ($f in $todelete) {
      Log "Removed filter $($f.platform) { os='$($f.operatingSystem)', category='$($f.category)', release='$($f.releaseType), characteristic='$($f.characteristic)' }"
    }
  }
  catch
  {
    err ("Could not remove filter from repository: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
  Retrieves the current repository definition
 
.DESCRIPTION
  This command retrieves the current repository definition as an object. This command must be executed inside an initialized repository.
   
.EXAMPLE
    $myrepository = Get-RepositoryInfo
     
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
#>

function Get-RepositoryInfo ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo")]
  param()

  $c = LoadRepository
  try {
    if (-not $c[0]) { return }
    $c[1]
  }
  catch
  {
    err ("Could not get repository info: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
  Synchronizes the current repository and generates a report that includes information about the repository
 
.DESCRIPTION
  This command performs a synchronization on the current repository by downloading the latest SoftPaqs associated with the repository filters and creates a repository report in a format (default .CSV) set via the Set-RepositoryConfiguration command.
 
  This command may be scheduled via task manager to run on a schedule. You can define a notification email via the Set-RepositoryNotificationConfiguration command to receive any failure notifications during unattended operation.
 
  This command may be followed by the Invoke-RepositoryCleanup command to remove any obsolete SoftPaqs from the repository.
 
  Please note that the Invoke-RepositorySync command is not supported in WinPE.
 
.PARAMETER Quiet
  If specified, this command will suppress progress messages during execution.
 
.PARAMETER ReferenceUrl
  Specifies an alternate location for the HP Image Assistant (HPIA) Reference files. This URL must be HTTPS. The Reference files are expected to be at the location pointed to by this URL inside a directory named after the platform ID you want a SoftPaq list for.
  Using system ID 83b2, OS Win10, and OSVer 2009 reference files as an example, this command will call the Get-SoftpaqList command to find the corresponding files in: $ReferenceUrl/83b2/83b2_64_10.0.2009.cab.
  If not specified, 'https://hpia.hpcloud.hp.com/ref/' is used by default, and fallback is set to 'https://ftp.hp.com/pub/caps-softpaq/cmit/imagepal/ref/'.
 
.EXAMPLE
  Invoke-RepositorySync -Quiet
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
#>

function Invoke-RepositorySync
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync")]
  param(
    [Parameter(Position = 0,Mandatory = $false)]
    [switch]$Quiet = $false,

    [Alias('Url')]
    [Parameter(Position = 1,Mandatory = $false)]
    [string]$ReferenceUrl = "https://hpia.hpcloud.hp.com/ref"
  )

  # only allow https or file paths with or without file:// URL prefix
  if ($ReferenceUrl -and -not ($ReferenceUrl.StartsWith("https://",$true,$null) -or [System.IO.Directory]::Exists($ReferenceUrl) -or $ReferenceUrl.StartsWith("file//:",$true,$null))) {
    throw [System.ArgumentException]"Only HTTPS or valid existing directory paths are supported."
  }

  if (-not $ReferenceUrl.EndsWith('/')) {
    $ReferenceUrl = $ReferenceUrl + "/"
  }

  # Fallback to FTP only if ReferenceUrl is the default, and not when a custom ReferenceUrl is specified
  if ($ReferenceUrl -eq 'https://hpia.hpcloud.hp.com/ref/') {
    $referenceFallbackUrL = 'https://ftp.hp.com/pub/caps-softpaq/cmit/imagepal/ref/'
  }
  else {
    $referenceFallbackUrL = ''
  }

  $repo = LoadRepository
  try {
    $cwd = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath((Get-Location))
    $cacheDir = Join-Path -Path $cwd -ChildPath ".repository"
    $cacheDirOffline = $cacheDir + "\cache\offline"
    $reportDir = $cacheDir

    # return if repository is not initialized
    if ($repo[0] -eq $false) { return }

    # return if repository is initialized but no filters added
    $filters = $repo[1].Filters
    if ($filters.Count -eq 0) {
      Write-Verbose "Repository has no filters defined - terminating."
      Write-Verbose ("Flushing the list of markers")
      FlushMarks
      return
    }

    $platformGroups = $filters | Group-Object -Property platform
    $normalized = @()

    foreach ($pobj in $platformGroups)
    {

      $items = $pobj.Group

      if ($items | Where-Object -Property operatingSystem -EQ -Value "*") {
        $items | ForEach-Object { $_.OperatingSystem = "*" }
      }

      if ($items | Where-Object -Property category -EQ -Value "*") {
        $items | ForEach-Object { $_.Category = "*" }
      }

      if ($items | Where-Object -Property releaseType -EQ -Value "*") {
        $items | ForEach-Object { $_.ReleaseType = "*" }
      }

      if ($items | Where-Object -Property characteristic -EQ -Value "*") {
        $items | ForEach-Object { $_.characteristic = "*" }
      }

      $normalized += $items | sort -Unique -Property operatingSystem,category,releaseType,characteristic
    }

    $softpaqlist = @()
    Log "Repository sync has started"
    $softpaqListCmd = @{}


    # build the list of SoftPaqs to download
    foreach ($c in $normalized) {
      Write-Verbose ($c | Format-List | Out-String)

      if (Get-HPDeviceDetails -Platform $c.platform -Url $ReferenceUrl)
      {
        $softpaqListCmd.platform = $c.platform.ToLower()
        $softpaqListCmd.Quiet = $Quiet
        $softpaqListCmd.verbose = $VerbosePreference

        Write-Verbose ("Working on a rule for platform $($softpaqListCmd.platform)")

        if ($c.OperatingSystem.StartsWith("win10:"))
        {
          $split = $c.OperatingSystem -split ':'
          $softpaqListCmd.OS = $split[0]
          $softpaqListCmd.osver = $split[1]
        }
        elseif ($c.OperatingSystem -eq "win10")
        {
          $softpaqListCmd.OS = "win10"
          $softpaqListCmd.osver = GetCurrentOSVer
        }
        elseif ($c.OperatingSystem.StartsWith("win11:"))
        {
          $split = $c.OperatingSystem -split ':'
          $softpaqListCmd.OS = $split[0]
          $softpaqListCmd.osver = $split[1]
        }
        elseif ($c.OperatingSystem -eq "win11")
        {
          $softpaqListCmd.OS = "win11"
          $softpaqListCmd.osver = GetCurrentOSVer
        }
        elseif ($c.OperatingSystem -ne "*")
        {
          $softpaqListCmd.OS = $c.OperatingSystem
          #$softpaqListCmd.osver = $null
        }

        if ($c.characteristic -ne "*")
        {
          $softpaqListCmd.characteristic = $c.characteristic.ToUpper().Split()
          Write-Verbose "Filter-characteristic:$($softpaqListCmd.characteristic)"
        }

        if ($c.ReleaseType -ne "*")
        {
          $softpaqListCmd.ReleaseType = $c.ReleaseType.Split()
          Write-Verbose "Filter-releaseType:$($softpaqListCmd.releaseType)"
        }
        if ($c.Category -ne "*")
        {
          $softpaqListCmd.Category = $c.Category.Split()
          Write-Verbose "Filter-category:$($softpaqListCmd.category)"
        }
        if ($c.preferLTSC -eq $true)
        {
          $softpaqListCmd.PreferLTSC = $true
          Write-Verbose "Filter-preferLTSC:$($softpaqListCmd.PreferLTSC)"
        }

        Log "Reading the softpaq list for platform $($softpaqListCmd.platform)"
        Write-Verbose "Trying to get SoftPaqs from $ReferenceUrl"
        $results = Get-SoftpaqList @softpaqListCmd -cacheDir $cacheDir -maxRetries $repo[1].settings.ExclusiveLockMaxRetries -ReferenceUrl $ReferenceUrl -AddHttps
        Log "softpaq list for platform $($softpaqListCmd.platform) created"
        $softpaqlist += $results


        $OfflineCacheMode = $repo[1].settings.OfflineCacheMode
        if ($OfflineCacheMode -eq "Enable") {

          # keep the download order of PlatformList, Advisory data and Knowledge Base as is to maintain unit tests
          $baseurl = $ReferenceUrl
          $url = $baseurl + "platformList.cab"
          $filename = "platformList.cab"
          Write-Verbose "Trying to download PlatformList... $url"
          $PlatformList = $null
          try {
            $PlatformList = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirOffline -Expand -Verbose:$VerbosePreference
            Write-Verbose "Finish downloading PlatformList - $PlatformList"
          }
          catch {
            if ($referenceFallbackUrL) {
              $url = "$($referenceFallbackUrL)platformList.cab"
              Write-Verbose "Trying to download PlatformList from FTP... $url"
              try {
                $PlatformList = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirOffline -Expand -Verbose:$VerbosePreference
              }
              catch {
                Write-Verbose "Error downloading $url. $($_.Exception.Message)"
                # Continue the execution with empty PlatformList file
                $PlatformList = ""
              }
            }
            if (-not $PlatformList) {
              $exception = $_.Exception
              switch ($repo[1].settings.OnRemoteFileNotFound) {
                "LogAndContinue" {
                  [string]$data = formatSyncErrorMessageAsHtml $exception
                  Log ($data -split "`n")
                  send "Softpaq repository synchronization error" $data
                }
                # "Fail"
                default {
                  throw $exception
                }
              }
            }
          }

          # download Advisory data
          $url = $baseurl + "$($softpaqListCmd.platform)/$($softpaqListCmd.platform)_cds.cab"
          $cacheDirAdvisory = $cacheDirOffline + "\$($softpaqListCmd.platform)"
          $filename = "$($softpaqListCmd.platform)_cds.cab"
          Write-Verbose "Trying to download Advisory Data Files... $url"
          $AdvisoryFile = $null
          try {
            $AdvisoryFile = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirAdvisory -Expand -Verbose:$VerbosePreference
            Write-Verbose "Finish downloading Advisory Data Files - $AdvisoryFile"
          }
          catch {
            if ($referenceFallbackUrL) {
              $url = "$($referenceFallbackUrL)$($softpaqListCmd.platform)/$($softpaqListCmd.platform)_cds.cab"
              Write-Verbose "Trying to download Advisory Data from FTP... $url"
              #$cacheDirAdvisory = $cacheDirOffline + "\$($softpaqListCmd.platform)"
              #$filename = "$($softpaqListCmd.platform)_cds.cab"
              try {
                $AdvisoryFile = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirAdvisory -Expand -Verbose:$VerbosePreference
                Write-Verbose "Finish downloading Advisory Data Files - $AdvisoryFile"
              }
              catch {
                Write-Verbose "Error downloading $url. $($_.Exception.Message)"
                # Continue the execution with empty advisory file
                $AdvisoryFile = ""
              }
            }
            if (-not $AdvisoryFile) {
              $exception = $_.Exception
              switch ($repo[1].settings.OnRemoteFileNotFound) {
                "LogAndContinue" {
                  [string]$data = formatSyncErrorMessageAsHtml $exception
                  Log ($data -split "`n")
                  send "Softpaq repository synchronization error" $data
                }
                # "Fail"
                default {
                  Write-Warning "Advisory file does not exist for platform $($softpaqListCmd.platform). $($exception.Message)."
                  #throw $exception # do not fail the whole repository sync when advisory file is missing
                }
              }
            }
          }

          # download Knowledge Base
          $url = $baseurl + "../kb/common/latest.cab"
          $cacheDirKb = $cacheDirOffline + "\kb\common"
          $filename = "latest.cab"
          Write-Verbose "Trying to download Knowledge Base... $url"
          $KnowledgeBase = $null
          try {
            $KnowledgeBase = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirKb -Verbose:$VerbosePreference
            Write-Verbose "Finish downloading Knowledge Base - $KnowledgeBase"
          }
          catch {
            if ($referenceFallbackUrL) {
              $url = "$($referenceFallbackUrL)../kb/common/latest.cab"
              Write-Verbose "Trying to download Knowledge Base from FTP... $url"
              #$cacheDirKb = $cacheDirOffline + "\kb\common"
              #$filename = "latest.cab"
              try {
                $KnowledgeBase = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirKb -Verbose:$VerbosePreference
              }
              catch {
                Write-Verbose "Error downloading $url. $($_.Exception.Message)"
                # Continue the execution with empty KnowledgeBase file
                $KnowledgeBase = ""
              }
              Write-Verbose "Finish downloading Knowledge Base - $KnowledgeBase"
            }
            if (-not $KnowledgeBase) {
              $exception = $_.Exception
              switch ($repo[1].settings.OnRemoteFileNotFound) {
                "LogAndContinue" {
                  [string]$data = formatSyncErrorMessageAsHtml $exception
                  Log ($data -split "`n")
                  send "Softpaq repository synchronization error" $data
                }
                # "Fail"
                default {
                  throw $exception
                }
              }
            }
          }
        }
      }
      else {
        Write-Host -ForegroundColor Cyan "Platform $($c.platform) doesn't exist. Please add a valid platform."
        Write-LogWarning "Platform $($c.platform) is not valid. Skipping it."
      }
    }

    Write-Verbose ("Done with the list, repository is $($softpaqlist.Count) softpaqs.")
    [array]$softpaqlist = @($softpaqlist | Sort-Object -Unique -Property Id)
    Write-Verbose ("After trimming duplicates, we have $($softpaqlist.Count) softpaqs.")

    Write-Verbose ("Flushing the list of markers")
    FlushMarks
    Write-Verbose ("Writing new marks")

    # generate .mark file for each SoftPaq to be downloaded
    foreach ($sp in $softpaqList) {
      $number = $sp.id.ToLower().TrimStart("sp")
      TouchFile -File ".repository/mark/$number.mark"
    }

    Write-Verbose ("Starting download")
    $downloadCmd = @{}
    $downloadCmd.Quiet = $quiet
    $downloadCmd.Verbose = $VerbosePreference

    Log "Download has started for $($softpaqlist.Count) softpaqs."
    foreach ($sp in $softpaqlist)
    {
      $downloadCmd.Number = $sp.id.ToLower().TrimStart("sp")
      $downloadCmd.Url = $sp.url -Replace "/$($sp.id).exe$",''
      Write-Verbose "Working on data for softpaq $($downloadCmd.number)"
      try {
        Log "Start downloading files for sp$($downloadCmd.number)."
        DownloadSoftpaq -DownloadSoftpaqCmd $downloadCmd -MaxRetries $repo[1].settings.ExclusiveLockMaxRetries -Verbose:$VerbosePreference

        if ($OfflineCacheMode -eq "Enable") {
          Log (" sp$($downloadCmd.number).html - Downloading Release Notes.")
          $ReleaseNotesurl = Get-HPPrivateItemUrl $downloadCmd.number "html"
          $target = "sp$($downloadCmd.number).html"
          $targetfile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($target)
          Invoke-HPPrivateDownloadFile -url $ReleaseNotesurl -Target $targetfile
          Log (" sp$($downloadCmd.number).html - Done Downloading Release Notes.")
        }
        Log "Finish downloading files for sp$($downloadCmd.number)."
      }
      catch {
        $exception = $_.Exception

        switch ($repo[1].settings.OnRemoteFileNotFound)
        {
          "LogAndContinue" {
            [string]$data = formatSyncErrorMessageAsHtml $exception
            Log ($data -split "`n")
            send "Softpaq repository synchronization error" $data
          }
          # "Fail"
          default {
            Write-Output "Error downloading $($downloadCmd.Url). $($exception.Message)"
            throw $exception
          }
        }
      }
    }

    Log "Repository sync has ended"
    Write-Verbose "Repository Sync has ended."

    Log "Repository Report creation started"
    Write-Verbose "Repository Report creation started."

    try {
      # get the configuration set for repository report if any
      $RepositoryReport = $repo[1].settings.RepositoryReport
      if ($RepositoryReport) {
        $Format = $RepositoryReport
        New-RepositoryReport -Format $Format -RepositoryPath "$cwd" -OutputFile "$cwd\.repository\Contents.$Format"
        Log "Repository Report created as Contents.$Format"
        Write-Verbose "Repository Report created as Content.$Format."
      }
    }
    catch [System.IO.FileNotFoundException]{
      Write-Verbose "No data available to create Repository Report as directory '$(Get-Location)' does not contain any CVA files."
      Log "No data available to create Repository Report as directory '$(Get-Location)' does not contain any CVA files."
    }
    catch {
      Write-Verbose "Error in creating Repository Report"
      Log "Error in creating Repository Report."
    }
  }
  catch
  {
    err "Repository synchronization failed: $($_.Exception.Message)" $true
    [string]$data = formatSyncErrorMessageAsHtml $_.Exception
    Log ($data -split "`n")
    send "Softpaq repository synchronization error" $data
  }
}

function formatSyncErrorMessageAsHtml ($exception)
{
  [string]$data = "An error occurred during softpaq synchronization.`n`n";
  $data += "The error was: <em>$($exception.Message)</em>`n"
  $data += "`nDetails:`n<pre>"
  $data += "<hr/>"
  $data += ($exception | Format-List -Force | Out-String)
  $data += "</pre>"
  $data += "<hr/>"
  $data
}

<#
.SYNOPSIS
    Removes obsolete SoftPaqs from the current repository
   
.DESCRIPTION
  This command removes SoftPaqs from the current repository that are labeled as obsolete. These may be SoftPaqs that have been replaced
  by newer versions, or SoftPaqs that no longer match the active repository filters.
 
.EXAMPLE
    Invoke-RepositoryCleanup
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
 
#>

function Invoke-RepositoryCleanup
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup")]
  param()
  $repo = LoadRepository
  Log ("Beginning repository cleanup")
  $deleted = 0

  try {
    Get-ChildItem "." -File | ForEach-Object {
      $name = $_.Name.ToLower().TrimStart("sp").Split('.')[0]
      if ($name -ne $null) {
        if (-not (Test-Path ".repository/mark/$name.mark" -PathType Leaf))
        {
          Write-Verbose "Deleting orphaned file $($_.Name)"
          Remove-Item $_.Name
          $deleted++
        }
        #else {
        # Write-Verbose "Softpaq $($_.Name) is still needed."
        #}
      }
    }
    Log ("Completed repository cleanup, deleted $deleted files.")
  }
  catch {
    err ("Could not clean repository: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
  Sets the repository notification configuration in the current repository
 
.DESCRIPTION
  This command defines a notification Simple Mail Transfer Protocol (SMTP) server (and optionally, port) for an email server to be used to send failure notifications during unattended synchronization via the Invoke-RepositorySync command.
 
  One or more recipients can then be added via the Add-RepositorySyncFailureRecipient command.
 
  This command must be invoked inside a directory initialized as a repository using the Initialize-Repository command.
 
 
.PARAMETER Server
  Specifies the server name (or IP) for the outgoing mail (SMTP) server
 
.PARAMETER Port
  Specifies a port for the SMTP server. If not specified, the default IANA-assigned port 25 will be used.
 
.PARAMETER Tls
  Specifies the usage for Transport Layer Security (TLS). The value may be 'true', 'false', or 'auto'. 'Auto' will automatically set TLS to true when the port is changed to a value different than 25. By default, TLS is false. Please note that TLS is the successor protocol to Secure Sockets Layer (SSL).
 
.PARAMETER UserName
  Specifies the SMTP server username for authenticated SMTP servers. If not specified, connection will be made without authentication.
 
.PARAMETER Password
  Specifies the SMTP server password for authenticated SMTP servers.
   
.PARAMETER From
  Specifies the email address from which the notification will appear to originate. Note that some servers may accept emails from specified domains only or require the email address to match the username.
 
.PARAMETER FromName
  Specifies the from address display name
 
.PARAMETER RemoveCredentials
  If specified, this command will remove the SMTP server credentials without removing the entire mail server configuration.
 
.EXAMPLE
  Set-RepositoryNotificationConfiguration smtp.mycompany.com
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
 
#>

function Set-RepositoryNotificationConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration")]
  param(
    [Parameter(Position = 0,Mandatory = $false)]
    [string]
    [ValidatePattern("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")]
    $Server = $null,

    [Parameter(Position = 1,Mandatory = $false)]
    [ValidateRange(1,65535)]
    [int]
    $Port = 0,

    [Parameter(Position = 2,Mandatory = $false)]
    [string]
    [ValidateSet('true','false','auto')]
    $Tls = $null,

    [Parameter(Position = 3,Mandatory = $false)]
    [string]
    $Username = $null,

    [Parameter(Position = 4,Mandatory = $false)]
    [string]
    $Password = $null,

    [Parameter(Position = 5,Mandatory = $false)]
    [string]
    [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
    $From = $null,

    [Parameter(Position = 6,Mandatory = $false)]
    [string]
    $FromName = $null,

    [Parameter(Position = 7,Mandatory = $false)]
    [switch]
    $RemoveCredentials
  )

  Write-Verbose "Beginning notification configuration update"

  if ($RemoveCredentials.IsPresent -and ([string]::IsNullOrEmpty($UserName) -eq $false -or [string]::IsNullOrEmpty($Password) -eq $false))
  {
    err ("-removeCredentials may not be specified with -username or -password")
    return
  }

  $c = LoadRepository
  try {
    if (-not $c[0]) { return }

    Write-Verbose "Applying configuration"
    if ([string]::IsNullOrEmpty($Server) -eq $false) {
      Write-Verbose ("Setting SMTP Server to: $Server")
      $c[1].Notifications.server = $Server
    }

    if ($Port) {
      Write-Verbose ("Setting SMTP Server port to: $Port")
      $c[1].Notifications.port = $Port
    }

    if (-not [string]::IsNullOrEmpty($UserName)) {
      Write-Verbose ("Setting SMTP server credential(username) to: $UserName")
      $c[1].Notifications.UserName = $UserName
    }

    if (-not [string]::IsNullOrEmpty($Password)) {
      Write-Verbose ("Setting SMTP server credential(password) to: (redacted)")
      $c[1].Notifications.Password = ConvertTo-SecureString $Password -Force -AsPlainText | ConvertFrom-SecureString
    }

    if ($RemoveCredentials.IsPresent)
    {
      Write-Verbose ("Clearing credentials from notification configuration")
      $c[1].Notifications.UserName = $null
      $c[1].Notifications.Password = $null
    }

    switch ($Tls)
    {
      "auto" {
        if ($Port -ne 25) { $c[1].Notifications.tls = $true }
        else { $c[1].Notifications.tls = $false }
        Write-Verbose ("SMTP server SSL auto-calculated to: $($c[1].Notifications.tls)")
      }

      "true" {
        $c[1].Notifications.tls = $true
        Write-Verbose ("Setting SMTP SSL to: $($c[1].Notifications.tls)")
      }
      "false" {
        $c[1].Notifications.tls = $false
        Write-Verbose ("Setting SMTP SSL to: $($c[1].Notifications.tls)")
      }
    }
    if (-not [string]::IsNullOrEmpty($From)) {
      Write-Verbose ("Setting Mail from address to: $From")
      $c[1].Notifications.from = $From }
    if (-not [string]::IsNullOrEmpty($FromName)) {
      Write-Verbose ("Setting Mail from displayname to: $FromName")
      $c[1].Notifications.fromname = $FromName }

    WriteRepositoryFile -obj $c[1]
    Log ("Updated notification configuration")
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
    Clears the repository notification configuration from the current repository
 
.DESCRIPTION
  This command removes notification configuration from the current repository, and as a result, notifications are turned off.
  This command must be invoked inside a directory initialized as a repository using the Initialize-Repository command.
  Please note that notification configuration must have been defined via the Set-RepositoryNotificationConfiguration command for this command to have any effect.
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
  Clear-RepositoryNotificationConfiguration
 
#>

function Clear-RepositoryNotificationConfiguration ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration")]
  param()
  Log "Clearing notification configuration"

  $c = LoadRepository
  try {
    if (-not $c[0]) { return }
    $c[1].Notifications = $null
    WriteRepositoryFile -obj $c[1]
    Write-Verbose ("Ok.")
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
  Retrieves the current notification configuration
 
.DESCRIPTION
  This command retrieves the current notification configuration as an object.
  This command must be invoked inside a directory initialized as a repository using the Initialize-Repository command.
  The current notification configuration must be set via the Set-RepositoryNotificationConfiguration command.
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
  $config = Get-RepositoryNotificationConfiguration
 
 
#>

function Get-RepositoryNotificationConfiguration ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration")]
  param()

  $c = LoadRepository
  if ((-not $c[0]) -or (-not $c[1].Notifications))
  {
    return $null
  }
  return $c[1].Notifications
}


<#
.SYNOPSIS
    Displays the current notification configuration onto the screen
 
 
.DESCRIPTION
  This command retrieves the current notification configuration as user-friendly screen output.
  This command must be invoked inside a directory initialized as a repository using the Initialize-Repository command.
  The current notification configuration must be set via the Set-RepositoryNotificationConfiguration command.
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
  Show-RepositoryNotificationConfiguration
#>

function Show-RepositoryNotificationConfiguration ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration")]
  param()

  try {
    $c = Get-RepositoryNotificationConfiguration
    if (-not $c)
    {
      err ("Notifications are not configured.")
      return
    }

    if (-not [string]::IsNullOrEmpty($c.UserName)) {
      Write-Host "Notification server: smtp://$($c.username):<password-redacted>@$($c.server):$($c.port)"
    }
    else {
      Write-Host "Notification server: smtp://$($c.server):$($c.port)"
    }
    Write-Host "Email will arrive from $($c.from) with name `"$($c.fromname)`""

    if ((-not $c.addresses) -or (-not $c.addresses.Count))
    {
      Write-Host "There are no recipients configured"
      return
    }
    foreach ($r in $c.addresses)
    {
      Write-Host "Recipient: $r"
    }
  }
  catch {
    err ("Failed to read repository configuration: $($_.Exception.Message)")
  }

}

<#
.SYNOPSIS
  Adds a recipient to the list of recipients to receive failure notification emails for the current repository
 
.DESCRIPTION
  This command adds a recipient via an email address to the list of recipients to receive failure notification emails for the current repository. If any failure occurs, notifications will be sent to this email address.
 
  This command must be invoked inside a directory initialized as a repository using the Initialize-Repository command.
 
.PARAMETER To
  Specifies the email address to add as a recipient of the failure notifications
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
  Add-RepositorySyncFailureRecipient -to someone@mycompany.com
 
#>

function Add-RepositorySyncFailureRecipient ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient")]
  param(
    [Parameter(Position = 0,Mandatory = $true)]
    [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
    [string]
    $To
  )

  Log "Adding '$To' as a recipient."
  $c = LoadRepository
  try {
    if (-not $c[0]) { return }

    if (-not $c[1].Notifications) {
      err ("Notifications are not configured")
      return
    }

    if (-not $c[1].Notifications.addresses) {
      $c[1].Notifications.addresses = $()
    }

    $c[1].Notifications.addresses += $To.trim()
    $c[1].Notifications.addresses = $c[1].Notifications.addresses | Sort-Object -Unique
    WriteRepositoryFile -obj ($c[1] | Sort-Object -Unique)
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }

}

<#
.SYNOPSIS
  Removes a recipient from the list of recipients that receive failure notification emails for the current repository
 
 
.DESCRIPTION
  This command removes an email address as a recipient for synchronization failure messages.
  This command must be invoked inside a directory initialized as a repository using the Initialize-Repository command.
  Notification configured via the Set-RepositoryNotificationConfiguration command.
 
.PARAMETER To
  Specifies the email address to remove
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
  Remove-RepositorySyncFailureRecipient -to someone@mycompany.com
 
#>

function Remove-RepositorySyncFailureRecipient
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient")]
  param(
    [Parameter(Position = 0,Mandatory = $true)]
    [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
    [string]
    $To
  )
  Log "Removing '$To' as a recipient."
  $c = LoadRepository
  try {
    if ($c[0] -eq $false) { return }

    if (-not $c[1].Notifications) {
      err ("Notifications are not configured")
      return
    }


    if (-not $c[1].Notifications.addresses) {
      $c[1].Notifications.addresses = $()
    }

    $c[1].Notifications.addresses = $c[1].Notifications.addresses | Where-Object { $_ -ne $To.trim() } | Sort-Object -Unique
    WriteRepositoryFile -obj ($c[1] | Sort-Object -Unique)
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }
}


<#
.SYNOPSIS
  Tests the email notification configuration by sending a test email
 
.DESCRIPTION
  This command sends a test email using the current repository configuration and reports
  any errors associated with the send process. This command is intended to debug the email server configuration.
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient)
 
.EXAMPLE
  Test-RepositoryNotificationConfiguration
 
#>

function Test-RepositoryNotificationConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration")]
  param()

  Log ("test email started")
  send "Repository Failure Notification (Test only)" "No content." -html $false
  Write-Verbose ("Ok.")
}

<#
.SYNOPSIS
  Sets repository configuration values
 
.DESCRIPTION
  This command is used to configure different settings of the repository synchronization:
 
  - OnRemoteFileNotFound: Indicates the behavior for when the SoftPaq is not found on the remote site. 'Fail' stops the execution. 'LogAndContinue' logs the errors and continues the execution.
  - RepositoryReport: Indicates the format of the report generated at repository synchronization. The default format is 'CSV' and other options available are 'JSON,' 'XML,' and 'ExcelCSV.'
  - OfflineCacheMode: Indicates that all repository files are required for offline use. Repository synchronization will include platform list, advisory, and knowledge base files. The default value is 'Disable' and the other option is 'Enable.'
 
.PARAMETER Setting
  Specifies the setting to configure. The value must be one of the following values: 'OnRemoteFileNotFound', 'OfflineCacheMode', or 'RepositoryReport'.
 
.PARAMETER Value
  Specifies the new value for the OnRemoteFileNotFound setting. The value must be either: 'Fail' (default), or 'LogAndContinue'.
 
.PARAMETER CacheValue
  Specifies the new value for the OfflineCacheMode setting. The value must be either: 'Disable' (default), or 'Enable'.
 
.PARAMETER Format
  Specifies the new value for the RepositoryReport setting. The value must be one of the following: 'CSV' (default), 'JSon', 'XML', or 'ExcelCSV'.
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
 
.LINK
  [Get-RepositoryConfiguration](https://developers.hp.com/hp-client-management/doc/Get-RepositoryConfiguration)
 
.Example
  Set-RepositoryConfiguration -Setting OnRemoteFileNotFound -Value LogAndContinue
 
.Example
  Set-RepositoryConfiguration -Setting OfflineCacheMode -CacheValue Enable
 
.Example
  Set-RepositoryConfiguration -Setting RepositoryReport -Format CSV
 
.NOTES
  - When using HP Image Assistant and offline mode, use: Set-RepositoryConfiguration -Setting OfflineCacheMode -CacheValue Enable
  - More information on using HPIA with CMSL can be found at this [blog post](https://developers.hp.com/hp-client-management/blog/driver-injection-hp-image-assistant-and-hp-cmsl-in-memcm).
  - To create a report outside the repository, use the New-RepositoryReport command.
#>

function Set-RepositoryConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Set-RepositoryConfiguration")]
  param(
    [ValidateSet('OnRemoteFileNotFound','OfflineCacheMode','RepositoryReport')]
    [Parameter(ParameterSetName = "ErrorHandler",Position = 0,Mandatory = $true)]
    [Parameter(ParameterSetName = "CacheMode",Position = 0,Mandatory = $true)]
    [Parameter(ParameterSetName = "ReportHandler",Position = 0,Mandatory = $true)]
    [string]$Setting,

    [Parameter(ParameterSetName = "ErrorHandler",Position = 1,Mandatory = $true)]
    [ErrorHandling]$Value,

    [ValidateSet('Enable','Disable')]
    [Parameter(ParameterSetName = "CacheMode",Position = 1,Mandatory = $true)]
    [string]$CacheValue,

    [ValidateSet('CSV','JSon','XML','ExcelCSV')]
    [Parameter(ParameterSetName = "ReportHandler",Position = 1,Mandatory = $true)]
    [string]$Format
  )
  $c = LoadRepository
  if (-not $c[0]) { return }
  if ($Setting -eq "OnRemoteFileNotFound") {
    if (($Value -eq "Fail") -or ($Value -eq "LogAndContinue")) {
      $c[1].settings. "${Setting}" = $Value
      WriteRepositoryFile -obj $c[1]
      Write-Verbose ("Ok.")
    }
    else {
      Write-Host -ForegroundColor Magenta "Enter valid Value for $Setting."
      Write-LogWarning "Enter valid Value for $Setting."
    }
  }
  elseif ($Setting -eq "OfflineCacheMode") {
    if ($CacheValue) {
      $c[1].settings. "${Setting}" = $CacheValue
      WriteRepositoryFile -obj $c[1]
      Write-Verbose ("Ok.")
    }
    else {
      Write-Host -ForegroundColor Magenta "Enter valid CacheValue for $Setting."
      Write-LogWarning "Enter valid CacheValue for $Setting."
    }
  }
  elseif ($Setting -eq "RepositoryReport") {
    if ($Format) {
      $c[1].settings. "${Setting}" = $Format
      WriteRepositoryFile -obj $c[1]
      Write-Verbose ("Ok.")
    }
    else {
      Write-Host -ForegroundColor Magenta "Enter valid Format for $Setting."
      Write-LogWarning "Enter valid Format for $Setting."
    }
  }
}

<#
.SYNOPSIS
    Retrieves the configuration values for a specified setting in the current repository
 
.DESCRIPTION
  This command retrieves various configuration options that control synchronization behavior. The settings this command can retrieve include:
 
  - OnRemoteFileNotFound: Indicates the behavior for when the SoftPaq is not found on the remote site. 'Fail' stops the execution. 'LogAndContinue' logs the errors and continues the execution.
  - RepositoryReport: Indicates the format of the report generated at repository synchronization. The default format is 'CSV' and other options available are 'JSON', 'XML', and 'ExcelCSV'.
  - OfflineCacheMode: Indicates that all repository files are required for offline use. Repository synchronization will include platform list, advisory, and knowledge base files. The default value is 'Disable' and the other option is 'Enable'.
 
    
.PARAMETER setting
  Specifies the setting to retrieve. The value can be one of the following: 'OnRemoteFileNotFound', 'RepositoryReport', or 'OfflineCacheMode'.
 
 
.Example
  Get-RepositoryConfiguration -Setting OfflineCacheMode
 
.Example
  Get-RepositoryConfiguration -Setting OnRemoteFileNotFound
 
.Example
  Get-RepositoryConfiguration -Setting RepositoryReport
 
.LINK
  [Set-RepositoryConfiguration](https://developers.hp.com/hp-client-management/doc/Set-RepositoryConfiguration)
 
.LINK
  [Initialize-Repository](https://developers.hp.com/hp-client-management/doc/Initialize-Repository)
#>

function Get-RepositoryConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Get-RepositoryConfiguration")]
  param(
    [Parameter(Position = 0,Mandatory = $true)]
    [string]
    [ValidateSet('OnRemoteFileNotFound','OfflineCacheMode','RepositoryReport')]
    $Setting
  )
  $c = LoadRepository
  if (-not $c[0]) { return }
  $c[1].settings. "${Setting}"
}


<#
.SYNOPSIS
  Creates a report from a repository directory
 
.DESCRIPTION
  This command creates a report from a repository directory or any directory containing CVAs (and EXEs) in one of the supported formats.
 
  The supported formats are:
 
  - XML: Returns an XML object
  - JSON: Returns a JSON document
  - CSV: Returns a CSV object
  - ExcelCSV: Returns a CSV object containing an Excel hint that defines the comma character as the delimiter. Use this format only if you plan on opening the CSV file with Excel.
 
  If a format is not specified, this command will return the output as PowerShell objects to the pipeline. Please note that the repository directory must contain CVAs for the command to generate a report successfully. EXEs are not required, but the EXEs will allow information like the time of download and size in bytes to be included in the report.
 
.PARAMETER Format
  Specifies the output format (CSV, JSON, or XML) of the report. If not specified, this command will return the output as PowerShell objects.
 
.PARAMETER RepositoryPath
  Specifies a different location for the repository. By default, this command assumes the repository is the current directory.
 
.PARAMETER OutputFile
  Specifies a file to write the output to. You can specify a relative path or an absolute path. If a relative path is specified, the file will be written relative to the current directory and if RepositoryPath parameter is also specified, the file will still be written relative to the current directory and not relative to the value in RepositoryPath.
  This parameter requires the -Format parameter to also be specified.
  If specified, this command will create the file (if it does not exist) and write the output to the file instead of returning the output as a PowerShell, XML, CSV, or JSON object.
  Please note that if the output file already exists, the contents of the file will be overwritten.
 
 
.EXAMPLE
  New-RepositoryReport -Format JSON -RepositoryPath c:\myrepository\softpaqs -OutputFile c:\repository\today.json
 
.EXAMPLE
  New-RepositoryReport -Format ExcelCSV -RepositoryPath c:\myrepository\softpaqs -OutputFile c:\repository\today.csv
 
.NOTES
  This command currently supports scenarios where the SoftPaq executable is stored under the format sp<softpaq-number>.exe.
#>

function New-RepositoryReport
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/New-RepositoryReport")]
  param(
    [Parameter(Position = 0,Mandatory = $false)]
    [ValidateSet('CSV','JSon','XML','ExcelCSV')]
    [string]$Format,

    [Parameter(Position = 1,Mandatory = $false)]
    [System.IO.DirectoryInfo]$RepositoryPath = '.',

    [Parameter(Position = 2,Mandatory = $false)]
    [System.IO.FileInfo]$OutputFile
  )
  if ($OutputFile -and -not $format) { throw "OutputFile parameter requires a Format specifier" }

  $cvaList = @(Get-ChildItem -Path $RepositoryPath -Filter '*.cva')

  if (-not $cvaList -or -not $cvaList.Length)
  {
    throw [System.IO.FileNotFoundException]"Directory '$(Get-Location)' does not contain CVA files."
  }

  if($cvaList.Length -eq 1){
    Write-Verbose "Processing $($cvaList.Length) CVA"
  }
  else{
    Write-Verbose "Processing $($cvaList.Length) CVAs"
  }

  $results = $cvaList | ForEach-Object {
    Write-Verbose "Processing $($_.FullName)"
    $cva = Get-HPPrivateReadINI $_.FullName

    try {
      $exe = Get-ChildItem -Path ($cva.Softpaq.SoftpaqNumber.trim() + ".exe") -ErrorAction stop
    }
    catch [System.Management.Automation.ItemNotFoundException]{
      $exe = $null
    }

    [pscustomobject]@{
      Softpaq = $cva.Softpaq.SoftpaqNumber
      Vendor = $cva.General.VendorName
      Title = $cva. "Software Title".US
      type = if ($Cva.General.Category.contains("-")) { $Cva.General.Category.substring(0,$Cva.General.Category.IndexOf('-')).trim() } else { $Cva.General.Category }
      Version = "$($cva.General.Version) Rev.$($cva.General.Revision)"
      Downloaded = if ($exe) { $exe.CreationTime } else { "" }
      Size = if ($exe) { "$($exe.Length)" } else { "" }
    }
  }
  switch ($format)
  {
    "CSV" {
      $r = $results | ConvertTo-Csv -NoTypeInformation
    }
    "ExcelCSV" {

      $r = $results | ConvertTo-Csv -NoTypeInformation
      $r = [string[]]"sep=," + $r
    }
    "JSon" {
      $r = $results | ConvertTo-Json
    }
    "XML" {
      $r = $results | ConvertTo-Xml -NoTypeInformation
    }
    default {
      return $results
    }
  }

  if ($OutputFile) {
    if ($format -eq "xml") { $r = $r.OuterXml }
    $r | Out-File -FilePath $OutputFile -Encoding utf8
  }
  else { $r }
}






# SIG # Begin signature block
# MIIoGAYJKoZIhvcNAQcCoIIoCTCCKAUCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDeDiAx78rR7PBD
# N072TGMavGdp62ib69cE9A2S3Z3CJqCCDYowggawMIIEmKADAgECAhAIrUCyYNKc
# TJ9ezam9k67ZMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV
# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0z
# NjA0MjgyMzU5NTlaMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg
# UlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
# ggIKAoICAQDVtC9C0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0
# JAfhS0/TeEP0F9ce2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJr
# Q5qZ8sU7H/Lvy0daE6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhF
# LqGfLOEYwhrMxe6TSXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+F
# LEikVoQ11vkunKoAFdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh
# 3K3kGKDYwSNHR7OhD26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJ
# wZPt4bRc4G/rJvmM1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQay
# g9Rc9hUZTO1i4F4z8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbI
# YViY9XwCFjyDKK05huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchAp
# QfDVxW0mdmgRQRNYmtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRro
# OBl8ZhzNeDhFMJlP/2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IB
# WTCCAVUwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+
# YXsIiGX0TkIwHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0P
# AQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAk
# BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAC
# hjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v
# dEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAED
# MAgGBmeBDAEEATANBgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql
# +Eg08yy25nRm95RysQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFF
# UP2cvbaF4HZ+N3HLIvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1h
# mYFW9snjdufE5BtfQ/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3Ryw
# YFzzDaju4ImhvTnhOE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5Ubdld
# AhQfQDN8A+KVssIhdXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw
# 8MzK7/0pNVwfiThV9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnP
# LqR0kq3bPKSchh/jwVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatE
# QOON8BUozu3xGFYHKi8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bn
# KD+sEq6lLyJsQfmCXBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQji
# WQ1tygVQK+pKHJ6l/aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbq
# yK+p/pQd52MbOoZWeE4wggbSMIIEuqADAgECAhAJvPMqSNxAYhV5FFpsbzOhMA0G
# CSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg
# UlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwHhcNMjQwMjE1MDAwMDAwWhcNMjUwMjE4
# MjM1OTU5WjBaMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAG
# A1UEBxMJUGFsbyBBbHRvMRAwDgYDVQQKEwdIUCBJbmMuMRAwDgYDVQQDEwdIUCBJ
# bmMuMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEApbF6fMFy6zhGVra3
# SZN418Cp2O8kjihQCU9tqPO9tkzbMyTsgveLJVnXPJNG9kQPMGUNp+wEHcoUzlRc
# YJMEL9fhfzpWPeSIIezGLPCdrkMmS3fdRUwFqEs7z/C6Ui2ZqMaKhKjBJTIWnipe
# rRfzGB7RoLepQcgqeF5s0DBy4oG83dqcRHo3IJRTBg39tHe3mD5uoGHn5n366abX
# vC+k53BVyD8w8XLppFVH5XuNlXMq/Ohf613i7DRb/+u92ZiAPVPXXnlxUE26cuDb
# OfJKN/bXPmvnWcNW3YHVp9ztPTQZhX4yWYXHrAI2Cv6HxUpO6NzhFoRoBTkcYNbA
# 91pf1Vagh/MNcA2BfQYT975/Vlvj9cfEZ/NwZthZuHa3rdrvCKhhjw7YU2QUeaTJ
# 0uaX4g6B9PFNqAASYLach3CDJiLmYEfus/utPh57mk0q27yL25fXo/PaMDXiDNIi
# 7Wuz7A+sPsbtdiY8zvEIRQ+XJXtKAlD4tqG9YzlTO6ZoQX/rAgMBAAGjggIDMIIB
# /zAfBgNVHSMEGDAWgBRoN+Drtjv4XxGG+/5hewiIZfROQjAdBgNVHQ4EFgQURH4F
# u5yEAuElYWUbyGRYkNLLrA8wPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEF
# BQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMA4GA1UdDwEB/wQEAwIH
# gDATBgNVHSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRw
# Oi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmlu
# Z1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hB
# Mzg0MjAyMUNBMS5jcmwwgZQGCCsGAQUFBwEBBIGHMIGEMCQGCCsGAQUFBzABhhho
# dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXAYIKwYBBQUHMAKGUGh0dHA6Ly9jYWNl
# cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNB
# NDA5NlNIQTM4NDIwMjFDQTEuY3J0MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQAD
# ggIBAFiCyuI6qmaQodDyMNpp0l7eIXFgJ4JI59o59PleFj4rcyd/+F4iI7u5if8G
# rV5Kn3s3tK9vfJO8SpqtEh7lL4e69z6v3ohcy4uy2hsjKQ/fFcDo9pQYDGmDVjCa
# D5qSVEIBlJHBe5NKEJAgUE0kaMjLzbi2+8DKJlNtvZ+hatuPl9fMnmU+VbQh7JhZ
# yJdz8Ay0tcQ9lC8HAX5Ah/pU+Vtv+c8gMSxjS1aWXoGCa1869IVi2O6qx7MuX12U
# 1eIpB9XxYr7HSebvg2G7Gz6nCh7u+4k7m3hJu9EStUIN2JII5260+E60uDWoHEhx
# tHbdueFQxJrTKnhplOSaaPFCVBDkWG83ZzN9N3z/45w1pBUNBiPJdRQJ58MhBYQe
# Zl90heMBL8QNQk2i0E5gHNT9pJiCR9+mvJkRxEVgUn+16ZpVnI6kzhThV9qBaWVF
# h83X4UWc/nwHKIuu+4x4fmkYc79A3MrsHflZIO8jOy0GC/xBnZTQ8s5b9Tb2UkHk
# w692Ypl7War3W7M37JCAPC/A7M4CwQYjdjG43zs5m36auYVaTvRLKtZVLzcj8oZX
# 4vqhlZ8+jCPXFiuDfoBXiTckTLpv/eHQ6q7Aoda+qARWPPE1U2v5r/lpKVqIx7B4
# PdFZAUf5MtG/Bj7LVXvXjW8ABIJv7L4cI2akn6Es0dmvd6PsMYIZ5DCCGeACAQEw
# fTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNV
# BAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hB
# Mzg0IDIwMjEgQ0ExAhAJvPMqSNxAYhV5FFpsbzOhMA0GCWCGSAFlAwQCAQUAoHww
# EAYKKwYBBAGCNwIBDDECMAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYK
# KwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIOfNvKrC
# LbL4ynom/QlApokO5yc5c5yJE9tNq9itEt50MA0GCSqGSIb3DQEBAQUABIIBgCTq
# kDdEMYy91oWW5ddTkKvIQMsYtZvfnI0TFPnBxptKuGEoRHE85mtoNb0desqpD5p8
# Slnlb9Nc9ls5AMU4kil0FufAT//YX7Z18a7ovwTpwm29T0NIpL5tujVCvvc216kb
# FZRlONWnLvRaMVB2ifnCTn36KbJzVaYngHxQTCslFBt3w4UffbuJUQn6Q7BmN32U
# EqexxWIUuDs7G4p9BuAx3aWl/l1ATvOecZKd8ro5bRw5+k0ca3O6MqabU1/nU/K3
# DopMbXNLydSQY30QDj1DMYTQzz3bsTWWdDXyGm+FIv0gMLNvGfc2sA/L8v2bQw9p
# Okj+j6N11jCjlS4xGRVAXhLoTCNfbJnzPW1nQqGr892UdBm1yPwY7lFdzyQ1CYp/
# RaI56ckmrZxGWNf5IeJOK8a83m0nxH19GX93ksM+WaVCTcc66wmwBKWJXj2IeOw4
# artnG7zjQDQuPFX+utz4NHP9Rao2IK7Bt/OEO+i7ZgzE6Nhb4e52DfYjnoRkZqGC
# Fzowghc2BgorBgEEAYI3AwMBMYIXJjCCFyIGCSqGSIb3DQEHAqCCFxMwghcPAgED
# MQ8wDQYJYIZIAWUDBAIBBQAweAYLKoZIhvcNAQkQAQSgaQRnMGUCAQEGCWCGSAGG
# /WwHATAxMA0GCWCGSAFlAwQCAQUABCBsiMBmmGSoC1qgEZG8QlZH1n21HqJcxBI5
# LT8omyX4qwIRAMcr5xrnuRVTP6cLYNUrpCYYDzIwMjQxMTA2MTcxNzMzWqCCEwMw
# gga8MIIEpKADAgECAhALrma8Wrp/lYfG+ekE4zMEMA0GCSqGSIb3DQEBCwUAMGMx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg
# Q0EwHhcNMjQwOTI2MDAwMDAwWhcNMzUxMTI1MjM1OTU5WjBCMQswCQYDVQQGEwJV
# UzERMA8GA1UEChMIRGlnaUNlcnQxIDAeBgNVBAMTF0RpZ2lDZXJ0IFRpbWVzdGFt
# cCAyMDI0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvmpzn/aVIauW
# MLpbbeZZo7Xo/ZEfGMSIO2qZ46XB/QowIEMSvgjEdEZ3v4vrrTHleW1JWGErrjOL
# 0J4L0HqVR1czSzvUQ5xF7z4IQmn7dHY7yijvoQ7ujm0u6yXF2v1CrzZopykD07/9
# fpAT4BxpT9vJoJqAsP8YuhRvflJ9YeHjes4fduksTHulntq9WelRWY++TFPxzZrb
# ILRYynyEy7rS1lHQKFpXvo2GePfsMRhNf1F41nyEg5h7iOXv+vjX0K8RhUisfqw3
# TTLHj1uhS66YX2LZPxS4oaf33rp9HlfqSBePejlYeEdU740GKQM7SaVSH3TbBL8R
# 6HwX9QVpGnXPlKdE4fBIn5BBFnV+KwPxRNUNK6lYk2y1WSKour4hJN0SMkoaNV8h
# yyADiX1xuTxKaXN12HgR+8WulU2d6zhzXomJ2PleI9V2yfmfXSPGYanGgxzqI+Sh
# oOGLomMd3mJt92nm7Mheng/TBeSA2z4I78JpwGpTRHiT7yHqBiV2ngUIyCtd0pZ8
# zg3S7bk4QC4RrcnKJ3FbjyPAGogmoiZ33c1HG93Vp6lJ415ERcC7bFQMRbxqrMVA
# Niav1k425zYyFMyLNyE1QulQSgDpW9rtvVcIH7WvG9sqYup9j8z9J1XqbBZPJ5XL
# ln8mS8wWmdDLnBHXgYly/p1DhoQo5fkCAwEAAaOCAYswggGHMA4GA1UdDwEB/wQE
# AwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMCAGA1Ud
# IAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATAfBgNVHSMEGDAWgBS6FtltTYUv
# cyl2mi91jGogj57IbzAdBgNVHQ4EFgQUn1csA3cOKBWQZqVjXu5Pkh92oFswWgYD
# VR0fBFMwUTBPoE2gS4ZJaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
# VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVTdGFtcGluZ0NBLmNybDCBkAYIKwYB
# BQUHAQEEgYMwgYAwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNv
# bTBYBggrBgEFBQcwAoZMaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lD
# ZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVTdGFtcGluZ0NBLmNydDANBgkq
# hkiG9w0BAQsFAAOCAgEAPa0eH3aZW+M4hBJH2UOR9hHbm04IHdEoT8/T3HuBSyZe
# q3jSi5GXeWP7xCKhVireKCnCs+8GZl2uVYFvQe+pPTScVJeCZSsMo1JCoZN2mMew
# /L4tpqVNbSpWO9QGFwfMEy60HofN6V51sMLMXNTLfhVqs+e8haupWiArSozyAmGH
# /6oMQAh078qRh6wvJNU6gnh5OruCP1QUAvVSu4kqVOcJVozZR5RRb/zPd++PGE3q
# F1P3xWvYViUJLsxtvge/mzA75oBfFZSbdakHJe2BVDGIGVNVjOp8sNt70+kEoMF+
# T6tptMUNlehSR7vM+C13v9+9ZOUKzfRUAYSyyEmYtsnpltD/GWX8eM70ls1V6QG/
# ZOB6b6Yum1HvIiulqJ1Elesj5TMHq8CWT/xrW7twipXTJ5/i5pkU5E16RSBAdOp1
# 2aw8IQhhA/vEbFkEiF2abhuFixUDobZaA0VhqAsMHOmaT3XThZDNi5U2zHKhUs5u
# HHdG6BoQau75KiNbh0c+hatSF+02kULkftARjsyEpHKsF7u5zKRbt5oK5YGwFvgc
# 4pEVUNytmB3BpIiowOIIuDgP5M9WArHYSAR16gc0dP2XdkMEP5eBsX7bf/MGN4K3
# HP50v/01ZHo/Z5lGLvNwQ7XHBx1yomzLP8lx4Q1zZKDyHcp4VQJLu2kWTsKsOqQw
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIFjTCCBHWgAwIBAgIQ
# DpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMjIwODAx
# MDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM
# RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQD
# ExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqGSIb3DQEBAQUAA4IC
# DwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEppz1Yq3aa
# za57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllV
# cq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT
# +CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd
# 463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+
# EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/CNdaSaTC5qmgZ92k
# J7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtmmnTK3kse5w5j
# rubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7
# f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJU
# KSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+wh
# X8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQAB
# o4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDgYDVR0P
# AQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29j
# c3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3J0MEUGA1UdHwQ+MDww
# OqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJ
# RFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRVHSAAMA0GCSqGSIb3DQEBDAUAA4IB
# AQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyhhyzshV6pGrsi+IcaaVQi7aSId229
# GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO0Cre+i1Wz/n096wwepqLsl7Uz9FD
# RJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo8L8vC6bp8jQ87PcDx4eo0kxAGTVG
# amlUsLihVo7spNU96LHc/RzY9HdaXFSMb++hUD38dglohJ9vytsgjTVgHAIDyyCw
# rFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5xaiNrIv8SuFQtJ37YOtnwtoeW/VvR
# XKwYw02fc7cBqZ9Xql4o4rmUMYIDdjCCA3ICAQEwdzBjMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0
# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBAhALrma8Wrp/lYfG
# +ekE4zMEMA0GCWCGSAFlAwQCAQUAoIHRMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B
# CRABBDAcBgkqhkiG9w0BCQUxDxcNMjQxMTA2MTcxNzMzWjArBgsqhkiG9w0BCRAC
# DDEcMBowGDAWBBTb04XuYtvSPnvk9nFIUIck1YZbRTAvBgkqhkiG9w0BCQQxIgQg
# psQnXJFDPOgFoR1SR/aF81pvuvvKQ4uvIZntSz4wvGswNwYLKoZIhvcNAQkQAi8x
# KDAmMCQwIgQgdnafqPJjLx9DCzojMK7WVnX+13PbBdZluQWTmEOPmtswDQYJKoZI
# hvcNAQEBBQAEggIAOp0Zuc4DPyvPlZRz9mxHPv5HhJSDzPeeBpAfwhizLnAeKzm8
# ZbN3KLSevaDbLBfGGgic4jaEfJxDQZWcg2rBWuja0jNx1QLlkkNcob7WyZRPOTPC
# W2ayv2CAk2Vnr3dW87teLmPtL9xqW/ciVeTVwYbuH5aLNmwCeV06O4VzbwkfN88T
# Qka/N4OeY1ohOGI71SIz0ePS60pm3PiYhTqcSNLacxTy8BgEBi0P1rPfpOLp+DHg
# ICWhp1E97wnkr2hJyZFuEUWA48/8OXmyo9eUjTYGk5d6RsbJnGntwsJHLDDVcC4w
# 9Rr621tCDwLmoFSLUo9aOtsL4y7yYOHuZAU+gSUqpnkf2vIvrNmRz+nz7JBAvVIt
# OcrtbFJhlgzojtsyRc+nMJGLAHYSEgafhQqSebxu8f/CazNlu5RCaBu/i+gp3MDC
# xpW9l1cJmStrKlfm4CqhGixkBB6QpoRWtvtj3ORthRBT557hcHgmuYhC8m1hSFlT
# DwvtAScvHRSZLbic/ZdDsV9d/GlB7iCuguPUpth0v11Q18FpWYypuP1+Epkru0pk
# JjqDzWDB2veoloRqIQTm8gnpA7TDxvmeQUzkGB3+mlAsLoJUlKjtUlc6w5N2Xa+v
# wYh+ZjC5bFAnkeuWcg64n5sHtCTc/UvsPbdzYUtr1SdoIfDmz2k1ili09Jg=
# SIG # End signature block