Merge pull request #6762 from thundernest/imap_authentication_failure
IMAP: Ignore errors during LOGIN fallback
This commit is contained in:
commit
709b55f2d1
5 changed files with 130 additions and 13 deletions
|
@ -415,7 +415,7 @@ class AccountSetupCheckSettings : K9Activity(), ConfirmationDialogFragmentListen
|
||||||
finish()
|
finish()
|
||||||
} catch (e: AuthenticationFailedException) {
|
} catch (e: AuthenticationFailedException) {
|
||||||
Timber.e(e, "Error while testing settings")
|
Timber.e(e, "Error while testing settings")
|
||||||
showErrorDialog(R.string.account_setup_failed_dlg_auth_message_fmt, e.message.orEmpty())
|
showErrorDialog(R.string.account_setup_failed_dlg_auth_message_fmt, e.messageFromServer.orEmpty())
|
||||||
} catch (e: CertificateValidationException) {
|
} catch (e: CertificateValidationException) {
|
||||||
handleCertificateValidationException(e)
|
handleCertificateValidationException(e)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -359,7 +359,11 @@ internal class RealImapConnection(
|
||||||
private fun handlePermanentOAuthFailure(e: NegativeImapResponseException): AuthenticationFailedException {
|
private fun handlePermanentOAuthFailure(e: NegativeImapResponseException): AuthenticationFailedException {
|
||||||
Timber.v(e, "Permanent failure during authentication using OAuth token")
|
Timber.v(e, "Permanent failure during authentication using OAuth token")
|
||||||
|
|
||||||
return AuthenticationFailedException(message = e.message!!, throwable = e, messageFromServer = e.alertText)
|
return AuthenticationFailedException(
|
||||||
|
message = "Authentication failed",
|
||||||
|
throwable = e,
|
||||||
|
messageFromServer = ResponseTextExtractor.getResponseText(e.lastResponse),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleTemporaryOAuthFailure(method: OAuthMethod, e: NegativeImapResponseException): List<ImapResponse> {
|
private fun handleTemporaryOAuthFailure(method: OAuthMethod, e: NegativeImapResponseException): List<ImapResponse> {
|
||||||
|
@ -445,7 +449,22 @@ internal class RealImapConnection(
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginOrThrow(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ThrowsCount")
|
||||||
|
private fun loginOrThrow(originalException: AuthenticationFailedException): List<ImapResponse> {
|
||||||
|
return try {
|
||||||
login()
|
login()
|
||||||
|
} catch (e: AuthenticationFailedException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.d(e, "LOGIN fallback failed")
|
||||||
|
throw originalException
|
||||||
|
} catch (e: MessagingException) {
|
||||||
|
Timber.d(e, "LOGIN fallback failed")
|
||||||
|
throw originalException
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -524,7 +543,11 @@ internal class RealImapConnection(
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationFailedException(negativeResponseException.message!!)
|
AuthenticationFailedException(
|
||||||
|
message = "Authentication failed",
|
||||||
|
throwable = negativeResponseException,
|
||||||
|
messageFromServer = ResponseTextExtractor.getResponseText(lastResponse),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
close()
|
close()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.fsck.k9.mail.store.imap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the response text from a (negative) status response.
|
||||||
|
*/
|
||||||
|
internal object ResponseTextExtractor {
|
||||||
|
private const val MINIMUM_RESPONSE_SIZE = 2
|
||||||
|
private const val RESPONSE_CODE_INDEX = 1
|
||||||
|
private const val SIMPLE_RESPONSE_TEXT_INDEX = 1
|
||||||
|
private const val EXTENDED_RESPONSE_TEXT_INDEX = 2
|
||||||
|
|
||||||
|
fun getResponseText(response: ImapResponse): String? {
|
||||||
|
if (response.size < MINIMUM_RESPONSE_SIZE) return null
|
||||||
|
|
||||||
|
val responseTextIndex = if (response.isList(RESPONSE_CODE_INDEX)) {
|
||||||
|
EXTENDED_RESPONSE_TEXT_INDEX
|
||||||
|
} else {
|
||||||
|
SIMPLE_RESPONSE_TEXT_INDEX
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (response.isString(responseTextIndex)) {
|
||||||
|
response.getString(responseTextIndex)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -182,8 +182,7 @@ class RealImapConnectionTest {
|
||||||
imapConnection.open()
|
imapConnection.open()
|
||||||
fail("Expected exception")
|
fail("Expected exception")
|
||||||
} catch (e: AuthenticationFailedException) {
|
} catch (e: AuthenticationFailedException) {
|
||||||
// FIXME: improve exception message
|
assertThat(e.messageFromServer).isEqualTo("Go away")
|
||||||
assertThat(e).hasMessageThat().contains("Go away")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.verifyConnectionClosed()
|
server.verifyConnectionClosed()
|
||||||
|
@ -231,8 +230,7 @@ class RealImapConnectionTest {
|
||||||
imapConnection.open()
|
imapConnection.open()
|
||||||
fail("Expected exception")
|
fail("Expected exception")
|
||||||
} catch (e: AuthenticationFailedException) {
|
} catch (e: AuthenticationFailedException) {
|
||||||
// FIXME: improve exception message
|
assertThat(e.messageFromServer).isEqualTo("Login Failure")
|
||||||
assertThat(e).hasMessageThat().contains("Login Failure")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.verifyConnectionClosed()
|
server.verifyConnectionClosed()
|
||||||
|
@ -288,8 +286,7 @@ class RealImapConnectionTest {
|
||||||
imapConnection.open()
|
imapConnection.open()
|
||||||
fail("Expected exception")
|
fail("Expected exception")
|
||||||
} catch (e: AuthenticationFailedException) {
|
} catch (e: AuthenticationFailedException) {
|
||||||
// FIXME: improve exception message
|
assertThat(e.messageFromServer).isEqualTo("Who are you?")
|
||||||
assertThat(e).hasMessageThat().contains("Who are you?")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.verifyConnectionClosed()
|
server.verifyConnectionClosed()
|
||||||
|
@ -395,8 +392,7 @@ class RealImapConnectionTest {
|
||||||
imapConnection.open()
|
imapConnection.open()
|
||||||
fail()
|
fail()
|
||||||
} catch (e: AuthenticationFailedException) {
|
} catch (e: AuthenticationFailedException) {
|
||||||
assertThat(e).hasMessageThat()
|
assertThat(e.messageFromServer).isEqualTo("SASL authentication failed")
|
||||||
.isEqualTo("Command: AUTHENTICATE XOAUTH2; response: #2# [NO, SASL authentication failed]")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,8 +478,7 @@ class RealImapConnectionTest {
|
||||||
imapConnection.open()
|
imapConnection.open()
|
||||||
fail()
|
fail()
|
||||||
} catch (e: AuthenticationFailedException) {
|
} catch (e: AuthenticationFailedException) {
|
||||||
assertThat(e).hasMessageThat()
|
assertThat(e.messageFromServer).isEqualTo("SASL authentication failed")
|
||||||
.isEqualTo("Command: AUTHENTICATE XOAUTH2; response: #3# [NO, SASL authentication failed]")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -995,6 +990,33 @@ class RealImapConnectionTest {
|
||||||
server.verifyInteractionCompleted()
|
server.verifyInteractionCompleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `disconnect during LOGIN fallback should throw AuthenticationFailedException`() {
|
||||||
|
val server = MockImapServer().apply {
|
||||||
|
output("* OK example.org server")
|
||||||
|
expect("1 CAPABILITY")
|
||||||
|
output("* CAPABILITY IMAP4 IMAP4REV1 AUTH=PLAIN")
|
||||||
|
output("1 OK CAPABILITY Completed")
|
||||||
|
expect("2 AUTHENTICATE PLAIN")
|
||||||
|
output("+")
|
||||||
|
expect("\u0000$USERNAME\u0000$PASSWORD".base64())
|
||||||
|
output("2 NO AUTHENTICATE failed")
|
||||||
|
expect("3 LOGIN \"$USERNAME\" \"$PASSWORD\"")
|
||||||
|
output("* BYE IMAP server terminating connection")
|
||||||
|
closeConnection()
|
||||||
|
}
|
||||||
|
val imapConnection = startServerAndCreateImapConnection(server)
|
||||||
|
|
||||||
|
try {
|
||||||
|
imapConnection.open()
|
||||||
|
fail("Expected exception")
|
||||||
|
} catch (e: AuthenticationFailedException) {
|
||||||
|
assertThat(e.messageFromServer).isEqualTo("AUTHENTICATE failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
server.verifyInteractionCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
private fun createImapConnection(
|
private fun createImapConnection(
|
||||||
settings: ImapSettings,
|
settings: ImapSettings,
|
||||||
socketFactory: TrustedSocketFactory,
|
socketFactory: TrustedSocketFactory,
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.fsck.k9.mail.store.imap
|
||||||
|
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import assertk.assertions.isNull
|
||||||
|
import com.fsck.k9.mail.store.imap.ImapResponseHelper.createImapResponse
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ResponseTextExtractorTest {
|
||||||
|
@Test
|
||||||
|
fun `response with response code and response text`() {
|
||||||
|
val imapResponse: ImapResponse = createImapResponse("x NO [AUTHENTICATIONFAILED] Authentication error #23")
|
||||||
|
|
||||||
|
val result = ResponseTextExtractor.getResponseText(imapResponse)
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("Authentication error #23")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `response with only response text`() {
|
||||||
|
val imapResponse: ImapResponse = createImapResponse("x NO AUTHENTICATE failed")
|
||||||
|
|
||||||
|
val result = ResponseTextExtractor.getResponseText(imapResponse)
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("AUTHENTICATE failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `response without response code or text`() {
|
||||||
|
val imapResponse: ImapResponse = createImapResponse("x NO")
|
||||||
|
|
||||||
|
val result = ResponseTextExtractor.getResponseText(imapResponse)
|
||||||
|
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `response with only a response code`() {
|
||||||
|
val imapResponse: ImapResponse = createImapResponse("x NO [AUTHENTICATIONFAILED]")
|
||||||
|
|
||||||
|
val result = ResponseTextExtractor.getResponseText(imapResponse)
|
||||||
|
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue