Add more errors to ServerSettingsValidationResult
This commit is contained in:
parent
b73ebdeecc
commit
928b18422e
12 changed files with 388 additions and 0 deletions
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue