InternalSmtpClient.psm1

using module .\Logger.psm1
Add-Type -AssemblyName System.IO
Add-Type -AssemblyName System.Net

class InternalSmtpClient {
    hidden [System.Net.Sockets.TcpClient]$TcpClient
    hidden [int]$TimeoutMs
    hidden [System.IO.StreamReader]$Reader
    hidden [System.IO.StreamWriter]$Writer
    hidden [string[]]$SessionCapabilities
    hidden [string]$LastSmtpResponse
    hidden [Logger]$Logger

    [int]$TimeoutSec = 60
    [int]$ResponseCode = -1

    InternalSmtpClient ([Logger]$logger) {
        $this.Logger = $logger
    }

    [void] Connect([string]$smtpServer, [int]$port, [bool]$useSsl, [bool]$acceptUntrustedCertificates, [System.Security.Cryptography.X509Certificates.X509Certificate]$clientCertificate, $enabledSslProtocols) {
        [bool]$useClientCert = $false
        $this.TimeoutMs = $this.TimeoutSec * 1000

        if ($null -ne $clientCertificate) {
            $useClientCert = $true
        }

        $this.logger.LogMessage("# Tcp Client Timeout Settings", "Information", $true, $true)
        $this.logger.LogMessage("TcpClientReceiveTimeOut: $($this.TimeoutSec)s", "Information", $true, $true)
        $this.logger.LogMessage("TcpClientSendTimeOut: $($this.TimeoutSec)s", "Information", $true, $true)
        $this.logger.LogMessage("", "Information", $true, $true)
        $this.logger.LogMessage("[Connecting to $SmtpServer" + ":$Port]", "Information", "Yellow", $false, $true)

        $this.TcpClient = New-Object -TypeName System.Net.Sockets.TcpClient
        $this.TcpClient.ReceiveTimeout = $this.TimeoutMs
        $this.TcpClient.SendTimeout = $this.TimeoutMs

        $result = $this.TcpClient.BeginConnect($SmtpServer, $Port, $null, $null)
        $result.AsyncWaitHandle.WaitOne($this.TimeoutMs) | Out-Null
        $this.TcpClient.EndConnect($result)

        if (-not $this.TcpClient.Connected) {
            # If not connected exception *should* be thrown in EndConnect()
            $this.Logger.LogError("Connection to remote host $($SmtpServer + ":$Port") failed!")
        }

        $this.Logger.LogMessage("[Connected]", "Information", "Green", $false, $true)

        $this.Reader = New-Object -TypeName System.IO.StreamReader -ArgumentList ($this.TcpClient.GetStream(), [System.Text.Encoding]::ASCII)
        $this.Writer = New-Object -TypeName System.IO.StreamWriter -ArgumentList ($this.TcpClient.GetStream(), [System.Text.Encoding]::ASCII)

        $this.ReadResponse($false)
        $this.SendEhlo()

        if ($useSsl) {
            $this.Logger.LogMessage("Starting TLS...", "Verbose", $false, $true)
            if ($this.SessionCapabilities.Contains("STARTTLS")) {
                $this.SmtpCmd("STARTTLS")
                if ($this.ResponseCode -eq 220) {
                    $this.Logger.LogMessage("* Starting TLS negotiation")
                    if ($AcceptUntrustedCertificates) {
                        $this.Logger.LogMessage("Ignoring certificate validation results.", "Verbose", $false, $true)
                        $sslstream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList ($this.TcpClient.GetStream(), $false, ({ $true } -as [Net.Security.RemoteCertificateValidationCallback]))
                    }
                    else {
                        $sslstream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList ($this.TcpClient.GetStream())
                    }
                    $certcol = $null

                    if ($useClientCert) {
                        $certcol = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509CertificateCollection
                        $certcol.Add($clientCertificate)
                    }
                    <#
                        If enabledSslProtocols is null, don't specify an ssl protocol to use.
                        AuthenticateAsClient will use the OS preferred TLS version
                        This avoids ArgumentException when using NONE and OS settings disable default tls versions.
 
                        https://referencesource.microsoft.com/#System/net/System/Net/SecureProtocols/_SslState.cs,164
                    #>

                    try {
                        if ($null -eq $enabledSslProtocols) {
                            $sslstream.AuthenticateAsClient($SmtpServer, $certcol, $true)
                        }
                        else {
                            $sslstream.AuthenticateAsClient($SmtpServer, $certcol, $enabledSslProtocols, $true)
                        }
                    }
                    # Catch TLS exceptions to provide better guidance and rethrow
                    catch {
                        $this.Logger.LogMessage("* .NET Service Point Manager TLS Setting: $([System.Net.ServicePointManager]::SecurityProtocol)", "Warning", $false, $true)
                        $this.Logger.LogMessage("* TLS authentication failed. Try using an alternate TLS version using the TlsVersion parameter (e.g., -TlsVersion tls12).", "Warning", $false, $true)
                        throw $_
                    }

                    $this.Writer = New-Object -TypeName System.IO.StreamWriter -ArgumentList ($sslstream, [System.Text.Encoding]::ASCII)
                    $this.Reader = New-Object -TypeName System.IO.StreamReader -ArgumentList ($sslstream, [System.Text.Encoding]::ASCII)

                    $this.Logger.LogMessage("* TLS negotiation completed. IgnoreCertValidation:$AcceptUntrustedCertificates CipherAlgorithm:$($sslstream.CipherAlgorithm) HashAlgorithm:$($sslstream.HashAlgorithm) TlsVersion:$($sslstream.SslProtocol)")
                    $this.Logger.LogMessage("* RemoteCertificate: '<S>$($sslstream.RemoteCertificate.Subject)<I>$($sslstream.RemoteCertificate.Issuer)' NotBefore:$($sslstream.RemoteCertificate.GetEffectiveDateString()) NotAfter:$($sslstream.RemoteCertificate.GetExpirationDateString())")
                    if ($useClientCert) {
                        $this.Logger.LogMessage("* ClientCertificate: '<S>$($sslstream.LocalCertificate.Subject)<I>$($sslstream.LocalCertificate.Issuer)' NotBefore:$($sslstream.LocalCertificate.GetEffectiveDateString()) NotAfter:$($sslstream.LocalCertificate.GetExpirationDateString())")
                    }

                    # Warn if using unsupported versions of TLS
                    # Intentionally not setting TLS to 1.2 or greater to expose default TLS behavior
                    if ($sslstream.SslProtocol -eq "Tls" -or $sslstream.SslProtocol -eq "Tls11") {
                        Write-Warning "TLS version is either 1.0 or 1.1. Consider enabling TLS 1.2 or greater."
                    }

                    $rawCert = "`n-----BEGIN CERTIFICATE-----"
                    $rawCert += "`n" + [Convert]::ToBase64String($sslstream.RemoteCertificate.GetRawCertData(), [System.Base64FormattingOptions]::InsertLineBreaks)
                    $rawCert += "`n-----END CERTIFICATE----- "

                    $this.Logger.LogMessage("Remote Certificate:", "Verbose", $false, $true)
                    $this.Logger.LogMessage($rawCert, "Verbose", $false, $true)

                    # Rediscover session capabilities
                    $this.SendEhlo()
                }
                else {
                    throw "Failed to start tls session with remote host."
                }
            }
            # STARTTLS verb not found
            else {
                throw "Session capabilities do not support STARTTLS."
            }
        }
    }

