Merge pull request #6762 from thundernest/imap_authentication_failure

IMAP: Ignore errors during LOGIN fallback
This commit is contained in:
cketti 2023-03-17 16:38:31 +01:00 committed by GitHub
commit 709b55f2d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 130 additions and 13 deletions

View file

@ -415,7 +415,7 @@ class AccountSetupCheckSettings : K9Activity(), ConfirmationDialogFragmentListen
finish()
} catch (e: AuthenticationFailedException) {
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) {
handleCertificateValidationException(e)
} catch (e: Exception) {

View file

@ -359,7 +359,11 @@ internal class RealImapConnection(
private fun handlePermanentOAuthFailure(e: NegativeImapResponseException): AuthenticationFailedException {
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> {
@ -445,7 +449,22 @@ internal class RealImapConnection(
throw e
}
loginOrThrow(e)
}
}
@Suppress("ThrowsCount")
private fun loginOrThrow(originalException: AuthenticationFailedException): List<ImapResponse> {
return try {
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()
}
AuthenticationFailedException(negativeResponseException.message!!)
AuthenticationFailedException(
message = "Authentication failed",
throwable = negativeResponseException,
messageFromServer = ResponseTextExtractor.getResponseText(lastResponse),
)
} else {
close()

View file

@ -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
}
}
}

View file

@ -182,8 +182,7 @@ class RealImapConnectionTest {
imapConnection.open()
fail("Expected exception")
} catch (e: AuthenticationFailedException) {
// FIXME: improve exception message
assertThat(e).hasMessageThat().contains("Go away")
assertThat(e.messageFromServer).isEqualTo("Go away")
}
server.verifyConnectionClosed()
@ -231,8 +230,7 @@ class RealImapConnectionTest {
imapConnection.open()
fail("Expected exception")
} catch (e: AuthenticationFailedException) {
// FIXME: improve exception message
assertThat(e).hasMessageThat().contains("Login Failure")
assertThat(e.messageFromServer).isEqualTo("Login Failure")
}
server.verifyConnectionClosed()
@ -288,8 +286,7 @@ class RealImapConnectionTest {
imapConnection.open()
fail("Expected exception")
} catch (e: AuthenticationFailedException) {
// FIXME: improve exception message
assertThat(e).hasMessageThat().contains("Who are you?")
assertThat(e.messageFromServer).isEqualTo("Who are you?")
}
server.verifyConnectionClosed()
@ -395,8 +392,7 @@ class RealImapConnectionTest {
imapConnection.open()
fail()
} catch (e: AuthenticationFailedException) {
assertThat(e).hasMessageThat()
.isEqualTo("Command: AUTHENTICATE XOAUTH2; response: #2# [NO, SASL authentication failed]")
assertThat(e.messageFromServer).isEqualTo("SASL authentication failed")
}
}
@ -482,8 +478,7 @@ class RealImapConnectionTest {
imapConnection.open()
fail()
} catch (e: AuthenticationFailedException) {
assertThat(e).hasMessageThat()
.isEqualTo("Command: AUTHENTICATE XOAUTH2; response: #3# [NO, SASL authentication failed]")
assertThat(e.messageFromServer).isEqualTo("SASL authentication failed")
}
}
@ -995,6 +990,33 @@ class RealImapConnectionTest {
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(
settings: ImapSettings,
socketFactory: TrustedSocketFactory,

View file

@ -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()
}
}