AttachmentCommands.ps1

#requires -Version 5.1


function Copy-KeeperFileAttachment {
    <#
    .Synopsis
    Download record attachments

    .Folder
    Keeper Folder

    .Record
    Keeper Record

    .Parameter Path
    Download folder path
#>


    [CmdletBinding()]
    Param (
        [Parameter(ParameterSetName = 'folder', Mandatory = $true)][string] $Folder,
        [Parameter(ParameterSetName = 'folder')][Switch] $Recursive,
        [Parameter(ParameterSetName = 'record', Mandatory = $true)][string] $Record,
        [Parameter()][string] $Name,
        [Parameter(Position = 0)][string] $Path
    )

    Begin {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault

        if (-not $Path) {
            $Path = '.'
        }
        $records = $null
        if ($Record) {
            $r = Get-KeeperRecord $record
            if ($r) {
                $records = @()
                $records += $r.Uid
            }

        } elseif ($Folder) {
            if ($Recursive.IsPresent) {
                $records = Get-KeeperChildItem $Folder -Recursive -SkipGrouping -ObjectType Record | Select-Object -ExpandProperty "Uid"
            } else {
                $records = Get-KeeperChildItem $Folder -SkipGrouping -ObjectType Record | Select-Object -ExpandProperty "Uid"
            }
        }
        if (-not $records) {
            Write-Error "No records were found" -ErrorAction Stop
        }
    }

    Process {
        if (-not (Test-Path $Path -PathType Container)) {
            New-Item -ItemType Directory -Path $Path | Out-Null
        }
        [KeeperSecurity.Vault.KeeperRecord]$keeperRecord
        [KeeperSecurity.Vault.IAttachment]$atta
        foreach($recordUid in $records) {
            $keeperRecord = Get-KeeperRecord $recordUid
            if (-not $keeperRecord) {
                continue
            }
            foreach ($atta in $vault.RecordAttachments($keeperRecord)) {
                if ($Name) {
                    if (-not (($atta.Name, $atta.Title) -contains $Name)) {
                        continue
                    }
                }
                $fileName = $atta.Id
                if ($atta.Title) {
                    $fileName = $atta.Title
                } elseif ($atta.Name) {
                    $fileName = $atta.Name
                }
                $filePath = Join-Path $path $fileName
                if (Test-Path $filePath -PathType Leaf) {
                    $filePath = Join-Path $path "$($atta.Id) - $fileName"
                    if (Test-Path $filePath -PathType Leaf) {
                        Write-Information -MessageData "File `"$filePath`" already exists"
                        continue
                    }
                }
                Write-Information -MessageData "Downloading `"$fileName`" into `"$filePath`""
                $newFile = New-Item -Name $filePath -ItemType File
                $fileStream = $newFile.OpenWrite()
                try {
                    $vault.DownloadAttachment($keeperRecord, $atta.Id, $fileStream).GetAwaiter().GetResult() | Out-Null
                }
                finally {
                    $fileStream.Dispose()
                }
            }
        }
    }
}
New-Alias -Name kda -Value Copy-KeeperFileAttachment

function Copy-KeeperFileAttachmentToStream {
    <#
    .Synopsis
    Get Attachment as stream

    .Record
    Keeper Record Uid

    .AttachmentName
    Attachment Name

    .Stream
    Attachment will be written to this stream
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)][string] $Record,
        [Parameter()][string] $AttachmentName,
        [Parameter(Position = 0, Mandatory = $true)][System.IO.Stream] $Stream
    )

    $keeperRecord = Get-KeeperRecord $Record
    if ($keeperRecord.Length -ne 1) {
        Write-Error "Record `"$Record`" was not found" -ErrorAction Stop
    }
    [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    $vault.DownloadAttachment($keeperRecord, $AttachmentName, $Stream).GetAwaiter().GetResult() | Out-Null
}

function Copy-FileToKeeperRecord {
    <#
    .Synopsis
    Upload file attachment to a record

    .Record
    Keeper Record Uid

    .Filename
    File path
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)][string] $Record,
        [Parameter(Position = 0, Mandatory = $true)][string] $Filename
    )

    $keeperRecord = Get-KeeperRecord $Record
    if ($null -eq $keeperRecord) {
        $keeperRecord = Get-KeeperRecord -Filter $Record
    }
    if ($null -eq $keeperRecord -or (@($keeperRecord).Count -ne 1)) {
        Write-Error "Record `"$Record`" was not found" -ErrorAction Stop
    }
    $keeperRecord = @($keeperRecord)[0]
    [KeeperSecurity.Vault.VaultOnline]$vault = getVault

    $path = Resolve-Path $Filename -ErrorAction Stop
    $uploadTask  = New-Object -TypeName KeeperSecurity.Vault.FileAttachmentUploadTask -ArgumentList $path.Path, $null

    $vault.UploadAttachment($keeperRecord, $uploadTask).GetAwaiter().GetResult() | Out-Null
}