    [void] ReadResponse([bool]$readAllLines) {
        $this.ResponseCode = - 1
        $resp = @()

        # Bail if not connected
        if (-not $this.TcpClient.Connected) {
            throw "Client is not connected."
        }

        $line = $this.Reader.ReadLine()
        $resp += $line

        # Parse response code
        $this.ResponseCode = [System.Int32]::Parse($line.Substring(0, 3))

        if ($readAllLines){
            while ($line -like "250-*") {
                Write-Debug "StreamReader: Reading more lines..."
                $line = $this.Reader.ReadLine()
                # Truncate response code and join all server capabilities
                $resp += $line.Substring(4)
            }
            $resp = $resp -join ','
        }

        $this.LastSmtpResponse = $resp
        $this.Logger.LogMessage("< " + $resp)
    }

    [void] SendEhlo() {
        $this.SmtpCmd("EHLO " + ([System.Environment]::MachineName))

        if ($this.ResponseCode -ne 250) {
            throw "Unexpected response on EHLO command. Response Code:$($this.ResponseCode)"
        }

        foreach ($c in $this.LastSmtpResponse.Split(',') | Where-Object { ($_) -and ($_ -notcontains "250") }){
            $this.SessionCapabilities += $c.Replace("250-", "").Trim()
        }
    }

    [void] SmtpCmd([string]$command) {
        $this.SmtpCmd($command, $false)
    }

    [void] SmtpCmd([string]$command, [bool]$redactCmd) {
        if ($redactCmd) {
            $this.Logger.LogMessage("Sending command: *REDACTED*", "Verbose", $false, $true)
            $this.Logger.LogMessage("> *REDACTED*")
        }
        else {
            $this.Logger.LogMessage("Sending command: $command", "Verbose", $false, $true)
            $this.Logger.LogMessage("> " + $command)
        }

        $this.Writer.WriteLine($command)
        $this.Writer.Flush()

        if (($command -ne "QUIT") -and (!$command.StartsWith("BDAT"))) {
            $this.ReadResponse($false)
        }

        if ($command -like "EHLO *") {
            $this.ReadResponse($true)
        }
    }

    [bool] ContainsCapability([string] $c) {
        foreach ($cap in $this.SessionCapabilities) {
            if ($cap.ToLower().Contains($c.ToLower())) {
                return $true
            }
        }

        return $false
    }

    [bool] AuthLogin([pscredential]$Credential) {
        if ($this.ContainsCapability("AUTH LOGIN")) {
            $this.SmtpCmd("AUTH LOGIN")

            if ($this.ResponseCode -eq 334) {
                $message = [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($this.LastSmtpResponse.Substring(4))).ToLower()
                if ($message -eq "username:") {
                    $this.SmtpCmd([System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($Credential.UserName)))
                }

                # Decode the response
                $message = [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($this.LastSmtpResponse.Substring(4))).ToLower()

                # If username accepted continue
                if (($this.ResponseCode -eq 334) -and ($message -eq "password:")) {
                    $this.SmtpCmd([System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($Credential.GetNetworkCredential().Password)), $true)
                }
                else {
                    $this.Logger.LogError("SMTP Authentication Failed. Invalid user name.")
                    return $false
                }
                if ($this.ResponseCode -eq 535 -and $($this.LastSmtpResponse.Substring(4)).StartsWith("5.7.139")) {
                    $this.Logger.LogError("SMTP Authentication Failed. Basic authentication disabled or blocked by policy.")
                    return $false
                }
                if ($this.ResponseCode -eq 421) {
                    $this.Logger.LogError("SMTP Authentication Failed. Check your TLS version is at least 1.2.")
                    return $false
                }
                if ($this.ResponseCode -ne 235) {
                    $this.Logger.LogError("SMTP Authentication Failed. Check user name and password.")
                    return $false
                }

                return $true
            }
            else {
                $this.Logger.LogError("Unexpected response code on AUTH LOGIN.")
            }
        }
        else {
            $this.Logger.LogError("Session capabilities do not support AUTH LOGIN")
        }
        return $false
    }

    [bool] XOAUTH2Login([string]$userName, [string]$token) {
        if ([System.String]::IsNullOrEmpty($token)) {
            $this.Logger.LogError("AccessToken is null or empty")
            return $false
        }
        # Build the auth blob
        $authBlob = "user=" + $UserName + "$([char]0x01)auth=Bearer " + $token + "$([char]0x01)$([char]0x01)"

        if ($this.ContainsCapability("XOAUTH2")) {
            $this.SmtpCmd("AUTH XOAUTH2")

            if ($this.ResponseCode -eq 334) {
                $this.SmtpCmd([System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($authBlob)))

                if ($this.ResponseCode -eq 421) {
                    $this.Logger.LogError("SMTP Authentication Failed. Check your TLS version is at least 1.2.")
                    return $false
                }
                if ($this.ResponseCode -eq 235) {
                    return $true
                }
                else {
                    $this.Logger.LogError("SMTP Authentication Failed. Check your OAUTH token is valid.")
                    return $false
                }
            }
            else {
                $this.Logger.LogError("Unexpected response code on AUTH XOAUTH2.")
                return $false
            }
        }
        else {
            $this.Logger.LogError("Session capabilities do not support AUTH XOAUTH2.")
            return $false
        }
    }

    [void] SendMail([string]$From, [string]$To) {
        $this.SmtpCmd("MAIL FROM: <$From>")
        if ($this.ResponseCode -ne 250) {
            $this.Logger.LogError("Unexpected response code on MAIL FROM command.")
            $this.SmtpCmd("QUIT")
            return
        }

        $this.SmtpCmd("RCPT TO: <$To>")
        if ($this.ResponseCode -ne 250) {
            $this.Logger.LogError("Unexpected response code on RCPT TO command.")
            $this.SmtpCmd("QUIT")
            return
        }

        $message = "From: `"$From`" <$From>"
        $message += "`nTo: `"$To`" <$To>"
        $message += "`nSubject: SMTP Client Submission Test"
        $message += "`nContent-Type: text/plain"
        $message += "`n"
        $message += "`nThis is a test message."

        # Check if server supports CHUNKING and send using BDAT command
        if ($this.ContainsCapability("CHUNKING")) {
            $this.SendContentUsingBdat($message)
        }
        # If capability not found send using DATA command
        else {
            $this.SendContentUsingData($message)
        }
        $this.ReadResponse($false)

        if ($this.ResponseCode -eq 430 -and $($this.LastSmtpResponse.Substring(4)).StartsWith("4.2.0 STOREDRV; mailbox logon failure;")) {
            $this.Logger.LogError("Failed to submit message. Verify that the authenticated user or application has the correct permission to logon to the mailbox.")
        }
        elseif ($this.ResponseCode -ne 250) {
            $this.Logger.LogError("Failed to submit message.")
        }

        $this.SmtpCmd("QUIT")
    }

    [void] SendContentUsingBdat($message)
    {
        $byteCount = [System.Text.Encoding]::ASCII.GetByteCount($message)
        $command = "BDAT $byteCount LAST"

        $this.SmtpCmd($command)
        $this.Logger.LogMessage("Writing message to stream...", "Verbose", $false, $true)
        $this.Writer.Write($message)
        $this.Writer.Flush()
    }

    [void] SendContentUsingData($message)
    {
        $command = "DATA"

        $this.SmtpCmd($command)
        $this.Logger.LogMessage("Writing message to stream...", "Verbose", $false, $true)
        $this.Writer.Write($message)
        $this.Logger.LogMessage("Writing terminator '<CRLF>.<CRLF>' to stream.", "Verbose", $false, $true)
        $this.Writer.Write("`r`n.`r`n")
        $this.Writer.Flush()
    }

    [void] DisposeResources() {
        if ($this.Writer) {
            $this.Writer.Dispose()
        }
        if ($this.Reader) {
            $this.Reader.Dispose()
        }
        if ($this.TcpClient) {
            $this.TcpClient.Dispose()
        }
    }
}

# SIG # Begin signature block
# MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUmLD+YJzOoa0srFvnw+lDEMch
# p9GggiFMMIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0B
# AQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk
# IElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw
# ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz
# 7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS
# 5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7
# bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfI
# SKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jH
# trHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14
# Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2
# h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt
# 6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPR
# iQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ER
# ElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4K
# Jpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAd
# BgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SS
# y4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAk
# BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAC
# hjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURS
# b290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRV
# HSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyh
# hyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO
# 0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo
# 8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++h
# UD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5x
# aiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMIIGsDCCBJig
# AwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG
# EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
# cnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEw
# NDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQg
# Q29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7
# xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAk
# ZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6
# mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4Q
# vRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSl
# rzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7
# CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiD
# NipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy7
# 40hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110
# YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOF
# ECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLM
# bDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYE
# FGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/n
# upiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3Bggr
# BgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNv
# bTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lD
# ZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2Ny
# bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0g
# BBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2N
# CHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6
# XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9
# oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjV
# Gv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gz
# yE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m
# 6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG
# 1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsTo
# FpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TO
# HnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampA
# MEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPt
# hkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIGtDCCBJygAwIBAgIQDcesVwX/
# IZkuQEMiDDpJhjANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UE
# ChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYD
# VQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjUwNTA3MDAwMDAwWhcN
# MzgwMTE0MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs
# IEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5n
# IFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
# MIICCgKCAgEAtHgx0wqYQXK+PEbAHKx126NGaHS0URedTa2NDZS1mZaDLFTtQ2oR
# jzUXMmxCqvkbsDpz4aH+qbxeLho8I6jY3xL1IusLopuW2qftJYJaDNs1+JH7Z+Qd
# SKWM06qchUP+AbdJgMQB3h2DZ0Mal5kYp77jYMVQXSZH++0trj6Ao+xh/AS7sQRu
# QL37QXbDhAktVJMQbzIBHYJBYgzWIjk8eDrYhXDEpKk7RdoX0M980EpLtlrNyHw0
# Xm+nt5pnYJU3Gmq6bNMI1I7Gb5IBZK4ivbVCiZv7PNBYqHEpNVWC2ZQ8BbfnFRQV
# ESYOszFI2Wv82wnJRfN20VRS3hpLgIR4hjzL0hpoYGk81coWJ+KdPvMvaB0WkE/2
# qHxJ0ucS638ZxqU14lDnki7CcoKCz6eum5A19WZQHkqUJfdkDjHkccpL6uoG8pbF
# 0LJAQQZxst7VvwDDjAmSFTUms+wV/FbWBqi7fTJnjq3hj0XbQcd8hjj/q8d6ylgx
# CZSKi17yVp2NL+cnT6Toy+rN+nM8M7LnLqCrO2JP3oW//1sfuZDKiDEb1AQ8es9X
# r/u6bDTnYCTKIsDq1BtmXUqEG1NqzJKS4kOmxkYp2WyODi7vQTCBZtVFJfVZ3j7O
# gWmnhFr4yUozZtqgPrHRVHhGNKlYzyjlroPxul+bgIspzOwbtmsgY1MCAwEAAaOC
# AV0wggFZMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO9vU0rp5AZ8esri
# kFb2L9RJ7MtOMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1Ud
# DwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkw
# JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcw
# AoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJv
# b3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwB
# BAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQAXzvsWgBz+Bz0RdnEw
# vb4LyLU0pn/N0IfFiBowf0/Dm1wGc/Do7oVMY2mhXZXjDNJQa8j00DNqhCT3t+s8
# G0iP5kvN2n7Jd2E4/iEIUBO41P5F448rSYJ59Ib61eoalhnd6ywFLerycvZTAz40
# y8S4F3/a+Z1jEMK/DMm/axFSgoR8n6c3nuZB9BfBwAQYK9FHaoq2e26MHvVY9gCD
# A/JYsq7pGdogP8HRtrYfctSLANEBfHU16r3J05qX3kId+ZOczgj5kjatVB+NdADV
# ZKON/gnZruMvNYY2o1f4MXRJDMdTSlOLh0HCn2cQLwQCqjFbqrXuvTPSegOOzr4E
# Wj7PtspIHBldNE2K9i697cvaiIo2p61Ed2p8xMJb82Yosn0z4y25xUbI7GIN/TpV
# fHIqQ6Ku/qjTY6hc3hsXMrS+U0yy+GWqAXam4ToWd2UQ1KYT70kZjE4YtL8Pbzg0
# c1ugMZyZZd/BdHLiRu7hAWE6bTEm4XYRkA6Tl4KSFLFk43esaUeqGkH/wyW4N7Oi
# gizwJWeukcyIPbAvjSabnf7+Pu0VrFgoiovRDiyx3zEdmcif/sYQsfch28bZeUz2
# rtY/9TCA6TD8dC3JE3rYkrhLULy7Dc90G6e8BlqmyIjlgp2+VqsS9/wQD7yFylIz
# 0scmbKvFoW2jNrbM1pD2T7m3XDCCBu0wggTVoAMCAQICEAqA7xhLjfEFgtHEdqeV
# dGgwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lD
# ZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFt
# cGluZyBSU0E0MDk2IFNIQTI1NiAyMDI1IENBMTAeFw0yNTA2MDQwMDAwMDBaFw0z
# NjA5MDMyMzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgU0hBMjU2IFJTQTQwOTYgVGltZXN0YW1w
# IFJlc3BvbmRlciAyMDI1IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDQRqwtEsae0OquYFazK1e6b1H/hnAKAd/KN8wZQjBjMqiZ3xTWcfsLwOvRxUwX
# cGx8AUjni6bz52fGTfr6PHRNv6T7zsf1Y/E3IU8kgNkeECqVQ+3bzWYesFtkepEr
# vUSbf+EIYLkrLKd6qJnuzK8Vcn0DvbDMemQFoxQ2Dsw4vEjoT1FpS54dNApZfKY6
# 1HAldytxNM89PZXUP/5wWWURK+IfxiOg8W9lKMqzdIo7VA1R0V3Zp3DjjANwqAf4
# lEkTlCDQ0/fKJLKLkzGBTpx6EYevvOi7XOc4zyh1uSqgr6UnbksIcFJqLbkIXIPb
# cNmA98Oskkkrvt6lPAw/p4oDSRZreiwB7x9ykrjS6GS3NR39iTTFS+ENTqW8m6TH
# uOmHHjQNC3zbJ6nJ6SXiLSvw4Smz8U07hqF+8CTXaETkVWz0dVVZw7knh1WZXOLH
# gDvundrAtuvz0D3T+dYaNcwafsVCGZKUhQPL1naFKBy1p6llN3QgshRta6Eq4B40
# h5avMcpi54wm0i2ePZD5pPIssoszQyF4//3DoK2O65Uck5Wggn8O2klETsJ7u8xE
# ehGifgJYi+6I03UuT1j7FnrqVrOzaQoVJOeeStPeldYRNMmSF3voIgMFtNGh86w3
# ISHNm0IaadCKCkUe2LnwJKa8TIlwCUNVwppwn4D3/Pt5pwIDAQABo4IBlTCCAZEw
# DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU5Dv88jHt/f3X85FxYxlQQ89hjOgwHwYD
# VR0jBBgwFoAU729TSunkBnx6yuKQVvYv1Ensy04wDgYDVR0PAQH/BAQDAgeAMBYG
# A1UdJQEB/wQMMAoGCCsGAQUFBwMIMIGVBggrBgEFBQcBAQSBiDCBhTAkBggrBgEF
# BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMF0GCCsGAQUFBzAChlFodHRw
# Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3Rh
# bXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5jcnQwXwYDVR0fBFgwVjBUoFKgUIZO
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0
# YW1waW5nUlNBNDA5NlNIQTI1NjIwMjVDQTEuY3JsMCAGA1UdIAQZMBcwCAYGZ4EM
# AQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAZSqt8RwnBLmuYEHs
# 0QhEnmNAciH45PYiT9s1i6UKtW+FERp8FgXRGQ/YAavXzWjZhY+hIfP2JkQ38U+w
# tJPBVBajYfrbIYG+Dui4I4PCvHpQuPqFgqp1PzC/ZRX4pvP/ciZmUnthfAEP1HSh
# TrY+2DE5qjzvZs7JIIgt0GCFD9ktx0LxxtRQ7vllKluHWiKk6FxRPyUPxAAYH2Vy
# 1lNM4kzekd8oEARzFAWgeW3az2xejEWLNN4eKGxDJ8WDl/FQUSntbjZ80FU3i54t
# px5F/0Kr15zW/mJAxZMVBrTE2oi0fcI8VMbtoRAmaaslNXdCG1+lqvP4FbrQ6IwS
# BXkZagHLhFU9HCrG/syTRLLhAezu/3Lr00GrJzPQFnCEH1Y58678IgmfORBPC1JK
# kYaEt2OdDh4GmO0/5cHelAK2/gTlQJINqDr6JfwyYHXSd+V08X1JUPvB4ILfJdmL
# +66Gp3CSBXG6IwXMZUXBhtCyIaehr0XkBoDIGMUG1dUtwq1qmcwbdUfcSYCn+Own
# cVUXf53VJUNOaMWMts0VlRYxe5nK+At+DI96HAlXHAL5SlfYxJ7La54i71McVWRP
# 66bW+yERNpbJCjyCYG2j+bdpxo/1Cy4uPcU3AWVPGrbn5PhDBf3Froguzzhk++am
# i+r3Qrx5bIbY3TVzgiFI7Gq3zWcwggdaMIIFQqADAgECAhANw2Fva20K+rkvK+/+
# Bxk+MA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdp
# Q2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNp
# Z25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwHhcNMjUwNzE1MDAwMDAwWhcN
# MjgwNzE2MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEOMAwGA1UECBMFVGV4YXMxDzAN
# BgNVBAcTBklydmluZzEYMBYGA1UEChMPUmljaGFyZCBGYWphcmRvMRgwFgYDVQQD
# Ew9SaWNoYXJkIEZhamFyZG8wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDsnVRBsMdpPCQMc05ysOVukiJC6uzFiamN/MuOMMicR/oTyBZa9o1+l0PV8uBJ
# 8v5yu/eDmZkwCuJRhL1QA/T5FrJ/gwwI3YtHOWBG1QDOXsg8qiekMsaFgdYWAjCe
# nEriC25YZE3I6mYNbWinJ/9/gsmP8EZCRGFbFtXfZO70c+83P2Q7TlS6R/Jt9ZgM
# d7ML6F6jvvvxSR7lJ2D+7tyiNE45r/eWjL0tYAXaKaKpnnZbMW+At/vYjRqN10CE
# OSnhQeuvTZTTiy4E/gjS6dN8gvregoO/9bAVofkDuRh6s+UvZtsp+yYyFpHTPbd6
# xEzwSwplT4/CYIVvaFEZ7mD0m3iIqR6RrdQYsdwB1dRKzJDgpxisUuAnpXRFUF8U
# NHM17/mXQPf8wwJHnleVJbnecQ8ltiVvsMKi1lveru7T8b5EeSdt1vwMzUxe6tS2
# qWeYPvBvToOc0SyrmKPvLYq3Ut6fveK5g/XRXdqj6F2JRNvelnP+gg+DIgctoKTU
# 7U8G07YIaJtO/lCIpvJXgNd0VD4iIZkhKMAbW95k4ovBhNJ9LZJ1mKF21i1MetFU
# 6vk4T2kE8oSCzAH6gFyZ+SXhGzyzcOwhv/tBhYAiwwA7keSotpqgbnhnQST0XEFP
# 9eQP1zMrpuGDNZMIxZFImD2dgNdP+X7e8zH8PJ6HxTwt/QIDAQABo4ICAzCCAf8w
# HwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYEFJflaBtr
# VOU8mYho6v2pIvOs2kN9MD4GA1UdIAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUH
# AgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMCB4Aw
# EwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDov
# L2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdS
# U0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4
# NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw
# OTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4IC
# AQBcJngbL8FgxIfCimjUkh+HAPlxzbWJW3brq7D3RHe9wfV5ItOrui6Op64vKVg5
# T5lyzZrOAzQM6GPXBBoWWVLfdSscLsI/NvLMx6eW81My+xFUgRnFSp/IQ/DwQNUf
# bthZJNN8us+urwPCoE7pinMHVOx4K5OtV9tBuu9qrqvti1IlAueOkkPZB9sKlN+a
# Q2e7o7IaEDeQ8cf9NXNlwUU5Npt4A15AyY/Rs4UZ0VA+V5+Jg12fxcP7GzonxUoB
# kNdeX6o9MnOSu0uyoahFqU6xdqhYt6rp9Jm4DCyTnOH0PhCOc2o4Yw+kyLCT89G9
# eh141zP3S3enNXUqZX1Y/oYHAk0yv1o5ANKmpLD6oHMYmD0MurprwX9Fs36mIllp
# ElxPpgnVZK6RvJjucrM9+Ohd1wTaQWLFJ/P1ma2gwZqucHebrd5m+8aABh//jtAs
# bU6cu5hQm/+Ip5jKD8BJE5xiZNr++fOokE6/x38MMgqtPSj29KNOwcAUAV73aY+x
# CKC6uRBoMaKHXVdN8r2f4/8a6dA26PaEHLY0v/PeWYUxCfiyCgMcqhFUkhP7HvsX
# 3/XxfRLB+FM2n1MkfECTc2C17mAb1+Gmt0yW6gd141JLzJAfogQxYt3FoJEcht7x
# /yvg+sWOkZtILz1ndlgt2eo/aQL/Z17OYxdPdo4rSXD8GTGCBkgwggZEAgEBMH0w
# aTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQD
# EzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4
# NCAyMDIxIENBMQIQDcNhb2ttCvq5Lyvv/gcZPjAJBgUrDgMCGgUAoHgwGAYKKwYB
# BAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAc
# BgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUKuoF
# wpka9KtGt0pB+o2kvWKfQ3swDQYJKoZIhvcNAQEBBQAEggIADNIpCNf7Ki4oMnfy
# Scy9ieHOe+9DG21nxIfB6NquVqGnCkMW2JVUn4IOvi/Z1RA8rlEyZu9gQpxilwYL
# 7AZ+gTfs9YkFCWLrtUDNrQ9xfvIDT9fgZutCugyj3WENq9UOm75hWnC2rCdQ/3cd
# sES+Khqn5RyvnK1vWcS/kNcdM449vdZhDv+rR2bh3zHOFsbg9H05pf1MMqkZ2OVm
# A3n/juHftI54OYWSXIvviZRVggIUtxyHYAoAQK5XyazxDUPlYDHmzVqjqAW/Or3J
# 7GiZ9NEo2I3urjNOsOLmc0S3rF4AJFVSxejtatJINNuVuWgB2epSpa5pfmYIIn+h
# IVfer1pTUulROFiez77EzDR6poprLCLt8gPzU9a7JFSxPzYAYTYVH3qZrNZqoA1Z
# 4FZrBKDDjHXf0ry0RWe4nq3dZs2v6uXRBxTCVyQOe1bIYRgfW1wuRdw0noFBZ9n0
# SpBNXBNTY3mC/AyFQwtpVHblNS6FdFrWRL9vMIHd449pQX5VYJDHYi8Jewef3oi5
# 5TdB0AbfNf8+rXIyxAXxpCkYMLZmRsT1JuMIfFzc6TNEwsYB3Y6tYvOVX9PfRJ+x
# MiJlsRcvW9TSmZag4h/mrFZB4JEfwCRbqf3cYnhWRlSA5423Nx3Il/TKMD0N/bFM
# W/PBIS5Q/FaVlyY4HtdnKf65S4+hggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8C
# AQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/
# BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg
# U0hBMjU2IDIwMjUgQ0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUA
# oGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjUx
# MTE4MjAxMjI3WjAvBgkqhkiG9w0BCQQxIgQgkiWYJ20hqJEUrl3u3LKB3yW0b25F
# a0wqpFX0D2hxYEUwDQYJKoZIhvcNAQEBBQAEggIABnNS0jqbMpj7DHW8d4FTngNq
# 1F+jNTdc3P8s5RL2H6ZoxUXLNcFB+ODPHLpMRbupeJ5s+7Tt0rUvgCJ6Bxh2PrrS
# R+iEFhudcRhO3lRTLyEmoRXFsFM6mOHDcMplh1VkFdeqMIqt+6vMM5qiAeilU1b1
# Yh88ZFMt38oMlNAL7GjWijscSZvaBLTqXzeldsj5mEn6IymjGF79Vt5Ld61hkBhf
# uz6FSivIq8VM8bcJt1U6JWbdxMA8rdYHwXQY2zZ+Tr/MbcHvI/Uq5rsaxxwbP/7F
# uknRq5QOrBGPeQpaV1eeSU06nU67Z9nYvLYh63Wykv+i/Yisxw6qf/IUKvhjqZI4
# N9KYtJGRdrD367ssh7mMjJxgXV0zpJMJE64eMqZN6Xa6vTWb3YZFpJyHHZjs/vHz
# QeAiG2cI7eZQFk5UFXlqJJ7PzOES2JWRr4NZ/kvdExKJ7UPV9FH4PyrhZY5H1aYo
# P1E15xkPMVYca40LQs3WszJBO2T9BH0OQQkWRguf8wPXR1i9+JTKr7tQX4KlA15m
# tmHr7XT9rynVPar7PhITQjqnGyZJHmEtXPx4PaPpLorcjTojR8I9eU7cu34oFaE4
# C+jzNlgyDybXvT/INIQrD+/JoGvGoDXhpFRWDVG0rAv+yBCKabgvwrYmdKbE5WNk
# WTOHpQQ73S7cYg8GIEU=
# SIG # End signature block