Merge pull request #6098 from k9mail/sasl_oauthbearer
Add support for the `OAUTHBEARER` SASL method
This commit is contained in:
commit
94c61a7999
9 changed files with 226 additions and 45 deletions
31
app/core/src/test/java/com/fsck/k9/sasl/OAuthBearerTest.kt
Normal file
31
app/core/src/test/java/com/fsck/k9/sasl/OAuthBearerTest.kt
Normal file
|
@ -0,0 +1,31 @@
|
|||
package com.fsck.k9.sasl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import okio.ByteString.Companion.decodeBase64
|
||||
import org.junit.Test
|
||||
|
||||
class OAuthBearerTest {
|
||||
@Test
|
||||
fun `username that does not need encoding`() {
|
||||
val username = "user@domain.example"
|
||||
val token = "token"
|
||||
|
||||
val result = buildOAuthBearerInitialClientResponse(username, token)
|
||||
|
||||
assertThat(result).isEqualTo("bixhPXVzZXJAZG9tYWluLmV4YW1wbGUsAWF1dGg9QmVhcmVyIHRva2VuAQE=")
|
||||
assertThat(result.decodeBase64()?.utf8())
|
||||
.isEqualTo("n,a=user@domain.example,\u0001auth=Bearer token\u0001\u0001")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `username contains equal sign that needs to be encoded`() {
|
||||
val username = "user=name@domain.example"
|
||||
val token = "token"
|
||||
|
||||
val result = buildOAuthBearerInitialClientResponse(username, token)
|
||||
|
||||
assertThat(result).isEqualTo("bixhPXVzZXI9M0RuYW1lQGRvbWFpbi5leGFtcGxlLAFhdXRoPUJlYXJlciB0b2tlbgEB")
|
||||
assertThat(result.decodeBase64()?.utf8())
|
||||
.isEqualTo("n,a=user=3Dname@domain.example,\u0001auth=Bearer token\u0001\u0001")
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import java.security.MessageDigest;
|
|||
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import com.fsck.k9.mail.filter.Hex;
|
||||
import okio.ByteString;
|
||||
|
||||
|
||||
public class Authentication {
|
||||
private static final String US_ASCII = "US-ASCII";
|
||||
|
@ -84,11 +86,8 @@ public class Authentication {
|
|||
}
|
||||
}
|
||||
|
||||
public static String computeXoauth(String username, String authToken) throws UnsupportedEncodingException {
|
||||
public static String computeXoauth(String username, String authToken) {
|
||||
String formattedAuthenticationString = String.format(XOAUTH_FORMAT, username, authToken);
|
||||
byte[] base64encodedAuthenticationString =
|
||||
Base64.encodeBase64(formattedAuthenticationString.getBytes());
|
||||
|
||||
return new String(base64encodedAuthenticationString, US_ASCII);
|
||||
return ByteString.encodeUtf8(formattedAuthenticationString).base64();
|
||||
}
|
||||
}
|
||||
|
|
14
mail/common/src/main/java/com/fsck/k9/sasl/OAuthBearer.kt
Normal file
14
mail/common/src/main/java/com/fsck/k9/sasl/OAuthBearer.kt
Normal file
|
@ -0,0 +1,14 @@
|
|||
@file:JvmName("OAuthBearer")
|
||||
package com.fsck.k9.sasl
|
||||
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
|
||||
/**
|
||||
* Builds an initial client response for the SASL `OAUTHBEARER` mechanism.
|
||||
*
|
||||
* See [RFC 7628](https://datatracker.ietf.org/doc/html/rfc7628).
|
||||
*/
|
||||
fun buildOAuthBearerInitialClientResponse(username: String, token: String): String {
|
||||
val saslName = username.replace(",", "=2C").replace("=", "=3D")
|
||||
return "n,a=$saslName,\u0001auth=Bearer $token\u0001\u0001".encodeUtf8().base64()
|
||||
}
|
|
@ -6,6 +6,7 @@ class Capabilities {
|
|||
public static final String CONDSTORE = "CONDSTORE";
|
||||
public static final String SASL_IR = "SASL-IR";
|
||||
public static final String AUTH_XOAUTH2 = "AUTH=XOAUTH2";
|
||||
public static final String AUTH_OAUTHBEARER = "AUTH=OAUTHBEARER";
|
||||
public static final String AUTH_CRAM_MD5 = "AUTH=CRAM-MD5";
|
||||
public static final String AUTH_PLAIN = "AUTH=PLAIN";
|
||||
public static final String AUTH_EXTERNAL = "AUTH=EXTERNAL";
|
||||
|
|
|
@ -8,6 +8,7 @@ class Commands {
|
|||
public static final String COMPRESS_DEFLATE = "COMPRESS DEFLATE";
|
||||
public static final String STARTTLS = "STARTTLS";
|
||||
public static final String AUTHENTICATE_XOAUTH2 = "AUTHENTICATE XOAUTH2";
|
||||
public static final String AUTHENTICATE_OAUTHBEARER = "AUTHENTICATE OAUTHBEARER";
|
||||
public static final String AUTHENTICATE_CRAM_MD5 = "AUTHENTICATE CRAM-MD5";
|
||||
public static final String AUTHENTICATE_PLAIN = "AUTHENTICATE PLAIN";
|
||||
public static final String AUTHENTICATE_EXTERNAL = "AUTHENTICATE EXTERNAL";
|
||||
|
|
|
@ -40,6 +40,7 @@ import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
|||
import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.imap.IdGrouper.GroupedIds;
|
||||
import com.fsck.k9.sasl.OAuthBearer;
|
||||
import com.jcraft.jzlib.JZlib;
|
||||
import com.jcraft.jzlib.ZOutputStream;
|
||||
import javax.net.ssl.SSLException;
|
||||
|
@ -88,7 +89,7 @@ class RealImapConnection implements ImapConnection {
|
|||
private ImapSettings settings;
|
||||
private Exception stacktraceForClose;
|
||||
private boolean open = false;
|
||||
private boolean retryXoauth2WithNewToken = true;
|
||||
private boolean retryOAuthWithNewToken = true;
|
||||
|
||||
|
||||
public RealImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory,
|
||||
|
@ -357,10 +358,14 @@ class RealImapConnection implements ImapConnection {
|
|||
case XOAUTH2:
|
||||
if (oauthTokenProvider == null) {
|
||||
throw new MessagingException("No OAuthToken Provider available.");
|
||||
} else if (hasCapability(Capabilities.AUTH_XOAUTH2) && hasCapability(Capabilities.SASL_IR)) {
|
||||
return authXoauth2withSASLIR();
|
||||
} else if (!hasCapability(Capabilities.SASL_IR)) {
|
||||
throw new MessagingException("SASL-IR capability is missing.");
|
||||
} else if (hasCapability(Capabilities.AUTH_OAUTHBEARER)) {
|
||||
return authWithOAuthToken(OAuthMethod.OAUTHBEARER);
|
||||
} else if (hasCapability(Capabilities.AUTH_XOAUTH2)) {
|
||||
return authWithOAuthToken(OAuthMethod.XOAUTH2);
|
||||
} else {
|
||||
throw new MessagingException("Server doesn't support SASL XOAUTH2.");
|
||||
throw new MessagingException("Server doesn't support SASL OAUTHBEARER or XOAUTH2.");
|
||||
}
|
||||
case CRAM_MD5: {
|
||||
if (hasCapability(Capabilities.AUTH_CRAM_MD5)) {
|
||||
|
@ -393,66 +398,67 @@ class RealImapConnection implements ImapConnection {
|
|||
}
|
||||
}
|
||||
|
||||
private List<ImapResponse> authXoauth2withSASLIR() throws IOException, MessagingException {
|
||||
retryXoauth2WithNewToken = true;
|
||||
private List<ImapResponse> authWithOAuthToken(OAuthMethod method) throws IOException, MessagingException {
|
||||
retryOAuthWithNewToken = true;
|
||||
try {
|
||||
return attemptXOAuth2();
|
||||
return attemptOAuth(method);
|
||||
} catch (NegativeImapResponseException e) {
|
||||
//TODO: Check response code so we don't needlessly invalidate the token.
|
||||
oauthTokenProvider.invalidateToken();
|
||||
|
||||
if (!retryXoauth2WithNewToken) {
|
||||
throw handlePermanentXoauth2Failure(e);
|
||||
if (!retryOAuthWithNewToken) {
|
||||
throw handlePermanentOAuthFailure(e);
|
||||
} else {
|
||||
return handleTemporaryXoauth2Failure(e);
|
||||
return handleTemporaryOAuthFailure(method, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticationFailedException handlePermanentXoauth2Failure(NegativeImapResponseException e) {
|
||||
Timber.v(e, "Permanent failure during XOAUTH2");
|
||||
private AuthenticationFailedException handlePermanentOAuthFailure(NegativeImapResponseException e) {
|
||||
Timber.v(e, "Permanent failure during authentication using OAuth token");
|
||||
return new AuthenticationFailedException(e.getMessage(), e, e.getAlertText());
|
||||
}
|
||||
|
||||
private List<ImapResponse> handleTemporaryXoauth2Failure(NegativeImapResponseException e) throws IOException, MessagingException {
|
||||
//We got a response indicating a retry might suceed after token refresh
|
||||
private List<ImapResponse> handleTemporaryOAuthFailure(OAuthMethod method, NegativeImapResponseException e)
|
||||
throws IOException, MessagingException {
|
||||
//We got a response indicating a retry might succeed after token refresh
|
||||
//We could avoid this 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
|
||||
|
||||
Timber.v(e, "Temporary failure - retrying with new token");
|
||||
try {
|
||||
return attemptXOAuth2();
|
||||
return attemptOAuth(method);
|
||||
} catch (NegativeImapResponseException e2) {
|
||||
//Okay, we failed on a new token.
|
||||
//Invalidate the token anyway but assume it's permanent.
|
||||
Timber.v(e, "Authentication exception for new token, permanent error assumed");
|
||||
oauthTokenProvider.invalidateToken();
|
||||
throw handlePermanentXoauth2Failure(e2);
|
||||
throw handlePermanentOAuthFailure(e2);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ImapResponse> attemptXOAuth2() throws MessagingException, IOException {
|
||||
private List<ImapResponse> attemptOAuth(OAuthMethod method) throws MessagingException, IOException {
|
||||
String token = oauthTokenProvider.getToken(OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
String authString = Authentication.computeXoauth(settings.getUsername(), token);
|
||||
String tag = sendSaslIrCommand(Commands.AUTHENTICATE_XOAUTH2, authString, true);
|
||||
String authString = method.buildInitialClientResponse(settings.getUsername(), token);
|
||||
String tag = sendSaslIrCommand(method.getCommand(), authString, true);
|
||||
|
||||
return responseParser.readStatusResponse(tag, Commands.AUTHENTICATE_XOAUTH2, getLogId(),
|
||||
return responseParser.readStatusResponse(tag, method.getCommand(), getLogId(),
|
||||
new UntaggedHandler() {
|
||||
@Override
|
||||
public void handleAsyncUntaggedResponse(ImapResponse response) throws IOException {
|
||||
handleXOAuthUntaggedResponse(response);
|
||||
handleOAuthUntaggedResponse(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleXOAuthUntaggedResponse(ImapResponse response) throws IOException {
|
||||
private void handleOAuthUntaggedResponse(ImapResponse response) throws IOException {
|
||||
if (!response.isContinuationRequested()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.isString(0)) {
|
||||
retryXoauth2WithNewToken = XOAuth2ChallengeParser.shouldRetry(response.getString(0), settings.getHost());
|
||||
retryOAuthWithNewToken = XOAuth2ChallengeParser.shouldRetry(response.getString(0), settings.getHost());
|
||||
}
|
||||
|
||||
outputStream.write("\r\n".getBytes());
|
||||
|
@ -891,4 +897,33 @@ class RealImapConnection implements ImapConnection {
|
|||
public int getConnectionGeneration() {
|
||||
return connectionGeneration;
|
||||
}
|
||||
|
||||
|
||||
private enum OAuthMethod {
|
||||
XOAUTH2 {
|
||||
@Override
|
||||
String getCommand() {
|
||||
return Commands.AUTHENTICATE_XOAUTH2;
|
||||
}
|
||||
|
||||
@Override
|
||||
String buildInitialClientResponse(String username, String token) {
|
||||
return Authentication.computeXoauth(username, token);
|
||||
}
|
||||
},
|
||||
OAUTHBEARER {
|
||||
@Override
|
||||
String getCommand() {
|
||||
return Commands.AUTHENTICATE_OAUTHBEARER;
|
||||
}
|
||||
|
||||
@Override
|
||||
String buildInitialClientResponse(String username, String token) {
|
||||
return OAuthBearer.buildOAuthBearerInitialClientResponse(username, token);
|
||||
}
|
||||
};
|
||||
|
||||
abstract String getCommand();
|
||||
abstract String buildInitialClientResponse(String username, String token);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ private const val XOAUTH_TOKEN = "token"
|
|||
private const val XOAUTH_TOKEN_2 = "token2"
|
||||
private val XOAUTH_STRING = "user=$USERNAME\u0001auth=Bearer $XOAUTH_TOKEN\u0001\u0001".base64()
|
||||
private val XOAUTH_STRING_RETRY = "user=$USERNAME\u0001auth=Bearer $XOAUTH_TOKEN_2\u0001\u0001".base64()
|
||||
private val OAUTHBEARER_STRING = "n,a=$USERNAME,\u0001auth=Bearer $XOAUTH_TOKEN\u0001\u0001".base64()
|
||||
|
||||
class RealImapConnectionTest {
|
||||
private var socketFactory = TestTrustedSocketFactory.newInstance()
|
||||
|
@ -317,6 +318,38 @@ class RealImapConnectionTest {
|
|||
server.verifyInteractionCompleted()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `open() AUTH OAUTHBEARER`() {
|
||||
val server = MockImapServer().apply {
|
||||
preAuthenticationDialog(capabilities = "SASL-IR AUTH=OAUTHBEARER")
|
||||
expect("2 AUTHENTICATE OAUTHBEARER $OAUTHBEARER_STRING")
|
||||
output("2 OK Success")
|
||||
postAuthenticationDialogRequestingCapabilities()
|
||||
}
|
||||
val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2)
|
||||
|
||||
imapConnection.open()
|
||||
|
||||
server.verifyConnectionStillOpen()
|
||||
server.verifyInteractionCompleted()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `open() AUTH OAUTHBEARER when AUTH=XOAUTH2 and AUTH=OAUTHBEARER capabilities are present`() {
|
||||
val server = MockImapServer().apply {
|
||||
preAuthenticationDialog(capabilities = "SASL-IR AUTH=XOAUTH2 AUTH=OAUTHBEARER")
|
||||
expect("2 AUTHENTICATE OAUTHBEARER $OAUTHBEARER_STRING")
|
||||
output("2 OK Success")
|
||||
postAuthenticationDialogRequestingCapabilities()
|
||||
}
|
||||
val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2)
|
||||
|
||||
imapConnection.open()
|
||||
|
||||
server.verifyConnectionStillOpen()
|
||||
server.verifyInteractionCompleted()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `open() AUTH XOAUTH2 with SASL-IR`() {
|
||||
val server = MockImapServer().apply {
|
||||
|
|
|
@ -24,6 +24,7 @@ import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
|||
import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpHelloResponse.Hello
|
||||
import com.fsck.k9.sasl.buildOAuthBearerInitialClientResponse
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.IOException
|
||||
|
@ -61,7 +62,7 @@ class SmtpTransport(
|
|||
private var is8bitEncodingAllowed = false
|
||||
private var isEnhancedStatusCodesProvided = false
|
||||
private var largestAcceptableMessage = 0
|
||||
private var retryXoauthWithNewToken = false
|
||||
private var retryOAuthWithNewToken = false
|
||||
private var isPipeliningSupported = false
|
||||
|
||||
private val logger: SmtpLogger = object : SmtpLogger {
|
||||
|
@ -157,6 +158,7 @@ class SmtpTransport(
|
|||
var authCramMD5Supported = false
|
||||
var authExternalSupported = false
|
||||
var authXoauth2Supported = false
|
||||
var authOAuthBearerSupported = false
|
||||
val saslMechanisms = extensions["AUTH"]
|
||||
if (saslMechanisms != null) {
|
||||
authLoginSupported = saslMechanisms.contains("LOGIN")
|
||||
|
@ -164,6 +166,7 @@ class SmtpTransport(
|
|||
authCramMD5Supported = saslMechanisms.contains("CRAM-MD5")
|
||||
authExternalSupported = saslMechanisms.contains("EXTERNAL")
|
||||
authXoauth2Supported = saslMechanisms.contains("XOAUTH2")
|
||||
authOAuthBearerSupported = saslMechanisms.contains("OAUTHBEARER")
|
||||
}
|
||||
parseOptionalSizeValue(extensions["SIZE"])
|
||||
|
||||
|
@ -190,10 +193,14 @@ class SmtpTransport(
|
|||
}
|
||||
}
|
||||
AuthType.XOAUTH2 -> {
|
||||
if (authXoauth2Supported && oauthTokenProvider != null) {
|
||||
saslXoauth2()
|
||||
if (oauthTokenProvider == null) {
|
||||
throw MessagingException("No OAuth2TokenProvider available.")
|
||||
} else if (authOAuthBearerSupported) {
|
||||
saslOAuth(OAuthMethod.OAUTHBEARER)
|
||||
} else if (authXoauth2Supported) {
|
||||
saslOAuth(OAuthMethod.XOAUTH2)
|
||||
} else {
|
||||
throw MessagingException("Authentication method XOAUTH2 is unavailable.")
|
||||
throw MessagingException("Server doesn't support SASL OAUTHBEARER or XOAUTH2.")
|
||||
}
|
||||
}
|
||||
AuthType.EXTERNAL -> {
|
||||
|
@ -550,10 +557,10 @@ class SmtpTransport(
|
|||
}
|
||||
}
|
||||
|
||||
private fun saslXoauth2() {
|
||||
retryXoauthWithNewToken = true
|
||||
private fun saslOAuth(method: OAuthMethod) {
|
||||
retryOAuthWithNewToken = true
|
||||
try {
|
||||
attemptXoauth2(username)
|
||||
attempOAuth(method, username)
|
||||
} catch (negativeResponse: NegativeSmtpReplyException) {
|
||||
if (negativeResponse.replyCode != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
throw negativeResponse
|
||||
|
@ -561,10 +568,10 @@ class SmtpTransport(
|
|||
|
||||
oauthTokenProvider!!.invalidateToken()
|
||||
|
||||
if (!retryXoauthWithNewToken) {
|
||||
if (!retryOAuthWithNewToken) {
|
||||
handlePermanentFailure(negativeResponse)
|
||||
} else {
|
||||
handleTemporaryFailure(username, negativeResponse)
|
||||
handleTemporaryFailure(method, username, negativeResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -573,13 +580,17 @@ class SmtpTransport(
|
|||
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
|
||||
// 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")
|
||||
|
||||
try {
|
||||
attemptXoauth2(username)
|
||||
attempOAuth(method, username)
|
||||
} catch (negativeResponseFromNewToken: NegativeSmtpReplyException) {
|
||||
if (negativeResponseFromNewToken.replyCode != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
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 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) {
|
||||
val replyText = response.joinedText
|
||||
retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, host)
|
||||
retryOAuthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, host)
|
||||
|
||||
// Per Google spec, respond to challenge with empty response
|
||||
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()
|
||||
}
|
||||
|
||||
@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
|
||||
fun `open() with XOAUTH2 extension`() {
|
||||
val server = MockSmtpServer().apply {
|
||||
|
@ -386,7 +422,7 @@ class SmtpTransportTest {
|
|||
transport.open()
|
||||
fail("Exception expected")
|
||||
} 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()
|
||||
|
@ -546,7 +582,7 @@ class SmtpTransportTest {
|
|||
output("250-smtp.gmail.com at your service, [86.147.34.216]")
|
||||
output("250-SIZE 35882577")
|
||||
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-PIPELINING")
|
||||
output("250-CHUNKING")
|
||||
|
|
Loading…
Reference in a new issue