Imports System.Net.Security
Imports DigitalData.Modules.Base
Imports DigitalData.Modules.Logging
Imports Limilabs.Client
Imports Limilabs.Client.IMAP
Imports Limilabs.Client.SMTP
Imports Microsoft.Identity.Client

Namespace Mail
    Public Class MailSession
        Inherits BaseClass

        Public ReadOnly Client As ClientBase
        Public ReadOnly OAuth2 As OAuth2

        Public Const AUTH_SSL = "SSL"
        Public Const AUTH_STARTTLS = "STARTTLS"
        Public Const AUTH_SSLTLS = "SSL/TLS"
        Public Const AUTH_NONE = "NONE"
        Public Const AUTH_OAUTH2 = "OAUTH2"

        Private Const SMTP_IGNORED_ERRORS As SslPolicyErrors =
            SslPolicyErrors.RemoteCertificateChainErrors Or ' self-signed
            SslPolicyErrors.RemoteCertificateNameMismatch   ' name mismatch

        Private _Session As SessionInfo

        Public ReadOnly Property Session As SessionInfo
            Get
                Return _Session
            End Get
        End Property

        Public Sub New(pLogConfig As LogConfig, pClient As ClientBase)
            MyBase.New(pLogConfig)
            Client = pClient
        End Sub

        Public Class SessionInfo
            Public Server As String
            Public Port As Integer
            Public User As String
            Public Password As String
            Public AuthType As String

            Public ClientId As String
            Public TenantId As String
            Public ClientSecret As String

            Public [Error] As Exception
            Public Connected As Boolean = False
        End Class

        Public Class MailSessionOptions
            Public Property EnableDefault As Boolean = True
            Public Property EnableTls1_1 As Boolean = False
            Public Property EnableTls1_2 As Boolean = False

            ' Not available in .NET 4.6.2
            'Public Property EnableTls1_3 As Boolean = False
        End Class

        ''' <summary>
        ''' Start a connection with the specified server and return the session info.
        ''' </summary>
        ''' <param name="pServer"></param>
        ''' <param name="pPort"></param>
        ''' <param name="pUser"></param>
        ''' <param name="pPassword"></param>
        ''' <param name="pAuthType"></param>
        ''' <returns></returns>
        Public Function ConnectToServerWithBasicAuth(pServer As String, pPort As Integer, pUser As String, pPassword As String, pAuthType As String, pOptions As MailSessionOptions) As SessionInfo
            Dim oSession = New SessionInfo With {
                .Server = pServer,
                .Port = pPort,
                .User = pUser,
                .Password = pPassword,
                .AuthType = pAuthType
            }

            Logger.Debug("Connecting to Server..")
            Logger.Debug("Server: [{0}]", oSession.Server)
            Logger.Debug("Port: [{0}]", oSession.Port)
            Logger.Debug("User: [{0}]", oSession.User)
            Logger.Debug("AuthType: [{0}]", oSession.AuthType)

            _Session = oSession

            Logger.Debug("Initializing Connection with Auth type [{0}].", oSession.AuthType)

            Return ConnectToServer(oSession, pOptions)
        End Function

        Public Function ConnectToServerWithO365OAuth(pUser As String, pClientId As String, pTenantId As String, pClientSecret As String, pOptions As MailSessionOptions) As SessionInfo
            Dim oSession = New SessionInfo With {
                .Server = OAuth2.O365_SERVER,
                .ClientId = pClientId,
                .ClientSecret = pClientSecret,
                .TenantId = pTenantId,
                .User = pUser,
                .AuthType = AUTH_OAUTH2
            }

            Logger.Debug("Connecting to Server..")
            Logger.Debug("Server: [{0}]", oSession.Server)
            Logger.Debug("User: [{0}]", oSession.User)
            Logger.Debug("ClientId: [{0}]", oSession.ClientId)
            Logger.Debug("TenantId: [{0}]", oSession.TenantId)
            Logger.Debug("AuthType: [{0}]", oSession.AuthType)

            _Session = oSession

            Logger.Debug("Initializing Connection with Auth type [{0}].", oSession.AuthType)

            Return ConnectToServer(oSession, pOptions)
        End Function

        Private Function ConnectToServer(pSession As SessionInfo, pOptions As MailSessionOptions) As SessionInfo
            AddHandler Client.ServerCertificateValidate, AddressOf Session_ServerCertificateValidate

            If pOptions.EnableDefault Then
                Logger.Debug("Enabling Default TLS Version")
                Client.SSLConfiguration.EnabledSslProtocols = Security.Authentication.SslProtocols.Default
            Else
                Logger.Debug("Disabling Default TLS Version")
                Client.SSLConfiguration.EnabledSslProtocols = Security.Authentication.SslProtocols.None
            End If

            ' Set TLS Version manually if requested
            If pOptions.EnableTls1_1 Then
                Logger.Debug("Enabling TLS Version 1.1")
                Client.SSLConfiguration.EnabledSslProtocols = Client.SSLConfiguration.EnabledSslProtocols Or Security.Authentication.SslProtocols.Tls11
            End If

            If pOptions.EnableTls1_2 Then
                Logger.Debug("Enabling TLS Version 1.2")
                Client.SSLConfiguration.EnabledSslProtocols = Client.SSLConfiguration.EnabledSslProtocols Or Security.Authentication.SslProtocols.Tls12
            End If

            ' This is not available in .NET 4.6.2, only in .NET 4.7/4.8
            'If pOptions.EnableTls1_3 Then
            '    Logger.Debug("Enabling TLS Version 1.3")
            '    oSession.SSLConfiguration.EnabledSslProtocols = oSession.SSLConfiguration.EnabledSslProtocols Or Security.Authentication.SslProtocols.Tls13
            'End If

            Logger.Debug("Enabled Encryption Protocols: [{0}]", Client.SSLConfiguration.EnabledSslProtocols)

            If pSession.AuthType = AUTH_OAUTH2 Then
                Try
                    If TypeOf Client Is Imap Then
                        Dim oClient As Imap = Client

                        Logger.Debug("Connecting with [ConnectSSL] on [{0}]", pSession.Server)
                        oClient.ConnectSSL(pSession.Server)
                    Else
                        Throw New ApplicationException("Only OAuth2 for IMAP is not yet supported!")
                    End If

                Catch ex As Exception
                    Logger.Warn("Error while connecting with Auth type OAuth2!")
                    Logger.Error(ex)

                    Session.Error = ex
                    Return Session
                End Try

            ElseIf pSession.AuthType = AUTH_SSL Then
                Try
                    If pSession.Port = 993 Then
                        Logger.Debug("Connecting with [ConnectSSL] on [{0}/{1}]", pSession.Server, pSession.Port)
                        Client.ConnectSSL(pSession.Server, pSession.Port)
                    Else
                        Logger.Debug("Connecting with [Connect] on [{0}/{1}]", pSession.Server, pSession.Port)
                        Client.Connect(pSession.Server, pSession.Port)
                    End If
                    Logger.Info("Connection Successful!")

                Catch ex As Exception
                    Logger.Warn("Error while connecting with Auth type SSL!")
                    Logger.Error(ex)

                    Session.Error = ex
                    Return Session
                End Try

            ElseIf Session.AuthType = AUTH_SSLTLS Or Session.AuthType = AUTH_STARTTLS Then

                Try
                    If pSession.Port = 993 Then
                        Logger.Debug("Connecting with [ConnectSSL] on [{0}/{1}]", pSession.Server, pSession.Port)
                        Client.ConnectSSL(pSession.Server, pSession.Port)
                    Else
                        Logger.Debug("Connecting with [Connect] on [{0}/{1}]", pSession.Server, pSession.Port)
                        Client.Connect(pSession.Server, pSession.Port)
                    End If
                    Logger.Info("Connection Successful!")

                    Dim oSupportsSTARTTLS As Boolean = SupportsSTARTTLS(Client)
                    Logger.Debug("Server supports STARTTLS: [{0}]", oSupportsSTARTTLS)

                    If oSupportsSTARTTLS Then
                        DoSTARTTLS(Client)
                        Logger.Info("STARTTLS Successful!")
                    Else
                        Logger.Debug("Server does not support STARTTLS. Enabling TLS1.2 instead.")
                        Client.SSLConfiguration.EnabledSslProtocols = Security.Authentication.SslProtocols.Tls12
                    End If

                Catch ex As Exception
                    Logger.Warn("Error while connecting with Auth type STARTTLS!")
                    Logger.Error(ex)

                    pSession.Error = ex
                    Return pSession
                End Try

            Else
                Try
                    Logger.Debug("Unknown Auth type. Using PLAINTEXT connection.")
                    Client.Connect(pSession.Server, pSession.Port, useSSL:=False)
                    Logger.Info("Connection Successful!")
                Catch ex As Exception
                    Logger.Warn("Error while connecting with Auth type PLAINTEXT!")
                    Logger.Error(ex)

                    pSession.Error = ex
                    Return pSession
                End Try

            End If

            Try
                If pSession.User <> String.Empty Then
                    Logger.Info("Logging in with user [{0}] and Auth Type [{1}]", pSession.User, pSession.AuthType)

                    If pSession.AuthType = AUTH_OAUTH2 Then
                        ' SessionInfo.Password will be the access token that was obtained 
                        ' in the OAuth2 flow before
                        DoUseBestLogin_OAuth2(Client, pSession)
                    Else
                        DoUseBestLogin_BasicAuth(Client, pSession.User, pSession.Password)
                    End If

                End If
            Catch ex As Exception
                Logger.Warn("Error while connecting with Auth type [{0}]!", pSession.AuthType)
                Logger.Error(ex)

                pSession.Error = ex
                Return pSession
            End Try

            pSession.Connected = True

            Return pSession
        End Function

        Public Function DisconnectFromServer() As Boolean
            Try
                If Client IsNot Nothing Then
                    Logger.Debug("Closing connection to Server [{0}].", Session.Server)
                    DoClose(Client)
                    Session.Connected = False
                    Logger.Info("Connection to Server [{0}] closed.", Session.Server)
                Else
                    Logger.Debug("No connection currently open. Exiting.")
                End If

                Return True

            Catch ex As Exception
                Logger.Error(ex)
                Return False

            End Try
        End Function

        Public Function TestLogin(pServer As String, pPort As Integer, pUser As String, pPassword As String, pAuthType As String, pOptions As MailSessionOptions) As Boolean
            Dim oInfo = ConnectToServerWithBasicAuth(pServer, pPort, pUser, pPassword, pAuthType, pOptions)
            If oInfo.Connected Then
                If DisconnectFromServer() Then
                    Return True
                Else
                    Logger.Warn("Login Test failed while disconnecting.")
                    Return False
                End If
            Else
                Logger.Warn("Login Test Failed while connecting.")
                Return False
            End If
        End Function

        Private Function SupportsSTARTTLS(pClient As ClientBase)
            If TypeOf pClient Is Smtp Then
                Return DirectCast(pClient, Smtp).SupportedExtensions.Contains(SmtpExtension.StartTLS)
            ElseIf TypeOf pClient Is Imap Then
                Return DirectCast(pClient, Imap).SupportedExtensions.Contains(ImapExtension.StartTLS)
            Else
                Logger.Error("Unknown session type: [{0}]", pClient.GetType.ToString)
                Return False
            End If
        End Function

        Private Sub DoClose(pClient As ClientBase)
            If TypeOf pClient Is Smtp Then
                DirectCast(pClient, Smtp).Close()
            ElseIf TypeOf pClient Is Imap Then
                DirectCast(pClient, Imap).Close()
            Else
                Logger.Error("Unknown session type: [{0}]", pClient.GetType.ToString)
            End If
        End Sub

        Private Sub DoUseBestLogin_BasicAuth(pClient As ClientBase, pUserName As String, pPassword As String)
            Logger.Debug("Logging in with Simple Auth")

            If TypeOf pClient Is Smtp Then
                DirectCast(pClient, Smtp).UseBestLogin(pUserName, pPassword)
            ElseIf TypeOf pClient Is Imap Then
                DirectCast(pClient, Imap).UseBestLogin(pUserName, pPassword)
            Else
                Logger.Error("Unknown session type: [{0}]", pClient.GetType.ToString)
            End If
        End Sub

        Private Sub DoUseBestLogin_OAuth2(pClient As ClientBase, pSession As SessionInfo)
            Logger.Debug("Logging in with O365 OAuth2")

            If TypeOf pClient Is Imap Then
                Dim oOAuth = New OAuth2(LogConfig, pSession.TenantId, pSession.ClientId, pSession.ClientSecret)
                Dim oAccessToken = oOAuth.GetAccessToken()

                DirectCast(pClient, Imap).LoginOAUTH2(pSession.User, oAccessToken)
            Else
                Logger.Error("Unknown session type: [{0}]", pClient.GetType.ToString)
            End If
        End Sub

        Private Sub DoSTARTTLS(pClient As ClientBase)
            If TypeOf pClient Is Smtp Then
                DirectCast(pClient, Smtp).StartTLS()
            ElseIf TypeOf pClient Is Imap Then
                DirectCast(pClient, Imap).StartTLS()
            Else
                Logger.Error("Unknown session type: [{0}]", pClient.GetType.ToString)
            End If
        End Sub

        Private Sub Session_ServerCertificateValidate(sender As Object, e As ServerCertificateValidateEventArgs)
            ' i dont know why it works but it does
            If (e.SslPolicyErrors And Not SMTP_IGNORED_ERRORS) = SslPolicyErrors.None Then
                e.IsValid = True
            Else
                e.IsValid = False
            End If
        End Sub
    End Class
End Namespace

