Add more errors to ServerSettingsValidationResult

This commit is contained in:
cketti 2023-12-08 12:41:06 +01:00
parent b73ebdeecc
commit 928b18422e
12 changed files with 388 additions and 0 deletions

View file

@ -136,6 +136,14 @@ abstract class BaseServerValidationViewModel(
Error.CertificateError(result.certificateChain), Error.CertificateError(result.certificateChain),
) )
ServerSettingsValidationResult.ClientCertificateError.ClientCertificateExpired -> updateError(
Error.ClientCertificateExpired,
)
ServerSettingsValidationResult.ClientCertificateError.ClientCertificateRetrievalFailure -> updateError(
Error.ClientCertificateRetrievalFailure,
)
is ServerSettingsValidationResult.NetworkError -> updateError( is ServerSettingsValidationResult.NetworkError -> updateError(
Error.NetworkError(result.exception), Error.NetworkError(result.exception),
) )
@ -144,6 +152,10 @@ abstract class BaseServerValidationViewModel(
Error.ServerError(result.serverMessage), Error.ServerError(result.serverMessage),
) )
is ServerSettingsValidationResult.MissingServerCapabilityError -> updateError(
Error.MissingServerCapabilityError(result.capabilityName),
)
is ServerSettingsValidationResult.UnknownError -> updateError( is ServerSettingsValidationResult.UnknownError -> updateError(
Error.UnknownError(result.exception.message ?: "Unknown error"), Error.UnknownError(result.exception.message ?: "Unknown error"),
) )

View file

@ -46,8 +46,11 @@ interface ServerValidationContract {
sealed interface Error { sealed interface Error {
data class NetworkError(val exception: IOException) : Error data class NetworkError(val exception: IOException) : Error
data class CertificateError(val certificateChain: List<X509Certificate>) : Error data class CertificateError(val certificateChain: List<X509Certificate>) : Error
data object ClientCertificateRetrievalFailure : Error
data object ClientCertificateExpired : Error
data class AuthenticationError(val serverMessage: String?) : Error data class AuthenticationError(val serverMessage: String?) : Error
data class ServerError(val serverMessage: String?) : Error data class ServerError(val serverMessage: String?) : Error
data class MissingServerCapabilityError(val capabilityName: String) : Error
data class UnknownError(val message: String) : Error data class UnknownError(val message: String) : Error
} }
} }

View file

@ -40,6 +40,22 @@ internal fun Error.toResourceString(resources: Resources): String {
detailsMessage = message, detailsMessage = message,
) )
} }
Error.ClientCertificateExpired -> {
resources.getString(R.string.account_server_validation_error_client_certificate_expired)
}
Error.ClientCertificateRetrievalFailure -> {
resources.getString(R.string.account_server_validation_error_client_certificate_retrieval_failure)
}
is Error.MissingServerCapabilityError -> {
resources.buildErrorString(
titleResId = R.string.account_server_validation_error_missing_server_capability,
detailsResId = R.string.account_server_validation_error_missing_server_capability_details,
detailsMessage = capabilityName,
)
}
} }
} }

View file

@ -4,8 +4,12 @@
<string name="account_server_validation_error_network">Network error</string> <string name="account_server_validation_error_network">Network error</string>
<string name="account_server_validation_error_server">Server error</string> <string name="account_server_validation_error_server">Server error</string>
<string name="account_server_validation_error_unknown">Unknown error</string> <string name="account_server_validation_error_unknown">Unknown error</string>
<string name="account_server_validation_error_client_certificate_expired">The client certificate is no longer valid</string>
<string name="account_server_validation_error_client_certificate_retrieval_failure">"The client certificate couldn't be accessed"</string>
<string name="account_server_validation_error_missing_server_capability">Missing server capability</string>
<string name="account_server_validation_error_server_message">The server returned the following message:\n%s</string> <string name="account_server_validation_error_server_message">The server returned the following message:\n%s</string>
<string name="account_server_validation_error_details">Details:\n%s</string> <string name="account_server_validation_error_details">Details:\n%s</string>
<string name="account_server_validation_error_missing_server_capability_details">The server is missing this capability:\n%s</string>
<string name="account_server_validation_incoming_loading_message">Checking incoming server settings…</string> <string name="account_server_validation_incoming_loading_message">Checking incoming server settings…</string>
<string name="account_server_validation_incoming_loading_error">Checking incoming server settings failed</string> <string name="account_server_validation_incoming_loading_error">Checking incoming server settings failed</string>
<string name="account_server_validation_incoming_success">Incoming server settings are valid</string> <string name="account_server_validation_incoming_success">Incoming server settings are valid</string>

View file

@ -23,6 +23,21 @@ sealed interface ServerSettingsValidationResult {
*/ */
data class CertificateError(val certificateChain: List<X509Certificate>) : ServerSettingsValidationResult data class CertificateError(val certificateChain: List<X509Certificate>) : ServerSettingsValidationResult
/**
* There's a problem with the client certificate.
*/
sealed interface ClientCertificateError : ServerSettingsValidationResult {
/**
* The client certificate couldn't be retrieved.
*/
data object ClientCertificateRetrievalFailure : ClientCertificateError
/**
* The client certificate (or another one in the chain) has expired.
*/
data object ClientCertificateExpired : ClientCertificateError
}
/** /**
* Authentication failed while checking the server settings. * Authentication failed while checking the server settings.
*/ */
@ -33,6 +48,11 @@ sealed interface ServerSettingsValidationResult {
*/ */
data class ServerError(val serverMessage: String?) : ServerSettingsValidationResult data class ServerError(val serverMessage: String?) : ServerSettingsValidationResult
/**
* The server is missing a capability that is required by the current server settings.
*/
data class MissingServerCapabilityError(val capabilityName: String) : ServerSettingsValidationResult
/** /**
* An unknown error occurred while checking the server settings. * An unknown error occurred while checking the server settings.
*/ */

View file

@ -2,12 +2,17 @@ package com.fsck.k9.mail.store.imap
import com.fsck.k9.mail.AuthenticationFailedException import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.CertificateValidationException import com.fsck.k9.mail.CertificateValidationException
import com.fsck.k9.mail.ClientCertificateError.CertificateExpired
import com.fsck.k9.mail.ClientCertificateError.RetrievalFailure
import com.fsck.k9.mail.ClientCertificateException
import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.MissingCapabilityException
import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.oauth.AuthStateStorage import com.fsck.k9.mail.oauth.AuthStateStorage
import com.fsck.k9.mail.oauth.OAuth2TokenProvider import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory
import com.fsck.k9.mail.server.ServerSettingsValidationResult import com.fsck.k9.mail.server.ServerSettingsValidationResult
import com.fsck.k9.mail.server.ServerSettingsValidationResult.ClientCertificateError
import com.fsck.k9.mail.server.ServerSettingsValidator import com.fsck.k9.mail.server.ServerSettingsValidator
import com.fsck.k9.mail.ssl.TrustedSocketFactory import com.fsck.k9.mail.ssl.TrustedSocketFactory
import java.io.IOException import java.io.IOException
@ -42,6 +47,13 @@ class ImapServerSettingsValidator(
ServerSettingsValidationResult.CertificateError(e.certificateChain) ServerSettingsValidationResult.CertificateError(e.certificateChain)
} catch (e: NegativeImapResponseException) { } catch (e: NegativeImapResponseException) {
ServerSettingsValidationResult.ServerError(e.responseText) ServerSettingsValidationResult.ServerError(e.responseText)
} catch (e: MissingCapabilityException) {
ServerSettingsValidationResult.MissingServerCapabilityError(e.capabilityName)
} catch (e: ClientCertificateException) {
when (e.error) {
RetrievalFailure -> ClientCertificateError.ClientCertificateRetrievalFailure
CertificateExpired -> ClientCertificateError.ClientCertificateExpired
}
} catch (e: MessagingException) { } catch (e: MessagingException) {
val cause = e.cause val cause = e.cause
if (cause is IOException) { if (cause is IOException) {

View file

@ -7,6 +7,7 @@ import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf import assertk.assertions.isInstanceOf
import assertk.assertions.prop import assertk.assertions.prop
import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ClientCertificateError
import com.fsck.k9.mail.ConnectionSecurity import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.helpers.FakeTrustManager import com.fsck.k9.mail.helpers.FakeTrustManager
@ -215,6 +216,96 @@ class ImapServerSettingsValidatorTest {
server.verifyInteractionCompleted() server.verifyInteractionCompleted()
} }
@Test
fun `missing capability should return MissingServerCapabilityError`() {
trustedSocketFactory.injectClientCertificateError(ClientCertificateError.RetrievalFailure)
val server = startServer {
output("* OK IMAP4rev1 server ready")
expect("1 CAPABILITY")
output("* CAPABILITY IMAP4rev1")
output("1 OK CAPABILITY Completed")
}
val serverSettings = ServerSettings(
type = "imap",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = USERNAME,
password = PASSWORD,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.MissingServerCapabilityError>()
.prop(ServerSettingsValidationResult.MissingServerCapabilityError::capabilityName).isEqualTo("STARTTLS")
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test
fun `client certificate retrieval failure connect should return ClientCertificateRetrievalFailure`() {
trustedSocketFactory.injectClientCertificateError(ClientCertificateError.RetrievalFailure)
val server = startServer {
output("* OK IMAP4rev1 server ready")
expect("1 CAPABILITY")
output("* CAPABILITY IMAP4rev1 AUTH=PLAIN STARTTLS")
output("1 OK CAPABILITY Completed")
expect("2 STARTTLS")
output("2 OK Begin TLS negotiation now")
startTls()
}
val serverSettings = ServerSettings(
type = "imap",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = USERNAME,
password = PASSWORD,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result)
.isInstanceOf<ServerSettingsValidationResult.ClientCertificateError.ClientCertificateRetrievalFailure>()
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test
fun `client certificate expired should return ClientCertificateExpired`() {
trustedSocketFactory.injectClientCertificateError(ClientCertificateError.CertificateExpired)
val server = startServer {
output("* OK IMAP4rev1 server ready")
expect("1 CAPABILITY")
output("* CAPABILITY IMAP4rev1 AUTH=PLAIN STARTTLS")
output("1 OK CAPABILITY Completed")
expect("2 STARTTLS")
output("2 OK Begin TLS negotiation now")
startTls()
}
val serverSettings = ServerSettings(
type = "imap",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = USERNAME,
password = PASSWORD,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result)
.isInstanceOf<ServerSettingsValidationResult.ClientCertificateError.ClientCertificateExpired>()
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test @Test
fun `non-existent hostname should return NetworkError`() { fun `non-existent hostname should return NetworkError`() {
val serverSettings = ServerSettings( val serverSettings = ServerSettings(

View file

@ -2,10 +2,15 @@ package com.fsck.k9.mail.store.pop3
import com.fsck.k9.mail.AuthenticationFailedException import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.CertificateValidationException import com.fsck.k9.mail.CertificateValidationException
import com.fsck.k9.mail.ClientCertificateError.CertificateExpired
import com.fsck.k9.mail.ClientCertificateError.RetrievalFailure
import com.fsck.k9.mail.ClientCertificateException
import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.MissingCapabilityException
import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.oauth.AuthStateStorage import com.fsck.k9.mail.oauth.AuthStateStorage
import com.fsck.k9.mail.server.ServerSettingsValidationResult import com.fsck.k9.mail.server.ServerSettingsValidationResult
import com.fsck.k9.mail.server.ServerSettingsValidationResult.ClientCertificateError
import com.fsck.k9.mail.server.ServerSettingsValidator import com.fsck.k9.mail.server.ServerSettingsValidator
import com.fsck.k9.mail.ssl.TrustedSocketFactory import com.fsck.k9.mail.ssl.TrustedSocketFactory
import java.io.IOException import java.io.IOException
@ -31,6 +36,13 @@ class Pop3ServerSettingsValidator(
ServerSettingsValidationResult.CertificateError(e.certificateChain) ServerSettingsValidationResult.CertificateError(e.certificateChain)
} catch (e: Pop3ErrorResponse) { } catch (e: Pop3ErrorResponse) {
ServerSettingsValidationResult.ServerError(e.responseText) ServerSettingsValidationResult.ServerError(e.responseText)
} catch (e: MissingCapabilityException) {
ServerSettingsValidationResult.MissingServerCapabilityError(e.capabilityName)
} catch (e: ClientCertificateException) {
when (e.error) {
RetrievalFailure -> ClientCertificateError.ClientCertificateRetrievalFailure
CertificateExpired -> ClientCertificateError.ClientCertificateExpired
}
} catch (e: MessagingException) { } catch (e: MessagingException) {
val cause = e.cause val cause = e.cause
if (cause is IOException) { if (cause is IOException) {

View file

@ -7,6 +7,7 @@ import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf import assertk.assertions.isInstanceOf
import assertk.assertions.prop import assertk.assertions.prop
import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ClientCertificateError
import com.fsck.k9.mail.ConnectionSecurity import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.helpers.FakeTrustManager import com.fsck.k9.mail.helpers.FakeTrustManager
@ -125,6 +126,99 @@ class Pop3ServerSettingsValidatorTest {
server.verifyInteractionCompleted() server.verifyInteractionCompleted()
} }
@Test
fun `missing capability should return MissingServerCapabilityError`() {
val server = startServer {
output("+OK POP3 server greeting")
expect("CAPA")
output("+OK Listing of supported mechanisms follows")
output(".")
expect("QUIT")
closeConnection()
}
val serverSettings = ServerSettings(
type = "pop3",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = USERNAME,
password = PASSWORD,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.MissingServerCapabilityError>()
.prop(ServerSettingsValidationResult.MissingServerCapabilityError::capabilityName).isEqualTo("STLS")
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test
fun `client certificate retrieval failure should return ClientCertificateRetrievalFailure`() {
trustedSocketFactory.injectClientCertificateError(ClientCertificateError.RetrievalFailure)
val server = startServer {
output("+OK POP3 server greeting")
expect("CAPA")
output("+OK Listing of supported mechanisms follows")
output("STLS")
output(".")
expect("STLS")
output("+OK Begin TLS negotiation")
startTls()
}
val serverSettings = ServerSettings(
type = "pop3",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = USERNAME,
password = PASSWORD,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result)
.isInstanceOf<ServerSettingsValidationResult.ClientCertificateError.ClientCertificateRetrievalFailure>()
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test
fun `client certificate expired error should return ClientCertificateExpired`() {
trustedSocketFactory.injectClientCertificateError(ClientCertificateError.CertificateExpired)
val server = startServer {
output("+OK POP3 server greeting")
expect("CAPA")
output("+OK Listing of supported mechanisms follows")
output("STLS")
output(".")
expect("STLS")
output("+OK Begin TLS negotiation")
startTls()
}
val serverSettings = ServerSettings(
type = "pop3",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = USERNAME,
password = PASSWORD,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result)
.isInstanceOf<ServerSettingsValidationResult.ClientCertificateError.ClientCertificateExpired>()
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test @Test
fun `certificate error when trying to connect should return CertificateError`() { fun `certificate error when trying to connect should return CertificateError`() {
fakeTrustManager.shouldThrowException = true fakeTrustManager.shouldThrowException = true

View file

@ -2,12 +2,17 @@ package com.fsck.k9.mail.transport.smtp
import com.fsck.k9.mail.AuthenticationFailedException import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.CertificateValidationException import com.fsck.k9.mail.CertificateValidationException
import com.fsck.k9.mail.ClientCertificateError.CertificateExpired
import com.fsck.k9.mail.ClientCertificateError.RetrievalFailure
import com.fsck.k9.mail.ClientCertificateException
import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.MissingCapabilityException
import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.oauth.AuthStateStorage import com.fsck.k9.mail.oauth.AuthStateStorage
import com.fsck.k9.mail.oauth.OAuth2TokenProvider import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory
import com.fsck.k9.mail.server.ServerSettingsValidationResult import com.fsck.k9.mail.server.ServerSettingsValidationResult
import com.fsck.k9.mail.server.ServerSettingsValidationResult.ClientCertificateError
import com.fsck.k9.mail.server.ServerSettingsValidator import com.fsck.k9.mail.server.ServerSettingsValidator
import com.fsck.k9.mail.ssl.TrustedSocketFactory import com.fsck.k9.mail.ssl.TrustedSocketFactory
import java.io.IOException import java.io.IOException
@ -35,6 +40,13 @@ class SmtpServerSettingsValidator(
ServerSettingsValidationResult.CertificateError(e.certificateChain) ServerSettingsValidationResult.CertificateError(e.certificateChain)
} catch (e: NegativeSmtpReplyException) { } catch (e: NegativeSmtpReplyException) {
ServerSettingsValidationResult.ServerError(e.replyText) ServerSettingsValidationResult.ServerError(e.replyText)
} catch (e: MissingCapabilityException) {
ServerSettingsValidationResult.MissingServerCapabilityError(e.capabilityName)
} catch (e: ClientCertificateException) {
when (e.error) {
RetrievalFailure -> ClientCertificateError.ClientCertificateRetrievalFailure
CertificateExpired -> ClientCertificateError.ClientCertificateExpired
}
} catch (e: MessagingException) { } catch (e: MessagingException) {
val cause = e.cause val cause = e.cause
if (cause is IOException) { if (cause is IOException) {

View file

@ -7,6 +7,7 @@ import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf import assertk.assertions.isInstanceOf
import assertk.assertions.prop import assertk.assertions.prop
import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ClientCertificateError
import com.fsck.k9.mail.ConnectionSecurity import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.helpers.FakeTrustManager import com.fsck.k9.mail.helpers.FakeTrustManager
@ -165,6 +166,102 @@ class SmtpServerSettingsValidatorTest {
server.verifyInteractionCompleted() server.verifyInteractionCompleted()
} }
@Test
fun `missing capability should return MissingServerCapabilityError`() {
val server = MockSmtpServer().apply {
output("220 localhost Simple Mail Transfer Service Ready")
expect("EHLO [127.0.0.1]")
output("250-localhost Hello 127.0.0.1")
output("250 HELP")
expect("QUIT")
closeConnection()
}
server.start()
val serverSettings = ServerSettings(
type = "smtp",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = USERNAME,
password = PASSWORD,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result).isInstanceOf<ServerSettingsValidationResult.MissingServerCapabilityError>()
.prop(ServerSettingsValidationResult.MissingServerCapabilityError::capabilityName).isEqualTo("STARTTLS")
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test
fun `client certificate retrieval failure should return ClientCertificateRetrievalFailure`() {
trustedSocketFactory.injectClientCertificateError(ClientCertificateError.RetrievalFailure)
val server = MockSmtpServer().apply {
output("220 localhost Simple Mail Transfer Service Ready")
expect("EHLO [127.0.0.1]")
output("250-localhost Hello 127.0.0.1")
output("250-STARTTLS")
output("250 HELP")
expect("STARTTLS")
output("220 Ready to start TLS")
startTls()
}
server.start()
val serverSettings = ServerSettings(
type = "smtp",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = USERNAME,
password = PASSWORD,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result)
.isInstanceOf<ServerSettingsValidationResult.ClientCertificateError.ClientCertificateRetrievalFailure>()
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test
fun `client certificate expired error should return ClientCertificateExpired`() {
trustedSocketFactory.injectClientCertificateError(ClientCertificateError.CertificateExpired)
val server = MockSmtpServer().apply {
output("220 localhost Simple Mail Transfer Service Ready")
expect("EHLO [127.0.0.1]")
output("250-localhost Hello 127.0.0.1")
output("250-STARTTLS")
output("250 HELP")
expect("STARTTLS")
output("220 Ready to start TLS")
startTls()
}
server.start()
val serverSettings = ServerSettings(
type = "smtp",
host = server.host,
port = server.port,
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = USERNAME,
password = PASSWORD,
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
)
val result = serverSettingsValidator.checkServerSettings(serverSettings, authStateStorage = null)
assertThat(result)
.isInstanceOf<ServerSettingsValidationResult.ClientCertificateError.ClientCertificateExpired>()
server.verifyConnectionClosed()
server.verifyInteractionCompleted()
}
@Test @Test
fun `certificate error when trying to connect should return CertificateError`() { fun `certificate error when trying to connect should return CertificateError`() {
fakeTrustManager.shouldThrowException = true fakeTrustManager.shouldThrowException = true

View file

@ -1,5 +1,7 @@
package com.fsck.k9.mail.helpers package com.fsck.k9.mail.helpers
import com.fsck.k9.mail.ClientCertificateError
import com.fsck.k9.mail.ClientCertificateException
import com.fsck.k9.mail.ssl.TrustedSocketFactory import com.fsck.k9.mail.ssl.TrustedSocketFactory
import java.net.Socket import java.net.Socket
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
@ -7,9 +9,18 @@ import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
class SimpleTrustedSocketFactory(private val trustManager: X509TrustManager) : TrustedSocketFactory { class SimpleTrustedSocketFactory(private val trustManager: X509TrustManager) : TrustedSocketFactory {
private var clientCertificateError: ClientCertificateError? = null
override fun createSocket(socket: Socket?, host: String, port: Int, clientCertificateAlias: String?): Socket { override fun createSocket(socket: Socket?, host: String, port: Int, clientCertificateAlias: String?): Socket {
requireNotNull(socket) requireNotNull(socket)
@Suppress("ThrowingExceptionsWithoutMessageOrCause")
when (val error = clientCertificateError) {
ClientCertificateError.RetrievalFailure -> throw ClientCertificateException(error, RuntimeException())
ClientCertificateError.CertificateExpired -> throw ClientCertificateException(error, RuntimeException())
null -> Unit
}
val trustManagers = arrayOf<TrustManager>(trustManager) val trustManagers = arrayOf<TrustManager>(trustManager)
val sslContext = SSLContext.getInstance("TLS").apply { val sslContext = SSLContext.getInstance("TLS").apply {
@ -23,4 +34,8 @@ class SimpleTrustedSocketFactory(private val trustManager: X509TrustManager) : T
true, true,
) )
} }
fun injectClientCertificateError(error: ClientCertificateError) {
clientCertificateError = error
}
} }