Add ImapServerSettingsValidator
This commit is contained in:
parent
1e42e92b1b
commit
2706107519
3 changed files with 258 additions and 0 deletions
|
@ -0,0 +1,51 @@
|
||||||
|
package com.fsck.k9.mail.store.imap
|
||||||
|
|
||||||
|
import com.fsck.k9.mail.AuthenticationFailedException
|
||||||
|
import com.fsck.k9.mail.CertificateValidationException
|
||||||
|
import com.fsck.k9.mail.MessagingException
|
||||||
|
import com.fsck.k9.mail.ServerSettings
|
||||||
|
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||||
|
import com.fsck.k9.mail.server.ServerSettingsValidationResult
|
||||||
|
import com.fsck.k9.mail.server.ServerSettingsValidator
|
||||||
|
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class ImapServerSettingsValidator(
|
||||||
|
private val trustedSocketFactory: TrustedSocketFactory,
|
||||||
|
private val oAuth2TokenProvider: OAuth2TokenProvider?,
|
||||||
|
private val clientIdAppName: String,
|
||||||
|
) : ServerSettingsValidator {
|
||||||
|
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
override fun checkServerSettings(serverSettings: ServerSettings): ServerSettingsValidationResult {
|
||||||
|
val config = object : ImapStoreConfig {
|
||||||
|
override val logLabel = "check"
|
||||||
|
override fun isSubscribedFoldersOnly() = false
|
||||||
|
override fun clientIdAppName() = clientIdAppName
|
||||||
|
}
|
||||||
|
val store = RealImapStore(serverSettings, config, trustedSocketFactory, oAuth2TokenProvider)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
store.checkSettings()
|
||||||
|
|
||||||
|
ServerSettingsValidationResult.Success
|
||||||
|
} catch (e: AuthenticationFailedException) {
|
||||||
|
ServerSettingsValidationResult.AuthenticationError(e.messageFromServer)
|
||||||
|
} catch (e: CertificateValidationException) {
|
||||||
|
ServerSettingsValidationResult.CertificateError(e.certChain.toList())
|
||||||
|
} catch (e: NegativeImapResponseException) {
|
||||||
|
ServerSettingsValidationResult.ServerError(e.responseText)
|
||||||
|
} catch (e: MessagingException) {
|
||||||
|
val cause = e.cause
|
||||||
|
if (cause is IOException) {
|
||||||
|
ServerSettingsValidationResult.NetworkError(cause)
|
||||||
|
} else {
|
||||||
|
ServerSettingsValidationResult.UnknownError(e)
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
ServerSettingsValidationResult.NetworkError(e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ServerSettingsValidationResult.UnknownError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,8 @@ internal open class RealImapStore(
|
||||||
private var connectionGeneration = 1
|
private var connectionGeneration = 1
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
require(serverSettings.type == "imap") { "Expected IMAP ServerSettings" }
|
||||||
|
|
||||||
val autoDetectNamespace = serverSettings.autoDetectNamespace
|
val autoDetectNamespace = serverSettings.autoDetectNamespace
|
||||||
val pathPrefixSetting = serverSettings.pathPrefix
|
val pathPrefixSetting = serverSettings.pathPrefix
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
package com.fsck.k9.mail.store.imap
|
||||||
|
|
||||||
|
import assertk.assertFailure
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.hasSize
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import assertk.assertions.isInstanceOf
|
||||||
|
import assertk.assertions.prop
|
||||||
|
import com.fsck.k9.mail.AuthType
|
||||||
|
import com.fsck.k9.mail.ConnectionSecurity
|
||||||
|
import com.fsck.k9.mail.ServerSettings
|
||||||
|
import com.fsck.k9.mail.helpers.FakeTrustManager
|
||||||
|
import com.fsck.k9.mail.helpers.SimpleTrustedSocketFactory
|
||||||
|
import com.fsck.k9.mail.server.ServerSettingsValidationResult
|
||||||
|
import com.fsck.k9.mail.store.imap.mockserver.MockImapServer
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
private const val USERNAME = "user"
|
||||||
|
private const val PASSWORD = "password"
|
||||||
|
private val CLIENT_CERTIFICATE_ALIAS: String? = null
|
||||||
|
private const val CLIENT_ID = "clientId"
|
||||||
|
|
||||||
|
class ImapServerSettingsValidatorTest {
|
||||||
|
private val fakeTrustManager = FakeTrustManager()
|
||||||
|
private val serverSettingsValidator = ImapServerSettingsValidator(
|
||||||
|
trustedSocketFactory = SimpleTrustedSocketFactory(fakeTrustManager),
|
||||||
|
oAuth2TokenProvider = null,
|
||||||
|
clientIdAppName = CLIENT_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `valid server settings should return Success`() {
|
||||||
|
val server = startServer {
|
||||||
|
output("* OK IMAP4rev1 server ready")
|
||||||
|
expect("1 CAPABILITY")
|
||||||
|
output("* CAPABILITY IMAP4rev1 AUTH=PLAIN")
|
||||||
|
output("1 OK CAPABILITY Completed")
|
||||||
|
expect("2 AUTHENTICATE PLAIN")
|
||||||
|
output("+")
|
||||||
|
expect("AHVzZXIAcGFzc3dvcmQ=")
|
||||||
|
output("2 OK [CAPABILITY IMAP4rev1 AUTH=PLAIN NAMESPACE ID] LOGIN completed")
|
||||||
|
expect("3 ID (\"name\" \"$CLIENT_ID\")")
|
||||||
|
output("* ID NIL")
|
||||||
|
output("3 OK ID completed")
|
||||||
|
expect("4 NAMESPACE")
|
||||||
|
output("* NAMESPACE ((\"\" \"/\")) NIL NIL")
|
||||||
|
output("4 OK command completed")
|
||||||
|
}
|
||||||
|
val serverSettings = ServerSettings(
|
||||||
|
type = "imap",
|
||||||
|
host = server.host,
|
||||||
|
port = server.port,
|
||||||
|
connectionSecurity = ConnectionSecurity.NONE,
|
||||||
|
authenticationType = AuthType.PLAIN,
|
||||||
|
username = USERNAME,
|
||||||
|
password = PASSWORD,
|
||||||
|
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
|
||||||
|
extra = ImapStoreSettings.createExtra(
|
||||||
|
autoDetectNamespace = true,
|
||||||
|
pathPrefix = null,
|
||||||
|
useCompression = false,
|
||||||
|
sendClientId = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = serverSettingsValidator.checkServerSettings(serverSettings)
|
||||||
|
|
||||||
|
assertThat(result).isInstanceOf<ServerSettingsValidationResult.Success>()
|
||||||
|
server.verifyConnectionClosed()
|
||||||
|
server.verifyInteractionCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `authentication error should return AuthenticationError`() {
|
||||||
|
val server = startServer {
|
||||||
|
output("* OK IMAP4rev1 server ready")
|
||||||
|
expect("1 CAPABILITY")
|
||||||
|
output("* CAPABILITY IMAP4rev1")
|
||||||
|
output("1 OK CAPABILITY Completed")
|
||||||
|
expect("2 LOGIN \"user\" \"password\"")
|
||||||
|
output("2 NO [AUTHENTICATIONFAILED] Authentication failed")
|
||||||
|
closeConnection()
|
||||||
|
}
|
||||||
|
val serverSettings = ServerSettings(
|
||||||
|
type = "imap",
|
||||||
|
host = server.host,
|
||||||
|
port = server.port,
|
||||||
|
connectionSecurity = ConnectionSecurity.NONE,
|
||||||
|
authenticationType = AuthType.PLAIN,
|
||||||
|
username = USERNAME,
|
||||||
|
password = PASSWORD,
|
||||||
|
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = serverSettingsValidator.checkServerSettings(serverSettings)
|
||||||
|
|
||||||
|
assertThat(result).isInstanceOf<ServerSettingsValidationResult.AuthenticationError>()
|
||||||
|
.prop(ServerSettingsValidationResult.AuthenticationError::serverMessage).isEqualTo("Authentication failed")
|
||||||
|
server.verifyConnectionClosed()
|
||||||
|
server.verifyInteractionCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `error response should return ServerError`() {
|
||||||
|
val server = startServer {
|
||||||
|
output("* OK IMAP4rev1 server ready")
|
||||||
|
expect("1 CAPABILITY")
|
||||||
|
output("1 BAD Something went wrong")
|
||||||
|
closeConnection()
|
||||||
|
}
|
||||||
|
val serverSettings = ServerSettings(
|
||||||
|
type = "imap",
|
||||||
|
host = server.host,
|
||||||
|
port = server.port,
|
||||||
|
connectionSecurity = ConnectionSecurity.NONE,
|
||||||
|
authenticationType = AuthType.PLAIN,
|
||||||
|
username = USERNAME,
|
||||||
|
password = PASSWORD,
|
||||||
|
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = serverSettingsValidator.checkServerSettings(serverSettings)
|
||||||
|
|
||||||
|
assertThat(result).isInstanceOf<ServerSettingsValidationResult.ServerError>()
|
||||||
|
server.verifyConnectionClosed()
|
||||||
|
server.verifyInteractionCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `certificate error when trying to connect should return CertificateError`() {
|
||||||
|
fakeTrustManager.shouldThrowException = true
|
||||||
|
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)
|
||||||
|
|
||||||
|
assertThat(result).isInstanceOf<ServerSettingsValidationResult.CertificateError>()
|
||||||
|
.prop(ServerSettingsValidationResult.CertificateError::certificateChain).hasSize(1)
|
||||||
|
server.verifyConnectionClosed()
|
||||||
|
server.verifyInteractionCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-existent hostname should return NetworkError`() {
|
||||||
|
val serverSettings = ServerSettings(
|
||||||
|
type = "imap",
|
||||||
|
host = "domain.invalid",
|
||||||
|
port = 587,
|
||||||
|
connectionSecurity = ConnectionSecurity.NONE,
|
||||||
|
authenticationType = AuthType.PLAIN,
|
||||||
|
username = USERNAME,
|
||||||
|
password = PASSWORD,
|
||||||
|
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = serverSettingsValidator.checkServerSettings(serverSettings)
|
||||||
|
|
||||||
|
assertThat(result).isInstanceOf<ServerSettingsValidationResult.NetworkError>()
|
||||||
|
.prop(ServerSettingsValidationResult.NetworkError::exception)
|
||||||
|
.isInstanceOf<UnknownHostException>()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ServerSettings with wrong type should throw`() {
|
||||||
|
val serverSettings = ServerSettings(
|
||||||
|
type = "wrong",
|
||||||
|
host = "domain.invalid",
|
||||||
|
port = 587,
|
||||||
|
connectionSecurity = ConnectionSecurity.NONE,
|
||||||
|
authenticationType = AuthType.PLAIN,
|
||||||
|
username = USERNAME,
|
||||||
|
password = PASSWORD,
|
||||||
|
clientCertificateAlias = CLIENT_CERTIFICATE_ALIAS,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertFailure {
|
||||||
|
serverSettingsValidator.checkServerSettings(serverSettings)
|
||||||
|
}.isInstanceOf<IllegalArgumentException>()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startServer(block: MockImapServer.() -> Unit): MockImapServer {
|
||||||
|
return MockImapServer().apply {
|
||||||
|
block()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue