mirror of
https://github.com/thunderbird/thunderbird-android.git
synced 2024-09-20 04:02:14 +02:00
Add support for the OAUTHBEARER SASL method (SMTP)
This commit is contained in:
parent
d3be6e249b
commit
bbd104f38a
@ -24,6 +24,7 @@ import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
|||||||
import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser
|
import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser
|
||||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||||
import com.fsck.k9.mail.transport.smtp.SmtpHelloResponse.Hello
|
import com.fsck.k9.mail.transport.smtp.SmtpHelloResponse.Hello
|
||||||
|
import com.fsck.k9.sasl.buildOAuthBearerInitialClientResponse
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -61,7 +62,7 @@ class SmtpTransport(
|
|||||||
private var is8bitEncodingAllowed = false
|
private var is8bitEncodingAllowed = false
|
||||||
private var isEnhancedStatusCodesProvided = false
|
private var isEnhancedStatusCodesProvided = false
|
||||||
private var largestAcceptableMessage = 0
|
private var largestAcceptableMessage = 0
|
||||||
private var retryXoauthWithNewToken = false
|
private var retryOAuthWithNewToken = false
|
||||||
private var isPipeliningSupported = false
|
private var isPipeliningSupported = false
|
||||||
|
|
||||||
private val logger: SmtpLogger = object : SmtpLogger {
|
private val logger: SmtpLogger = object : SmtpLogger {
|
||||||
@ -157,6 +158,7 @@ class SmtpTransport(
|
|||||||
var authCramMD5Supported = false
|
var authCramMD5Supported = false
|
||||||
var authExternalSupported = false
|
var authExternalSupported = false
|
||||||
var authXoauth2Supported = false
|
var authXoauth2Supported = false
|
||||||
|
var authOAuthBearerSupported = false
|
||||||
val saslMechanisms = extensions["AUTH"]
|
val saslMechanisms = extensions["AUTH"]
|
||||||
if (saslMechanisms != null) {
|
if (saslMechanisms != null) {
|
||||||
authLoginSupported = saslMechanisms.contains("LOGIN")
|
authLoginSupported = saslMechanisms.contains("LOGIN")
|
||||||
@ -164,6 +166,7 @@ class SmtpTransport(
|
|||||||
authCramMD5Supported = saslMechanisms.contains("CRAM-MD5")
|
authCramMD5Supported = saslMechanisms.contains("CRAM-MD5")
|
||||||
authExternalSupported = saslMechanisms.contains("EXTERNAL")
|
authExternalSupported = saslMechanisms.contains("EXTERNAL")
|
||||||
authXoauth2Supported = saslMechanisms.contains("XOAUTH2")
|
authXoauth2Supported = saslMechanisms.contains("XOAUTH2")
|
||||||
|
authOAuthBearerSupported = saslMechanisms.contains("OAUTHBEARER")
|
||||||
}
|
}
|
||||||
parseOptionalSizeValue(extensions["SIZE"])
|
parseOptionalSizeValue(extensions["SIZE"])
|
||||||
|
|
||||||
@ -190,10 +193,14 @@ class SmtpTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
AuthType.XOAUTH2 -> {
|
AuthType.XOAUTH2 -> {
|
||||||
if (authXoauth2Supported && oauthTokenProvider != null) {
|
if (oauthTokenProvider == null) {
|
||||||
saslXoauth2()
|
throw MessagingException("No OAuth2TokenProvider available.")
|
||||||
|
} else if (authOAuthBearerSupported) {
|
||||||
|
saslOAuth(OAuthMethod.OAUTHBEARER)
|
||||||
|
} else if (authXoauth2Supported) {
|
||||||
|
saslOAuth(OAuthMethod.XOAUTH2)
|
||||||
} else {
|
} else {
|
||||||
throw MessagingException("Authentication method XOAUTH2 is unavailable.")
|
throw MessagingException("Server doesn't support SASL OAUTHBEARER or XOAUTH2.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AuthType.EXTERNAL -> {
|
AuthType.EXTERNAL -> {
|
||||||
@ -550,10 +557,10 @@ class SmtpTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saslXoauth2() {
|
private fun saslOAuth(method: OAuthMethod) {
|
||||||
retryXoauthWithNewToken = true
|
retryOAuthWithNewToken = true
|
||||||
try {
|
try {
|
||||||
attemptXoauth2(username)
|
attempOAuth(method, username)
|
||||||
} catch (negativeResponse: NegativeSmtpReplyException) {
|
} catch (negativeResponse: NegativeSmtpReplyException) {
|
||||||
if (negativeResponse.replyCode != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
if (negativeResponse.replyCode != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||||
throw negativeResponse
|
throw negativeResponse
|
||||||
@ -561,10 +568,10 @@ class SmtpTransport(
|
|||||||
|
|
||||||
oauthTokenProvider!!.invalidateToken()
|
oauthTokenProvider!!.invalidateToken()
|
||||||
|
|
||||||
if (!retryXoauthWithNewToken) {
|
if (!retryOAuthWithNewToken) {
|
||||||
handlePermanentFailure(negativeResponse)
|
handlePermanentFailure(negativeResponse)
|
||||||
} else {
|
} else {
|
||||||
handleTemporaryFailure(username, negativeResponse)
|
handleTemporaryFailure(method, username, negativeResponse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -573,13 +580,17 @@ class SmtpTransport(
|
|||||||
throw AuthenticationFailedException(negativeResponse.message!!, negativeResponse)
|
throw AuthenticationFailedException(negativeResponse.message!!, negativeResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleTemporaryFailure(username: String, negativeResponseFromOldToken: NegativeSmtpReplyException) {
|
private fun handleTemporaryFailure(
|
||||||
|
method: OAuthMethod,
|
||||||
|
username: String,
|
||||||
|
negativeResponseFromOldToken: NegativeSmtpReplyException
|
||||||
|
) {
|
||||||
// Token was invalid. We could avoid this double check if we had a reasonable chance of knowing if a token was
|
// Token was invalid. We could avoid this double check if we had a reasonable chance of knowing if a token was
|
||||||
// invalid before use (e.g. due to expiry). But we don't. This is the intended behaviour per AccountManager.
|
// invalid before use (e.g. due to expiry). But we don't. This is the intended behaviour per AccountManager.
|
||||||
Timber.v(negativeResponseFromOldToken, "Authentication exception, re-trying with new token")
|
Timber.v(negativeResponseFromOldToken, "Authentication exception, re-trying with new token")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
attemptXoauth2(username)
|
attempOAuth(method, username)
|
||||||
} catch (negativeResponseFromNewToken: NegativeSmtpReplyException) {
|
} catch (negativeResponseFromNewToken: NegativeSmtpReplyException) {
|
||||||
if (negativeResponseFromNewToken.replyCode != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
if (negativeResponseFromNewToken.replyCode != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||||
throw negativeResponseFromNewToken
|
throw negativeResponseFromNewToken
|
||||||
@ -593,14 +604,14 @@ class SmtpTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun attemptXoauth2(username: String) {
|
private fun attempOAuth(method: OAuthMethod, username: String) {
|
||||||
val token = oauthTokenProvider!!.getToken(OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong())
|
val token = oauthTokenProvider!!.getToken(OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong())
|
||||||
val authString = Authentication.computeXoauth(username, token)
|
val authString = method.buildInitialClientResponse(username, token)
|
||||||
|
|
||||||
val response = executeSensitiveCommand("AUTH XOAUTH2 %s", authString)
|
val response = executeSensitiveCommand("%s %s", method.command, authString)
|
||||||
if (response.replyCode == SMTP_CONTINUE_REQUEST) {
|
if (response.replyCode == SMTP_CONTINUE_REQUEST) {
|
||||||
val replyText = response.joinedText
|
val replyText = response.joinedText
|
||||||
retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, host)
|
retryOAuthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, host)
|
||||||
|
|
||||||
// Per Google spec, respond to challenge with empty response
|
// Per Google spec, respond to challenge with empty response
|
||||||
executeCommand("")
|
executeCommand("")
|
||||||
@ -622,3 +633,23 @@ class SmtpTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum class OAuthMethod {
|
||||||
|
XOAUTH2 {
|
||||||
|
override val command = "AUTH XOAUTH2"
|
||||||
|
|
||||||
|
override fun buildInitialClientResponse(username: String, token: String): String {
|
||||||
|
return Authentication.computeXoauth(username, token)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OAUTHBEARER {
|
||||||
|
override val command = "AUTH OAUTHBEARER"
|
||||||
|
|
||||||
|
override fun buildInitialClientResponse(username: String, token: String): String {
|
||||||
|
return buildOAuthBearerInitialClientResponse(username, token)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
abstract val command: String
|
||||||
|
abstract fun buildInitialClientResponse(username: String, token: String): String
|
||||||
|
}
|
||||||
|
@ -170,6 +170,42 @@ class SmtpTransportTest {
|
|||||||
server.verifyInteractionCompleted()
|
server.verifyInteractionCompleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `open() with OAUTHBEARER method`() {
|
||||||
|
val server = MockSmtpServer().apply {
|
||||||
|
output("220 localhost Simple Mail Transfer Service Ready")
|
||||||
|
expect("EHLO [127.0.0.1]")
|
||||||
|
output("250-localhost Hello client.localhost")
|
||||||
|
output("250 AUTH OAUTHBEARER")
|
||||||
|
expect("AUTH OAUTHBEARER bixhPXVzZXIsAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=")
|
||||||
|
output("235 2.7.0 Authentication successful")
|
||||||
|
}
|
||||||
|
val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2)
|
||||||
|
|
||||||
|
transport.open()
|
||||||
|
|
||||||
|
server.verifyConnectionStillOpen()
|
||||||
|
server.verifyInteractionCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `open() with OAUTHBEARER method when XOAUTH2 method is also available`() {
|
||||||
|
val server = MockSmtpServer().apply {
|
||||||
|
output("220 localhost Simple Mail Transfer Service Ready")
|
||||||
|
expect("EHLO [127.0.0.1]")
|
||||||
|
output("250-localhost Hello client.localhost")
|
||||||
|
output("250 AUTH XOAUTH2 OAUTHBEARER")
|
||||||
|
expect("AUTH OAUTHBEARER bixhPXVzZXIsAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=")
|
||||||
|
output("235 2.7.0 Authentication successful")
|
||||||
|
}
|
||||||
|
val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2)
|
||||||
|
|
||||||
|
transport.open()
|
||||||
|
|
||||||
|
server.verifyConnectionStillOpen()
|
||||||
|
server.verifyInteractionCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `open() with XOAUTH2 extension`() {
|
fun `open() with XOAUTH2 extension`() {
|
||||||
val server = MockSmtpServer().apply {
|
val server = MockSmtpServer().apply {
|
||||||
@ -386,7 +422,7 @@ class SmtpTransportTest {
|
|||||||
transport.open()
|
transport.open()
|
||||||
fail("Exception expected")
|
fail("Exception expected")
|
||||||
} catch (e: MessagingException) {
|
} catch (e: MessagingException) {
|
||||||
assertThat(e).hasMessageThat().isEqualTo("Authentication method XOAUTH2 is unavailable.")
|
assertThat(e).hasMessageThat().isEqualTo("Server doesn't support SASL OAUTHBEARER or XOAUTH2.")
|
||||||
}
|
}
|
||||||
|
|
||||||
server.verifyConnectionClosed()
|
server.verifyConnectionClosed()
|
||||||
@ -546,7 +582,7 @@ class SmtpTransportTest {
|
|||||||
output("250-smtp.gmail.com at your service, [86.147.34.216]")
|
output("250-smtp.gmail.com at your service, [86.147.34.216]")
|
||||||
output("250-SIZE 35882577")
|
output("250-SIZE 35882577")
|
||||||
output("250-8BITMIME")
|
output("250-8BITMIME")
|
||||||
output("250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH")
|
output("250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN XOAUTH")
|
||||||
output("250-ENHANCEDSTATUSCODES")
|
output("250-ENHANCEDSTATUSCODES")
|
||||||
output("250-PIPELINING")
|
output("250-PIPELINING")
|
||||||
output("250-CHUNKING")
|
output("250-CHUNKING")
|
||||||
|
Loading…
Reference in New Issue
Block a user