function Remove-KeeperFileAttachment {
    <#
    .Synopsis
    Remove file attachments from a record

    .Parameter Record
    Keeper Record Uid or Name

    .Parameter FileName
    Attachment filename(s) to delete. Can be used multiple times to delete multiple filenames.

    .Example
    Remove-KeeperFileAttachment -Record "My Record" -FileName "document.pdf"

    .Example
    Remove-KeeperFileAttachment -Record "My Record" -FileName "document.pdf", "image.jpg", "report.docx"

    .Example
    Remove-KeeperFileAttachment -Record "record-uid" -FileName "attachment-id"
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    Param (
        [Parameter(Mandatory = $true)]
        [string] $Record,
        
        [Parameter(Mandatory = $true)]
        [Alias('f')]
        [string[]] $FileName
    )

    Begin {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }

    Process {
        $keeperRecord = Get-KeeperRecord $Record
        if (-not $keeperRecord) {
            Write-Error "Record `"$Record`" was not found" -ErrorAction Stop
        }

        $attachments = @($vault.RecordAttachments($keeperRecord))
        
        if ($attachments.Count -eq 0) {
            Write-Warning "Record `"$($keeperRecord.Title)`" has no attachments"
            return
        }

        $allAttachmentsToDelete = @()
        $notFoundFiles = @()

        foreach ($file in $FileName) {
            $matchingAttachments = $attachments | Where-Object {
                ($_.Id -eq $file) -or
                ($_.Title -eq $file) -or
                ($_.Name -eq $file) -or
                ($_.Title.ToLower().Trim() -eq $file.ToLower().Trim()) -or
                ($_.Name.ToLower().Trim() -eq $file.ToLower().Trim())
            }

            if (-not $matchingAttachments) {
                $notFoundFiles += $file
            }
            else {
                Write-Host "Found $($matchingAttachments.Count) attachment(s) matching '$file'"
                $allAttachmentsToDelete += $matchingAttachments
            }
        }

        if ($allAttachmentsToDelete.Count -eq 0) {
            Write-Warning "No matching attachments found to delete."
            if ($notFoundFiles.Count -gt 0) {
                Write-Host "Files not found: $($notFoundFiles -join ', ')" -ForegroundColor Yellow
                Write-Host "Available attachments:"
                foreach ($attachment in $attachments) {
                    $displayName = if ($attachment.Title) { $attachment.Title } elseif ($attachment.Name) { $attachment.Name } else { $attachment.Id }
                    Write-Host " - $displayName (ID: $($attachment.Id))"
                }
            }
            return
        }

        $confirmMessage = "Delete $($allAttachmentsToDelete.Count) attachment(s) from record '$($keeperRecord.Title)'"
        
        if ($PSCmdlet.ShouldProcess($confirmMessage, "Remove Attachments")) {
            $deletedCount = 0
            $failedCount = 0

            foreach ($attachment in $allAttachmentsToDelete) {
                $displayName = if ($attachment.Title) { $attachment.Title } 
                              elseif ($attachment.Name) { $attachment.Name } 
                              else { $attachment.Id }

                try {
                    $success = $vault.DeleteAttachment($keeperRecord, $attachment.Id).GetAwaiter().GetResult()
                    
                    if ($success) {
                        Write-Host "Deleted '$displayName' (ID: $($attachment.Id))" -ForegroundColor Green
                        $deletedCount++
                    }
                    else {
                        Write-Host "Failed to delete '$displayName' (ID: $($attachment.Id))" -ForegroundColor Red
                        $failedCount++
                    }
                }
                catch {
                    Write-Host "Error deleting '$displayName': $($_.Exception.Message)" -ForegroundColor Red
                    $failedCount++
                }
            }

            Write-Host "Summary: $deletedCount deleted, $failedCount failed" -ForegroundColor Cyan
            
            if ($notFoundFiles.Count -gt 0) {
                Write-Host "Files not found: $($notFoundFiles -join ', ')" -ForegroundColor Yellow
                Write-Host "Available attachments:"
                foreach ($attachment in $attachments) {
                    $displayName = if ($attachment.Title) { $attachment.Title } elseif ($attachment.Name) { $attachment.Name } else { $attachment.Id }
                    Write-Host " - $displayName (ID: $($att.Id))"
                }
            }
        }
    }
}
New-Alias -Name krfa -Value Remove-KeeperFileAttachment

function Get-KeeperFileReport {
    <#
    .SYNOPSIS
    List records with file attachments.

    .DESCRIPTION
    Generates a report of all records in the vault that have file attachments.
    Supports both legacy PasswordRecord (v2) and modern TypedRecord (v3) with fileRef fields.
    Optionally tests download accessibility for each attachment using HTTP Range requests.

    .PARAMETER TryDownload
    Try downloading every attachment you have access to. Tests accessibility by making
    an HTTP Range request (bytes=0-1) for each file and reports OK or the HTTP status code.

    .PARAMETER Format
    Output format: table (default), csv, json.

    .PARAMETER Output
    Export report results to a file path.

    .EXAMPLE
    Get-KeeperFileReport
    List all records with file attachments in table format.

    .EXAMPLE
    Get-KeeperFileReport -TryDownload
    List all file attachments and verify each one is downloadable.

    .EXAMPLE
    Get-KeeperFileReport -Format csv -Output "file_report.csv"
    Export the file attachment report to a CSV file.

    .EXAMPLE
    Get-KeeperFileReport -Format json
    Output the file attachment report as JSON.

    .EXAMPLE
    Get-KeeperFileReport -TryDownload -Format csv -Output "downloads.csv"
    Verify download accessibility and export results to CSV.
    #>

    [CmdletBinding()]
    Param (
        [Alias('dl')]
        [switch] $TryDownload,

        [ValidateSet('table', 'csv', 'json')]
        [string] $Format = 'table',

        [string] $Output
    )

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }
    catch {
        Write-Error "Failed to connect to vault"
        return
    }

    $options = New-Object KeeperSecurity.Vault.FileReportOptions
    $options.TryDownload = $TryDownload.IsPresent

    if ($TryDownload.IsPresent) {
        Write-Host "Scanning vault for file attachments and verifying download accessibility..."
    }
    else {
        Write-Host "Scanning vault for file attachments..."
    }

    try {
        $report = [KeeperSecurity.Vault.KeeperFileReport]::GenerateFileReport($vault, $options, $null).GetAwaiter().GetResult()
    }
    catch {
        Write-Error "Failed to generate file report: $($_.Exception.Message)"
        return
    }

    if ($report.Count -eq 0) {
        Write-Host "No records with file attachments found."
        return
    }

    $result = [System.Collections.Generic.List[PSCustomObject]]::new()
    foreach ($item in $report) {
        $row = [ordered]@{
            'Title'       = (if ($null -ne $item.RecordTitle) { $item.RecordTitle } else { '' })
            'Record UID'  = (if ($null -ne $item.RecordUid) { $item.RecordUid } else { '' })
            'Record Type' = (if ($null -ne $item.RecordType) { $item.RecordType } else { '' })
            'File ID'     = (if ($null -ne $item.FileId) { $item.FileId } else { '' })
            'File Name'   = (if ($null -ne $item.FileName) { $item.FileName } else { '' })
            'File Size'   = $item.FileSize
        }
        if ($TryDownload.IsPresent) {
            $row['Downloadable'] = (if ($null -ne $item.Downloadable) { $item.Downloadable } else { '' })
        }
        $result.Add([PSCustomObject]$row)
    }

    if ($Output) {
        switch ($Format) {
            'json' { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 }
            'csv'  { $result | Export-Csv -Path $Output -NoTypeInformation -Encoding utf8 }
            default { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 }
        }
        Write-Host "Report exported to $Output ($($result.Count) file(s) found)"
    }
    else {
        switch ($Format) {
            'json' { $result | ConvertTo-Json -Depth 5 }
            'csv'  { $result | ConvertTo-Csv -NoTypeInformation }
            default {
                Write-Host ""
                $result | Format-Table -AutoSize
            }
        }
    }
}
New-Alias -Name file-report -Value Get-KeeperFileReport
# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBRm9tzplhwJ8va
# UaZ+YHwHChRm/v6318t8fO9EVBEJY6CCITswggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg
# MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit
# eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS
# 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM
# swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC
# Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3
# /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j
# q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5
# OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo
# 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU
# tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm
# KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP
# TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq
# hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK
# r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda
# qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+
# lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a
# brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS
# y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK
# iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb
# KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q
# xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm
# zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn
# HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w
# gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1
# c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo
# dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi
# 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg
# xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF
# cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ
# m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS
# GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1
# ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9
# MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7
# Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG
# RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6
# X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd
# BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx
# XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF
# BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
# aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo
# dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy
# bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL
# BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj
# aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0
# hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0
# F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT
# mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf
# ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE
# wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh
# OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX
# gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO
# LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG
# WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg
# AwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG
# EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0
# IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex
# MB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMx
# FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy
# NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZI
# hvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3
# zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8Tch
# TySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWj
# FDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2Uo
# yrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjP
# KHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KS
# uNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7w
# JNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vW
# doUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOg
# rY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K
# 096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCf
# gPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zy
# Me39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL
# TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG
# AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
# b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp
# Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy
# dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j
# cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB
# CwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZ
# D9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/
# ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu
# +WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4o
# bEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2h
# ECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasn
# M9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol
# /DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgY
# xQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3oc
# CVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcB
# ZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB0kwggUx
# oAMCAQICEAe0P3SLJmcoVNrErUyxTt0wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE
# BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy
# dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB
# MTAeFw0yNTEyMzEwMDAwMDBaFw0yOTAxMDIyMzU5NTlaMIHRMRMwEQYLKwYBBAGC
# NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ
# cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC
# VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK
# ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5
# IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUcNMoSVmxAi0a
# vG+StFJMNFFTUIOo3HdBZ+0gqA1XpNgUx11vB1vCZrvFsD9m5oA58tdp4gZN3LmQ
# aMvCl2ANUT7MilI02Hf1RWlygBzon6iE0GpU3lgRrwrk1dhtLpGsR6dbMKUUHprc
# vKpXk90/VN+vhzY1uik1tCTxkDCPu/AYJg7m9+tR2KqvMuYMaMLhii66eWUAGsBC
# h/uZxjkGoJF6qZ0DgFd7rW7VYljbfYSNPeZNGTDgB0J/wOsKl0mn612DTseIvAKt
# 4vra/FLFukyEyStnfQ8lWYDcLLCMCjNVrzGipmT5E2iyx7Y1RZCIpNwVogp3Ixbk
# Gbq5A/41YNOLLd4cFewyB2F037RevBCRsUODZEt1qBf7Jbu3DiYo1G+zTj9E0R1s
# FzyijcfdsTm6X5ble+yCJeGkX5XgsyPnZpyz/FX9Fr0N9pMPGWwW2PKyHEnSytXm
# 0Dxdq2P4mA4CBUxq7YoV26L2PF6QEh9BQdXTPcnLysUv7SI/a0ECAwEAAaOCAgIw
# ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRG
# 4H6CH8pvNX632bsdnrda4MtJLDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB
# BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC
# B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p
# bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI
# QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT
# QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA
# A4ICAQA1Wlq0WzJa3N6DgjgBU7nagIJBab1prPARXZreX1MOv9VjnS5o0CrfQLr6
# z3bmWHw7xT8dt6bcSwRixqvPJtv4q8Rvo80O3eUMvMxQzqmi7z1zf+HG+/3G4F+2
# IYegvPc8Ui151XCV9rjA8tvFWRLRMX0ZRxY1zfT027HMw0iYL20z44+Cky//FAnL
# iRwoNDGiRkZiHbB9YOftPAYNMG3gm1z3zOW5RdfKPrqvMuijE+dfyLIAA6Immpzu
# FMH+Wgn8NnSlot9b4YKycaqqdjd7wXDjPub/oQ7VShuCSBWj+UNOTVh0vcZGackc
# H1DLVgwp2dcKlxJiQKtkHT/T6LloY6LTe6+8wkVkr8EAv1W+q/+M1a4Ao+ykFbIA
# 2LBEmA9qdgoLtenAYIiEg+48SjMPgyBbVPE3bhL1vIqjEIxYCfdmi6wx33oYX7HB
# +bJ7zitHw4GgtpfPV8y8QRZImKmeDOKyXjQPDmQM/Eglm/Ns0GzBkVXM8h6UI34b
# WZrHz9sbLSE20m5Svmxftvw5zju+I3WsmS/stNfWlOkwU0niUgwPHaz21kjXEA5A
# g+aqv26wodqZcnGOlChoWDvSJ8KKgdOFbeAYKAMp1NY7iWV315zpGH19RipCR1NH
# 0ND8iIubk3WGNf2rzEfqlOi3h2ywqVkU6AKXHdO5JV4otSKKEDGCBdkwggXVAgEB
# MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD
# VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI
# QTM4NCAyMDIxIENBMQIQB7Q/dIsmZyhU2sStTLFO3TANBglghkgBZQMEAgEFAKCB
# hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE
# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ
# BDEiBCD3RaR4yoHqAkCUIAxh9FTKCdibAuz9J+E9OpaUWN+EnzANBgkqhkiG9w0B
# AQEFAASCAYAjb/ZmNWVP3XKCkoyKR4ZHgHvWMuB3J85kIN4bzgsw0Nr3ox14y7dC
# x9pR40nzQk2RAaKBS+ovOAd3XLiIOTbulgXPgWdBsqvAa6ibGcKsJaoOLG/herFN
# Me6PfxBHeehriDCGJqe65Fpv0IrBts33tYhsWvQ++9z6v2aywtlkqLu+2Dj4W6/j
# lwiQvSCHpUIx1Oh0iLkOf6I5kuLJJy8tBZc5GX+4FnS+wOZ/o6pnrdxAeXUXHHBe
# VsnC+0WkOEOenCCfA9gKU7MCx2F/ET7BDRn6JQCfS9Tci5edbQ8S69VmzvohtZMb
# P6Zg19nVYjy9tRp5esjV2XGZQe6ZfSpgvJ0n4xz0QJjppo0F4q0Yd4ah/u0rXAUS
# SCCNTL8e1P0eS/vqUE2BBYYzJSpcZ0Reeuk59qYkHjv0PD4Cr+IhfjW8xYsA950a
# nhNOcGtjOlCxRvAo1Qrm1a2iYRmjQoK0SZGxMN73RADZzdrilQK+6K9PuPkNjWMG
# vpcffGkhfgahggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwMjI1MjIzNzQ1WjAv
# BgkqhkiG9w0BCQQxIgQgi8X3LP2O9cSeqOv1/uRl5w3Yr491yuIA80Vq6Eljup8w
# DQYJKoZIhvcNAQEBBQAEggIAHhAMYN+wPHdE2PvGSuJ0LNKkBnjQmnvJun3RJvOe
# 07tIHeB2ueZ1ESwCK7/9JlH2vCE6PFH2gVgCpu4W6e5q1ZPm8dsmLYk4SqskukvW
# kIp4HLhbNcri87Mnf8QftF6XfW5m42NpnTiABvWvV0dZHLAgFb/oLSX5CAUWrUVK
# G53s1YXAezmqi+R3qBj3YQGc7y/7qB/TIvtI2F1xHuQw8qW9eQrtJyl46Fw4FY+r
# 3S7KfJjtQqdrNs1VXelolOxf2EadJbgZrZmrdWR8EYu2MsGJ1Mg89DjlYtm9dVWp
# 1jpI/jZjk+XWBhx4ar/JtwcNfPpAV/p8g+1MVJdWEJ/VsfpLa1YsGUASRQnzRQTW
# MmJ6NtW7DWvh8ZjdDkIclsWKTa4ZvWFTe2iS8xQQRkD3GoaA4ipsKQH1/PcPdM7Q
# JXZbb5j+YnK/UxQodOmvqL1SOsWTrzFj+QYZGn5Kz700xESCAMql7Pg/Fh89L9eW
# Ys631N6PIAS43ZVTwwVKjH/JyZsEPnLy9cWuLnSOtg+gOO36MTUdO8LpVLMbqR5N
# JnUq43Io7JPXxCm4trumpy+rAwi6Lxg4jwNuhoV0Q0sotf1Q+pefZqZUs0n2XCem
# 2SC2JaspEPhDt88AxZONFGdH0HFLxUTyNPsdpz7S5A8vflWUato95OAD3+WDEcBf
# CV0=
# SIG # End signature block