Back-end changes for Google XOAUTH2
This commit is contained in:
parent
0342648568
commit
7774ebc788
22 changed files with 1020 additions and 72 deletions
|
@ -16,6 +16,13 @@ public enum AuthType {
|
|||
CRAM_MD5,
|
||||
EXTERNAL,
|
||||
|
||||
/**
|
||||
* XOAUTH2 is an OAuth2.0 protocol designed/used by GMail.
|
||||
*
|
||||
* https://developers.google.com/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism
|
||||
*/
|
||||
XOAUTH2,
|
||||
|
||||
/*
|
||||
* The following are obsolete authentication settings that were used with
|
||||
* SMTP. They are no longer presented to the user as options, but they may
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.fsck.k9.mail;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
|
@ -7,6 +8,7 @@ import com.fsck.k9.mail.filter.Hex;
|
|||
|
||||
public class Authentication {
|
||||
private static final String US_ASCII = "US-ASCII";
|
||||
private static final String XOAUTH_FORMAT = "user=%1s\001auth=Bearer %2s\001\001";
|
||||
|
||||
/**
|
||||
* Computes the response for CRAM-MD5 authentication mechanism given the user credentials and
|
||||
|
@ -82,4 +84,10 @@ public class Authentication {
|
|||
throw new MessagingException("Something went wrong during CRAM-MD5 computation", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String computeXoauth(String username, String authToken) throws UnsupportedEncodingException {
|
||||
return new String(
|
||||
Base64.encodeBase64(String.format(XOAUTH_FORMAT, username, authToken).getBytes()),
|
||||
US_ASCII);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.fsck.k9.mail;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
import com.fsck.k9.mail.ServerSettings.Type;
|
||||
|
@ -19,10 +20,12 @@ public abstract class Transport {
|
|||
// RFC 1047
|
||||
protected static final int SOCKET_READ_TIMEOUT = 300000;
|
||||
|
||||
public synchronized static Transport getInstance(Context context, StoreConfig storeConfig) throws MessagingException {
|
||||
public synchronized static Transport getInstance(Context context, StoreConfig storeConfig,
|
||||
OAuth2TokenProvider oauth2TokenProvider) throws MessagingException {
|
||||
String uri = storeConfig.getTransportUri();
|
||||
if (uri.startsWith("smtp")) {
|
||||
return new SmtpTransport(storeConfig, new DefaultTrustedSocketFactory(context));
|
||||
return new SmtpTransport(storeConfig, new DefaultTrustedSocketFactory(context),
|
||||
oauth2TokenProvider);
|
||||
} else if (uri.startsWith("webdav")) {
|
||||
return new WebDavTransport(storeConfig);
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package com.fsck.k9.mail.oauth;
|
||||
|
||||
public class AuthorizationException extends Exception {
|
||||
public AuthorizationException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
public AuthorizationException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package com.fsck.k9.mail.oauth;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface OAuth2TokenProvider {
|
||||
|
||||
/**
|
||||
* A default timeout value to use when fetching tokens.
|
||||
*/
|
||||
public static final int OAUTH2_TIMEOUT = 30000;
|
||||
|
||||
/**
|
||||
* @return Accounts suitable for OAuth 2.0 token provision.
|
||||
*/
|
||||
List<String> getAccounts();
|
||||
|
||||
/**
|
||||
* Provides an asynchronous response to an
|
||||
* {@link OAuth2TokenProvider#authorizeAPI(String, Activity, OAuth2TokenProviderAuthCallback)} request
|
||||
*/
|
||||
interface OAuth2TokenProviderAuthCallback {
|
||||
|
||||
void success();
|
||||
|
||||
void failure(AuthorizationException e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request API authorization. This is a foreground action that may produce a dialog to interact with.
|
||||
* @param username Username
|
||||
* @param activity The responsible activity
|
||||
* @param callback A callback to process the asynchronous response
|
||||
*/
|
||||
void authorizeAPI(String username, Activity activity,
|
||||
OAuth2TokenProviderAuthCallback callback);
|
||||
|
||||
/**
|
||||
* Fetch a token. No guarantees are provided for validity.
|
||||
* @param username Username
|
||||
* @return Token string
|
||||
* @throws AuthenticationFailedException
|
||||
*/
|
||||
String getToken(String username, long timeoutMillis) throws AuthenticationFailedException;
|
||||
|
||||
/**
|
||||
* Invalidate the token for this username.
|
||||
* Note that the token should always be invalidated on credential failure.
|
||||
* However invalidating a token every single time is not recommended.
|
||||
*
|
||||
* Invalidating a token and then failure with a new token
|
||||
* should be treated as a permanent failure.
|
||||
*
|
||||
* @param username
|
||||
*/
|
||||
void invalidateToken(String username);
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package com.fsck.k9.mail.oauth;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
||||
|
||||
/**
|
||||
* Parses Google's Error/Challenge responses
|
||||
* See: https://developers.google.com/gmail/xoauth2_protocol#error_response
|
||||
*/
|
||||
public class XOAuth2ChallengeParser {
|
||||
public static final String BAD_RESPONSE = "400";
|
||||
|
||||
public static boolean shouldRetry(String response, String host) {
|
||||
String decodedResponse = Base64.decode(response);
|
||||
|
||||
Log.v(LOG_TAG, "Challenge response: "+ decodedResponse);
|
||||
|
||||
try {
|
||||
JSONObject json = new JSONObject(decodedResponse);
|
||||
if(!json.getString("status").equals(BAD_RESPONSE)) {
|
||||
return false;
|
||||
}
|
||||
} catch (JSONException jsonException) {
|
||||
Log.i(LOG_TAG, "Error decoding JSON response from:"
|
||||
+ host + ". Response was:" + decodedResponse);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.fsck.k9.mail.store;
|
||||
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
|
||||
|
@ -7,6 +8,7 @@ import com.fsck.k9.mail.MessagingException;
|
|||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.ServerSettings.Type;
|
||||
import com.fsck.k9.mail.Store;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.imap.ImapStore;
|
||||
|
@ -38,8 +40,9 @@ public abstract class RemoteStore extends Store {
|
|||
/**
|
||||
* Get an instance of a remote mail store.
|
||||
*/
|
||||
public synchronized static Store getInstance(Context context,
|
||||
StoreConfig storeConfig) throws MessagingException {
|
||||
public synchronized static Store getInstance(Context context, StoreConfig storeConfig,
|
||||
OAuth2TokenProvider oAuth2TokenProvider)
|
||||
throws MessagingException {
|
||||
String uri = storeConfig.getStoreUri();
|
||||
|
||||
if (uri.startsWith("local")) {
|
||||
|
@ -49,14 +52,19 @@ public abstract class RemoteStore extends Store {
|
|||
Store store = sStores.get(uri);
|
||||
if (store == null) {
|
||||
if (uri.startsWith("imap")) {
|
||||
store = new ImapStore(storeConfig,
|
||||
new DefaultTrustedSocketFactory(context),
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
|
||||
store = new ImapStore(
|
||||
storeConfig,
|
||||
new DefaultTrustedSocketFactory(context),
|
||||
(ConnectivityManager) context
|
||||
.getSystemService(Context.CONNECTIVITY_SERVICE),
|
||||
oAuth2TokenProvider
|
||||
);
|
||||
} else if (uri.startsWith("pop3")) {
|
||||
store = new Pop3Store(storeConfig,
|
||||
new DefaultTrustedSocketFactory(context));
|
||||
new DefaultTrustedSocketFactory(context));
|
||||
} else if (uri.startsWith("webdav")) {
|
||||
store = new WebDavStore(storeConfig, new WebDavHttpClient.WebDavHttpClientFactory());
|
||||
store = new WebDavStore(storeConfig,
|
||||
new WebDavHttpClient.WebDavHttpClientFactory());
|
||||
}
|
||||
|
||||
if (store != null) {
|
||||
|
|
|
@ -3,6 +3,8 @@ package com.fsck.k9.mail.store.imap;
|
|||
|
||||
class Capabilities {
|
||||
public static final String IDLE = "IDLE";
|
||||
public static final String SASL_IR = "SASL-IR";
|
||||
public static final String AUTH_XOAUTH2 = "AUTH=XOAUTH2";
|
||||
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";
|
||||
|
|
|
@ -6,6 +6,7 @@ class Commands {
|
|||
public static final String CAPABILITY = "CAPABILITY";
|
||||
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_CRAM_MD5 = "AUTHENTICATE CRAM-MD5";
|
||||
public static final String AUTHENTICATE_PLAIN = "AUTHENTICATE PLAIN";
|
||||
public static final String AUTHENTICATE_EXTERNAL = "AUTHENTICATE EXTERNAL";
|
||||
|
|
|
@ -39,6 +39,8 @@ import com.fsck.k9.mail.MessagingException;
|
|||
import com.fsck.k9.mail.NetworkType;
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import com.fsck.k9.mail.filter.PeekableInputStream;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.jcraft.jzlib.JZlib;
|
||||
import com.jcraft.jzlib.ZOutputStream;
|
||||
|
@ -61,6 +63,7 @@ class ImapConnection {
|
|||
|
||||
|
||||
private final ConnectivityManager connectivityManager;
|
||||
private final OAuth2TokenProvider oauthTokenProvider;
|
||||
private final TrustedSocketFactory socketFactory;
|
||||
private final int socketConnectTimeout;
|
||||
private final int socketReadTimeout;
|
||||
|
@ -74,22 +77,26 @@ class ImapConnection {
|
|||
private ImapSettings settings;
|
||||
private Exception stacktraceForClose;
|
||||
private boolean open = false;
|
||||
private boolean retryXoauth2WithNewToken = true;
|
||||
|
||||
|
||||
public ImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory,
|
||||
ConnectivityManager connectivityManager) {
|
||||
ConnectivityManager connectivityManager, OAuth2TokenProvider oauthTokenProvider) {
|
||||
this.settings = settings;
|
||||
this.socketFactory = socketFactory;
|
||||
this.connectivityManager = connectivityManager;
|
||||
this.oauthTokenProvider = oauthTokenProvider;
|
||||
this.socketConnectTimeout = SOCKET_CONNECT_TIMEOUT;
|
||||
this.socketReadTimeout = SOCKET_READ_TIMEOUT;
|
||||
}
|
||||
|
||||
ImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory, ConnectivityManager connectivityManager,
|
||||
ImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory,
|
||||
ConnectivityManager connectivityManager, OAuth2TokenProvider oauthTokenProvider,
|
||||
int socketConnectTimeout, int socketReadTimeout) {
|
||||
this.settings = settings;
|
||||
this.socketFactory = socketFactory;
|
||||
this.connectivityManager = connectivityManager;
|
||||
this.oauthTokenProvider = oauthTokenProvider;
|
||||
this.socketConnectTimeout = socketConnectTimeout;
|
||||
this.socketReadTimeout = socketReadTimeout;
|
||||
}
|
||||
|
@ -322,6 +329,13 @@ class ImapConnection {
|
|||
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
|
||||
private void authenticate() throws MessagingException, IOException {
|
||||
switch (settings.getAuthType()) {
|
||||
case XOAUTH2:
|
||||
if (hasCapability(Capabilities.AUTH_XOAUTH2) && hasCapability(Capabilities.SASL_IR)) {
|
||||
authXoauth2withSASLIR();
|
||||
} else {
|
||||
throw new MessagingException("Server doesn't support SASL XOAUTH2.");
|
||||
}
|
||||
break;
|
||||
case CRAM_MD5: {
|
||||
if (hasCapability(Capabilities.AUTH_CRAM_MD5)) {
|
||||
authCramMD5();
|
||||
|
@ -356,6 +370,75 @@ class ImapConnection {
|
|||
}
|
||||
}
|
||||
|
||||
private void authXoauth2withSASLIR() throws IOException, MessagingException {
|
||||
retryXoauth2WithNewToken = true;
|
||||
try {
|
||||
attemptXOAuth2();
|
||||
} catch (NegativeImapResponseException e) {
|
||||
//We couldn't login with the token so invalidate it
|
||||
oauthTokenProvider.invalidateToken(settings.getUsername());
|
||||
|
||||
if (!retryXoauth2WithNewToken) {
|
||||
handlePermanentXoauth2Failure(e);
|
||||
} else {
|
||||
handleTemporaryXoauth2Failure(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePermanentXoauth2Failure(NegativeImapResponseException e) throws AuthenticationFailedException {
|
||||
Log.v(LOG_TAG, "Permanent failure during XOAUTH2", e);
|
||||
throw new AuthenticationFailedException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
private void handleTemporaryXoauth2Failure(NegativeImapResponseException e) throws IOException, MessagingException {
|
||||
//We got a response indicating a retry might suceed 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
|
||||
|
||||
Log.v(LOG_TAG, "Temporary failure - retrying with new token", e);
|
||||
try {
|
||||
attemptXOAuth2();
|
||||
} catch (NegativeImapResponseException e2) {
|
||||
//Okay, we failed on a new token.
|
||||
//Invalidate the token anyway but assume it's permanent.
|
||||
Log.v(LOG_TAG, "Authentication exception for new token, permanent error assumed", e);
|
||||
oauthTokenProvider.invalidateToken(settings.getUsername());
|
||||
handlePermanentXoauth2Failure(e2);
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptXOAuth2() throws MessagingException, IOException {
|
||||
String token = oauthTokenProvider.getToken(settings.getUsername(),
|
||||
OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
String tag = sendSaslIrCommand(Commands.AUTHENTICATE_XOAUTH2,
|
||||
Authentication.computeXoauth(settings.getUsername(), token), true);
|
||||
|
||||
extractCapabilities(
|
||||
responseParser.readStatusResponse(tag, Commands.AUTHENTICATE_XOAUTH2, getLogId(),
|
||||
new UntaggedHandler() {
|
||||
@Override
|
||||
public void handleAsyncUntaggedResponse(ImapResponse response) throws IOException {
|
||||
handleXOAuthUntaggedResponse(response);
|
||||
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private void handleXOAuthUntaggedResponse(ImapResponse response) throws IOException {
|
||||
if (response.isString(0)) {
|
||||
retryXoauth2WithNewToken = XOAuth2ChallengeParser.shouldRetry(
|
||||
response.getString(0), settings.getHost());
|
||||
}
|
||||
|
||||
if(response.isContinuationRequested()) {
|
||||
outputStream.write("\r\n".getBytes());
|
||||
outputStream.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void authCramMD5() throws MessagingException, IOException {
|
||||
String command = Commands.AUTHENTICATE_CRAM_MD5;
|
||||
String tag = sendCommand(command, false);
|
||||
|
@ -635,6 +718,31 @@ class ImapConnection {
|
|||
return responseParser.readStatusResponse(tag, commandToLog, getLogId(), untaggedHandler);
|
||||
}
|
||||
|
||||
public String sendSaslIrCommand(String command, String initialClientResponse, boolean sensitive)
|
||||
throws IOException, MessagingException {
|
||||
try {
|
||||
open();
|
||||
|
||||
String tag = Integer.toString(nextCommandTag++);
|
||||
String commandToSend = tag + " " + command + " " + initialClientResponse+ "\r\n";
|
||||
outputStream.write(commandToSend.getBytes());
|
||||
outputStream.flush();
|
||||
|
||||
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) {
|
||||
if (sensitive && !K9MailLib.isDebugSensitive()) {
|
||||
Log.v(LOG_TAG, getLogId() + ">>> [Command Hidden, Enable Sensitive Debug Logging To Show]");
|
||||
} else {
|
||||
Log.v(LOG_TAG, getLogId() + ">>> " + tag + " " + command+ " " + initialClientResponse);
|
||||
}
|
||||
}
|
||||
|
||||
return tag;
|
||||
} catch (IOException | MessagingException e) {
|
||||
close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public String sendCommand(String command, boolean sensitive) throws MessagingException, IOException {
|
||||
try {
|
||||
open();
|
||||
|
|
|
@ -109,7 +109,7 @@ class ImapResponseParser {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (untaggedHandler != null) {
|
||||
if (response.getTag() == null && untaggedHandler != null) {
|
||||
untaggedHandler.handleAsyncUntaggedResponse(response);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import android.accounts.AccountManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -26,6 +27,7 @@ import com.fsck.k9.mail.NetworkType;
|
|||
import com.fsck.k9.mail.PushReceiver;
|
||||
import com.fsck.k9.mail.Pusher;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.RemoteStore;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
|
@ -42,6 +44,7 @@ import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
|||
public class ImapStore extends RemoteStore {
|
||||
private Set<Flag> permanentFlagsIndex = EnumSet.noneOf(Flag.class);
|
||||
private ConnectivityManager connectivityManager;
|
||||
private OAuth2TokenProvider oauthTokenProvider;
|
||||
|
||||
private String host;
|
||||
private int port;
|
||||
|
@ -73,8 +76,10 @@ public class ImapStore extends RemoteStore {
|
|||
return ImapStoreUriCreator.create(server);
|
||||
}
|
||||
|
||||
public ImapStore(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
|
||||
ConnectivityManager connectivityManager) throws MessagingException {
|
||||
public ImapStore(StoreConfig storeConfig,
|
||||
TrustedSocketFactory trustedSocketFactory,
|
||||
ConnectivityManager connectivityManager,
|
||||
OAuth2TokenProvider oauthTokenProvider) throws MessagingException {
|
||||
super(storeConfig, trustedSocketFactory);
|
||||
|
||||
ImapStoreSettings settings;
|
||||
|
@ -89,6 +94,7 @@ public class ImapStore extends RemoteStore {
|
|||
|
||||
connectionSecurity = settings.connectionSecurity;
|
||||
this.connectivityManager = connectivityManager;
|
||||
this.oauthTokenProvider = oauthTokenProvider;
|
||||
|
||||
authType = settings.authenticationType;
|
||||
username = settings.username;
|
||||
|
@ -340,7 +346,11 @@ public class ImapStore extends RemoteStore {
|
|||
}
|
||||
|
||||
ImapConnection createImapConnection() {
|
||||
return new ImapConnection(new StoreImapSettings(), mTrustedSocketFactory, connectivityManager);
|
||||
return new ImapConnection(
|
||||
new StoreImapSettings(),
|
||||
mTrustedSocketFactory,
|
||||
connectivityManager,
|
||||
oauthTokenProvider);
|
||||
}
|
||||
|
||||
FolderNameCodec getFolderNameCodec() {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.fsck.k9.mail.store.imap;
|
||||
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
|
@ -83,14 +85,26 @@ class ImapStoreUriDecoder {
|
|||
String[] userInfoParts = userinfo.split(":");
|
||||
|
||||
if (userinfo.endsWith(":")) {
|
||||
// Password is empty. This can only happen after an account was imported.
|
||||
authenticationType = AuthType.valueOf(userInfoParts[0]);
|
||||
username = decodeUtf8(userInfoParts[1]);
|
||||
// Last field (password/certAlias) is empty.
|
||||
// For imports e.g.: PLAIN:username: or username:
|
||||
// Or XOAUTH2 where it's a valid config - XOAUTH:username:
|
||||
if(userInfoParts.length > 1) {
|
||||
authenticationType = AuthType.valueOf(userInfoParts[0]);
|
||||
username = decodeUtf8(userInfoParts[1]);
|
||||
} else {
|
||||
authenticationType = AuthType.PLAIN;
|
||||
username = decodeUtf8(userInfoParts[0]);
|
||||
}
|
||||
} else if (userInfoParts.length == 2) {
|
||||
// Old/standard style of encoding - PLAIN auth only:
|
||||
// username:password
|
||||
authenticationType = AuthType.PLAIN;
|
||||
username = decodeUtf8(userInfoParts[0]);
|
||||
password = decodeUtf8(userInfoParts[1]);
|
||||
} else if (userInfoParts.length == 3) {
|
||||
// Standard encoding
|
||||
// PLAIN:username:password
|
||||
// EXTERNAL:username:certAlias
|
||||
authenticationType = AuthType.valueOf(userInfoParts[0]);
|
||||
username = decodeUtf8(userInfoParts[1]);
|
||||
|
||||
|
@ -122,5 +136,6 @@ class ImapStoreUriDecoder {
|
|||
|
||||
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, username,
|
||||
password, clientCertificateAlias, autoDetectNamespace, pathPrefix);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,8 @@ import com.fsck.k9.mail.MessagingException;
|
|||
class NegativeImapResponseException extends MessagingException {
|
||||
private static final long serialVersionUID = 3725007182205882394L;
|
||||
|
||||
|
||||
private final String alertText;
|
||||
|
||||
|
||||
public NegativeImapResponseException(String message, String alertText) {
|
||||
super(message, true);
|
||||
this.alertText = alertText;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.fsck.k9.mail.store.imap;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
interface UntaggedHandler {
|
||||
void handleAsyncUntaggedResponse(ImapResponse response);
|
||||
void handleAsyncUntaggedResponse(ImapResponse response) throws IOException;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
package com.fsck.k9.mail.transport;
|
||||
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.mail.*;
|
||||
|
@ -13,6 +14,8 @@ import com.fsck.k9.mail.filter.PeekableInputStream;
|
|||
import com.fsck.k9.mail.filter.SmtpDataStuffing;
|
||||
import com.fsck.k9.mail.internet.CharsetSupport;
|
||||
import com.fsck.k9.mail.CertificateValidationException;
|
||||
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.StoreConfig;
|
||||
|
||||
|
@ -31,7 +34,11 @@ import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
|||
import static com.fsck.k9.mail.CertificateValidationException.Reason.MissingCapability;
|
||||
|
||||
public class SmtpTransport extends Transport {
|
||||
public static final int SMTP_CONTINUE_REQUEST = 334;
|
||||
public static final int SMTP_AUTHENTICATION_FAILURE_ERROR_CODE = 535;
|
||||
|
||||
private TrustedSocketFactory mTrustedSocketFactory;
|
||||
private OAuth2TokenProvider oauthTokenProvider;
|
||||
|
||||
/**
|
||||
* Decodes a SmtpTransport URI.
|
||||
|
@ -103,7 +110,7 @@ public class SmtpTransport extends Transport {
|
|||
username = decodeUtf8(userInfoParts[0]);
|
||||
password = decodeUtf8(userInfoParts[1]);
|
||||
} else if (userInfoParts.length == 3) {
|
||||
// NOTE: In SmptTransport URIs, the authType comes last!
|
||||
// NOTE: In SmtpTransport URIs, the authType comes last!
|
||||
authType = AuthType.valueOf(userInfoParts[2]);
|
||||
username = decodeUtf8(userInfoParts[0]);
|
||||
if (authType == AuthType.EXTERNAL) {
|
||||
|
@ -184,8 +191,10 @@ public class SmtpTransport extends Transport {
|
|||
private OutputStream mOut;
|
||||
private boolean m8bitEncodingAllowed;
|
||||
private int mLargestAcceptableMessage;
|
||||
private boolean retryXoauthWithNewToken;
|
||||
|
||||
public SmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory)
|
||||
public SmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
|
||||
OAuth2TokenProvider oauth2TokenProvider)
|
||||
throws MessagingException {
|
||||
ServerSettings settings;
|
||||
try {
|
||||
|
@ -204,6 +213,7 @@ public class SmtpTransport extends Transport {
|
|||
mPassword = settings.password;
|
||||
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||
mTrustedSocketFactory = trustedSocketFactory;
|
||||
oauthTokenProvider = oauth2TokenProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -301,18 +311,21 @@ public class SmtpTransport extends Transport {
|
|||
boolean authPlainSupported = false;
|
||||
boolean authCramMD5Supported = false;
|
||||
boolean authExternalSupported = false;
|
||||
boolean authXoauth2Supported = false;
|
||||
if (extensions.containsKey("AUTH")) {
|
||||
List<String> saslMech = Arrays.asList(extensions.get("AUTH").split(" "));
|
||||
authLoginSupported = saslMech.contains("LOGIN");
|
||||
authPlainSupported = saslMech.contains("PLAIN");
|
||||
authCramMD5Supported = saslMech.contains("CRAM-MD5");
|
||||
authExternalSupported = saslMech.contains("EXTERNAL");
|
||||
authXoauth2Supported = saslMech.contains("XOAUTH2");
|
||||
}
|
||||
parseOptionalSizeValue(extensions);
|
||||
|
||||
if (mUsername != null
|
||||
&& mUsername.length() > 0
|
||||
&& (mPassword != null && mPassword.length() > 0 || AuthType.EXTERNAL == mAuthType)) {
|
||||
if (!TextUtils.isEmpty(mUsername)
|
||||
&& (!TextUtils.isEmpty(mPassword) ||
|
||||
AuthType.EXTERNAL == mAuthType ||
|
||||
AuthType.XOAUTH2 == mAuthType)) {
|
||||
|
||||
switch (mAuthType) {
|
||||
|
||||
|
@ -340,7 +353,13 @@ public class SmtpTransport extends Transport {
|
|||
throw new MessagingException("Authentication method CRAM-MD5 is unavailable.");
|
||||
}
|
||||
break;
|
||||
|
||||
case XOAUTH2:
|
||||
if (authXoauth2Supported) {
|
||||
saslXoauth2(mUsername);
|
||||
} else {
|
||||
throw new MessagingException("Authentication method XOAUTH2 is unavailable.");
|
||||
}
|
||||
break;
|
||||
case EXTERNAL:
|
||||
if (authExternalSupported) {
|
||||
saslAuthExternal(mUsername);
|
||||
|
@ -499,7 +518,6 @@ public class SmtpTransport extends Transport {
|
|||
|
||||
private void sendMessageTo(List<String> addresses, Message message)
|
||||
throws MessagingException {
|
||||
|
||||
close();
|
||||
open();
|
||||
|
||||
|
@ -638,12 +656,23 @@ public class SmtpTransport extends Transport {
|
|||
|
||||
throw new NegativeSmtpReplyException(replyCode, message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@Deprecated
|
||||
private List<String> executeSimpleCommand(String command) throws IOException, MessagingException {
|
||||
return executeSimpleCommand(command, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: All responses should be checked to confirm that they start with a valid
|
||||
* reply code, and that the reply code is appropriate for the command being executed.
|
||||
* That means it should either be a 2xx code (generally) or a 3xx code in special cases
|
||||
* (e.g., DATA & AUTH LOGIN commands). Reply codes should be made available as part of
|
||||
* the returned object.
|
||||
*
|
||||
* This should be doing using the non-deprecated API below.
|
||||
*/
|
||||
@Deprecated
|
||||
private List<String> executeSimpleCommand(String command, boolean sensitive)
|
||||
throws IOException, MessagingException {
|
||||
List<String> results = new ArrayList<String>();
|
||||
|
@ -651,17 +680,66 @@ public class SmtpTransport extends Transport {
|
|||
writeLine(command, sensitive);
|
||||
}
|
||||
|
||||
/*
|
||||
* Read lines as long as the length is 4 or larger, e.g. "220-banner text here".
|
||||
* Shorter lines are either errors of contain only a reply code. Those cases will
|
||||
* be handled by checkLine() below.
|
||||
*
|
||||
* TODO: All responses should be checked to confirm that they start with a valid
|
||||
* reply code, and that the reply code is appropriate for the command being executed.
|
||||
* That means it should either be a 2xx code (generally) or a 3xx code in special cases
|
||||
* (e.g., DATA & AUTH LOGIN commands). Reply codes should be made available as part of
|
||||
* the returned object.
|
||||
*/
|
||||
String line = readCommandResponseLine(results);
|
||||
|
||||
// Check if the reply code indicates an error.
|
||||
checkLine(line);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static class CommandResponse {
|
||||
|
||||
private final int replyCode;
|
||||
private final String message;
|
||||
|
||||
public CommandResponse(int replyCode, String message) {
|
||||
this.replyCode = replyCode;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
private CommandResponse executeSimpleCommandWithResponse(String command, boolean sensitive) throws IOException, MessagingException {
|
||||
List<String> results = new ArrayList<String>();
|
||||
if (command != null) {
|
||||
writeLine(command, sensitive);
|
||||
}
|
||||
|
||||
String line = readCommandResponseLine(results);
|
||||
|
||||
int length = line.length();
|
||||
if (length < 1) {
|
||||
throw new MessagingException("SMTP response is 0 length");
|
||||
}
|
||||
|
||||
int replyCode = -1;
|
||||
String message = line;
|
||||
if (length >= 3) {
|
||||
try {
|
||||
replyCode = Integer.parseInt(line.substring(0, 3));
|
||||
} catch (NumberFormatException e) { /* ignore */ }
|
||||
|
||||
if (length > 4) {
|
||||
message = line.substring(4);
|
||||
} else {
|
||||
message = "";
|
||||
}
|
||||
}
|
||||
|
||||
char c = line.charAt(0);
|
||||
if ((c == '4') || (c == '5')) {
|
||||
throw new NegativeSmtpReplyException(replyCode, message);
|
||||
}
|
||||
|
||||
return new CommandResponse(replyCode, message);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Read lines as long as the length is 4 or larger, e.g. "220-banner text here".
|
||||
* Shorter lines are either errors of contain only a reply code.
|
||||
*/
|
||||
private String readCommandResponseLine(List<String> results) throws IOException {
|
||||
String line = readLine();
|
||||
while (line.length() >= 4) {
|
||||
if (line.length() > 4) {
|
||||
|
@ -675,11 +753,7 @@ public class SmtpTransport extends Transport {
|
|||
}
|
||||
line = readLine();
|
||||
}
|
||||
|
||||
// Check if the reply code indicates an error.
|
||||
checkLine(line);
|
||||
|
||||
return results;
|
||||
return line;
|
||||
}
|
||||
|
||||
|
||||
|
@ -707,7 +781,7 @@ public class SmtpTransport extends Transport {
|
|||
executeSimpleCommand(Base64.encode(username), true);
|
||||
executeSimpleCommand(Base64.encode(password), true);
|
||||
} catch (NegativeSmtpReplyException exception) {
|
||||
if (exception.getReplyCode() == 535) {
|
||||
if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
// Authentication credentials invalid
|
||||
throw new AuthenticationFailedException("AUTH LOGIN failed ("
|
||||
+ exception.getMessage() + ")");
|
||||
|
@ -723,7 +797,7 @@ public class SmtpTransport extends Transport {
|
|||
try {
|
||||
executeSimpleCommand("AUTH PLAIN " + data, true);
|
||||
} catch (NegativeSmtpReplyException exception) {
|
||||
if (exception.getReplyCode() == 535) {
|
||||
if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
// Authentication credentials invalid
|
||||
throw new AuthenticationFailedException("AUTH PLAIN failed ("
|
||||
+ exception.getMessage() + ")");
|
||||
|
@ -747,7 +821,7 @@ public class SmtpTransport extends Transport {
|
|||
try {
|
||||
executeSimpleCommand(b64CRAMString, true);
|
||||
} catch (NegativeSmtpReplyException exception) {
|
||||
if (exception.getReplyCode() == 535) {
|
||||
if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
// Authentication credentials invalid
|
||||
throw new AuthenticationFailedException(exception.getMessage(), exception);
|
||||
} else {
|
||||
|
@ -756,6 +830,70 @@ public class SmtpTransport extends Transport {
|
|||
}
|
||||
}
|
||||
|
||||
private void saslXoauth2(String username) throws MessagingException, IOException {
|
||||
retryXoauthWithNewToken = true;
|
||||
try {
|
||||
attemptXoauth2(username);
|
||||
} catch (NegativeSmtpReplyException negativeResponse) {
|
||||
if (negativeResponse.getReplyCode() != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
throw negativeResponse;
|
||||
}
|
||||
|
||||
oauthTokenProvider.invalidateToken(username);
|
||||
|
||||
if (!retryXoauthWithNewToken) {
|
||||
handlePermanentFailure(negativeResponse);
|
||||
} else {
|
||||
handleTemporaryFailure(username, negativeResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePermanentFailure(NegativeSmtpReplyException negativeResponse) throws AuthenticationFailedException {
|
||||
throw new AuthenticationFailedException(negativeResponse.getMessage(), negativeResponse);
|
||||
}
|
||||
|
||||
private void handleTemporaryFailure(String username, NegativeSmtpReplyException negativeResponseFromOldToken)
|
||||
throws IOException, MessagingException {
|
||||
// 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
|
||||
|
||||
Log.v(LOG_TAG, "Authentication exception, re-trying with new token", negativeResponseFromOldToken);
|
||||
try {
|
||||
attemptXoauth2(username);
|
||||
} catch (NegativeSmtpReplyException negativeResponseFromNewToken) {
|
||||
if (negativeResponseFromNewToken.getReplyCode() != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
throw negativeResponseFromNewToken;
|
||||
}
|
||||
|
||||
//Okay, we failed on a new token.
|
||||
//Invalidate the token anyway but assume it's permanent.
|
||||
Log.v(LOG_TAG, "Authentication exception for new token, permanent error assumed",
|
||||
negativeResponseFromNewToken);
|
||||
|
||||
oauthTokenProvider.invalidateToken(username);
|
||||
|
||||
handlePermanentFailure(negativeResponseFromNewToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptXoauth2(String username) throws MessagingException, IOException {
|
||||
CommandResponse response = executeSimpleCommandWithResponse("AUTH XOAUTH2 " +
|
||||
Authentication.computeXoauth(username,
|
||||
oauthTokenProvider.getToken(username, OAuth2TokenProvider.OAUTH2_TIMEOUT)),
|
||||
true);
|
||||
if(response.replyCode == SMTP_CONTINUE_REQUEST) {
|
||||
retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(
|
||||
response.message, mHost);
|
||||
|
||||
//Per Google spec, respond to challenge with empty response
|
||||
executeSimpleCommandWithResponse("", false);
|
||||
}
|
||||
}
|
||||
|
||||
private void saslAuthExternal(String username) throws MessagingException, IOException {
|
||||
executeSimpleCommand(
|
||||
String.format("AUTH EXTERNAL %s",
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.fsck.k9.mail;
|
||||
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
|
||||
|
||||
public class XOAuth2ChallengeParserTest {
|
||||
public static final String STATUS_400_RESPONSE = Base64.encode(
|
||||
"{\"status\":\"400\",\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"}");
|
||||
public static final String STATUS_401_RESPONSE = Base64.encode(
|
||||
"{\"status\":\"401\",\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"}");
|
||||
public static final String MISSING_STATUS_RESPONSE = Base64.encode(
|
||||
"{\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"}");
|
||||
public static final String INVALID_RESPONSE = Base64.encode(
|
||||
"{\"status\":\"401\",\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"");
|
||||
}
|
|
@ -13,6 +13,9 @@ import com.fsck.k9.mail.CertificateValidationException.Reason;
|
|||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.K9MailLib;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.XOAuth2ChallengeParserTest;
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.imap.mockserver.MockImapServer;
|
||||
import com.fsck.k9.mail.helpers.TestTrustedSocketFactory;
|
||||
|
@ -21,6 +24,7 @@ import okio.ByteString;
|
|||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InOrder;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowLog;
|
||||
|
@ -31,7 +35,9 @@ import static org.junit.Assert.assertFalse;
|
|||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
|
@ -45,14 +51,17 @@ public class ImapConnectionTest {
|
|||
private static final int SOCKET_READ_TIMEOUT = 10000;
|
||||
|
||||
|
||||
|
||||
private TrustedSocketFactory socketFactory;
|
||||
private ConnectivityManager connectivityManager;
|
||||
private OAuth2TokenProvider oAuth2TokenProvider;
|
||||
private SimpleImapSettings settings;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
connectivityManager = mock(ConnectivityManager.class);
|
||||
oAuth2TokenProvider = mock(OAuth2TokenProvider.class);
|
||||
socketFactory = new TestTrustedSocketFactory();
|
||||
|
||||
settings = new SimpleImapSettings();
|
||||
|
@ -182,7 +191,7 @@ public class ImapConnectionTest {
|
|||
fail("Expected exception");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
//FIXME: improve exception message
|
||||
assertThat(e.getMessage(), containsString("response: #3# [NO, Go away]"));
|
||||
assertThat(e.getMessage(), containsString("Go away"));
|
||||
}
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
|
@ -239,7 +248,7 @@ public class ImapConnectionTest {
|
|||
fail("Expected exception");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
//FIXME: improve exception message
|
||||
assertThat(e.getMessage(), containsString("response: #2# [NO, Who are you?]"));
|
||||
assertThat(e.getMessage(), containsString("Who are you?"));
|
||||
}
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
|
@ -264,6 +273,192 @@ public class ImapConnectionTest {
|
|||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIr() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token\001\001"
|
||||
).base64());
|
||||
server.output("2 OK Success");
|
||||
simplePostAuthenticationDialog(server);
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
imapConnection.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrThrowsExeptionOn401Response() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token").thenReturn("token2");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token\001\001"
|
||||
).base64());
|
||||
server.output("+ "+ XOAuth2ChallengeParserTest.STATUS_401_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("2 NO SASL authentication failed");
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
try {
|
||||
imapConnection.open();
|
||||
fail();
|
||||
} catch (AuthenticationFailedException e) {
|
||||
assertEquals(
|
||||
"Command: AUTHENTICATE XOAUTH2; response: #2# [NO, SASL authentication failed]",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrInvalidatesAndRetriesNewTokenOn400Response() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token").thenReturn("token2");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token\001\001"
|
||||
).base64());
|
||||
server.output("+ "+XOAuth2ChallengeParserTest.STATUS_400_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("2 NO SASL authentication failed");
|
||||
server.expect("3 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token2\001\001"
|
||||
).base64());
|
||||
server.output("3 OK Success");
|
||||
simplePostAuthenticationDialog(server, "4");
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
imapConnection.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken("user");
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrInvalidatesAndRetriesNewTokenOnInvalidJsonResponse() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token").thenReturn("token2");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token\001\001"
|
||||
).base64());
|
||||
server.output("+ "+XOAuth2ChallengeParserTest.INVALID_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("2 NO SASL authentication failed");
|
||||
server.expect("3 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token2\001\001"
|
||||
).base64());
|
||||
server.output("3 OK Success");
|
||||
simplePostAuthenticationDialog(server, "4");
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
imapConnection.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken("user");
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrInvalidatesAndRetriesNewTokenOnMissingStatusJsonResponse() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token").thenReturn("token2");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token\001\001"
|
||||
).base64());
|
||||
server.output("+ "+XOAuth2ChallengeParserTest.MISSING_STATUS_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("2 NO SASL authentication failed");
|
||||
server.expect("3 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token2\001\001"
|
||||
).base64());
|
||||
server.output("3 OK Success");
|
||||
simplePostAuthenticationDialog(server, "4");
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
imapConnection.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken("user");
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrWithOldTokenThrowsExceptionIfRetryFails() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token").thenReturn("token2");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token\001\001"
|
||||
).base64());
|
||||
server.output("+ r3j3krj3irj3oir3ojo");
|
||||
server.expect("");
|
||||
server.output("2 NO SASL authentication failed");
|
||||
server.expect("3 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token2\001\001"
|
||||
).base64());
|
||||
server.output("+ 433ba3a3a");
|
||||
server.expect("");
|
||||
server.output("3 NO SASL authentication failed");
|
||||
simplePostAuthenticationDialog(server);
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
try {
|
||||
imapConnection.open();
|
||||
fail();
|
||||
} catch (AuthenticationFailedException e) {
|
||||
assertEquals(
|
||||
"Command: AUTHENTICATE XOAUTH2; response: #3# [NO, SASL authentication failed]",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrParsesCapabilities() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 "+ByteString.encodeUtf8(
|
||||
"user=user\001auth=Bearer token\001\001"
|
||||
).base64());
|
||||
server.output("2 OK [CAPABILITY IMAP4REV1 IDLE XM-GM-EXT-1]");
|
||||
simplePostAuthenticationDialog(server);
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
imapConnection.open();
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
assertTrue(imapConnection.hasCapability("XM-GM-EXT-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authExternal() throws Exception {
|
||||
settings.setAuthType(AuthType.EXTERNAL);
|
||||
|
@ -286,7 +481,7 @@ public class ImapConnectionTest {
|
|||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "AUTH=EXTERNAL");
|
||||
server.expect("2 AUTHENTICATE EXTERNAL " + ByteString.encodeUtf8(USERNAME).base64());
|
||||
server.output("2 NO");
|
||||
server.output("2 NO Bad certificate");
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
try {
|
||||
|
@ -294,7 +489,7 @@ public class ImapConnectionTest {
|
|||
fail("Expected exception");
|
||||
} catch (CertificateValidationException e) {
|
||||
//FIXME: improve exception message
|
||||
assertThat(e.getMessage(), containsString("response: #2# [NO]"));
|
||||
assertThat(e.getMessage(), containsString("Bad certificate"));
|
||||
}
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
|
@ -338,13 +533,13 @@ public class ImapConnectionTest {
|
|||
public void open_withConnectionError_shouldThrow() throws Exception {
|
||||
settings.setHost("127.1.2.3");
|
||||
settings.setPort(143);
|
||||
ImapConnection imapConnection = createImapConnection(settings, socketFactory, connectivityManager);
|
||||
ImapConnection imapConnection = createImapConnection(
|
||||
settings, socketFactory, connectivityManager, oAuth2TokenProvider);
|
||||
|
||||
try {
|
||||
imapConnection.open();
|
||||
fail("Expected exception");
|
||||
} catch (MessagingException e) {
|
||||
//FIXME: Throw ConnectException
|
||||
assertEquals("Cannot connect to host", e.getMessage());
|
||||
assertTrue(e.getCause() instanceof IOException);
|
||||
}
|
||||
|
@ -354,7 +549,8 @@ public class ImapConnectionTest {
|
|||
public void open_withInvalidHostname_shouldThrow() throws Exception {
|
||||
settings.setHost("host name");
|
||||
settings.setPort(143);
|
||||
ImapConnection imapConnection = createImapConnection(settings, socketFactory, connectivityManager);
|
||||
ImapConnection imapConnection = createImapConnection(
|
||||
settings, socketFactory, connectivityManager, oAuth2TokenProvider);
|
||||
|
||||
try {
|
||||
imapConnection.open();
|
||||
|
@ -423,7 +619,7 @@ public class ImapConnectionTest {
|
|||
imapConnection.open();
|
||||
fail("Expected exception");
|
||||
} catch (NegativeImapResponseException e) {
|
||||
assertThat(e.getMessage(), containsString("response: #2# [NO]"));
|
||||
assertEquals(e.getMessage(), "Command: STARTTLS; response: #2# [NO]");
|
||||
}
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
|
@ -522,7 +718,8 @@ public class ImapConnectionTest {
|
|||
|
||||
@Test
|
||||
public void isConnected_withoutPreviousOpen_shouldReturnFalse() throws Exception {
|
||||
ImapConnection imapConnection = createImapConnection(settings, socketFactory, connectivityManager);
|
||||
ImapConnection imapConnection = createImapConnection(
|
||||
settings, socketFactory, connectivityManager, oAuth2TokenProvider);
|
||||
|
||||
boolean result = imapConnection.isConnected();
|
||||
|
||||
|
@ -558,7 +755,8 @@ public class ImapConnectionTest {
|
|||
|
||||
@Test
|
||||
public void close_withoutOpen_shouldNotThrow() throws Exception {
|
||||
ImapConnection imapConnection = createImapConnection(settings, socketFactory, connectivityManager);
|
||||
ImapConnection imapConnection = createImapConnection(
|
||||
settings, socketFactory, connectivityManager, oAuth2TokenProvider);
|
||||
|
||||
imapConnection.close();
|
||||
}
|
||||
|
@ -620,16 +818,16 @@ public class ImapConnectionTest {
|
|||
}
|
||||
|
||||
private ImapConnection createImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory,
|
||||
ConnectivityManager connectivityManager) {
|
||||
return new ImapConnection(settings, socketFactory, connectivityManager, SOCKET_CONNECT_TIMEOUT,
|
||||
SOCKET_READ_TIMEOUT);
|
||||
ConnectivityManager connectivityManager, OAuth2TokenProvider oAuth2TokenProvider) {
|
||||
return new ImapConnection(settings, socketFactory, connectivityManager, oAuth2TokenProvider,
|
||||
SOCKET_CONNECT_TIMEOUT, SOCKET_READ_TIMEOUT);
|
||||
}
|
||||
|
||||
private ImapConnection startServerAndCreateImapConnection(MockImapServer server) throws IOException {
|
||||
server.start();
|
||||
settings.setHost(server.getHost());
|
||||
settings.setPort(server.getPort());
|
||||
return createImapConnection(settings, socketFactory, connectivityManager);
|
||||
return createImapConnection(settings, socketFactory, connectivityManager, oAuth2TokenProvider);
|
||||
}
|
||||
|
||||
private ImapConnection simpleOpen(MockImapServer server) throws Exception {
|
||||
|
|
|
@ -84,7 +84,8 @@ public class ImapResponseParserTest {
|
|||
|
||||
@Test
|
||||
public void testReadStatusResponseWithOKResponse() throws Exception {
|
||||
ImapResponseParser parser = createParser("* COMMAND BAR\tBAZ\r\nTAG OK COMMAND completed\r\n");
|
||||
ImapResponseParser parser = createParser("* COMMAND BAR\tBAZ\r\n" +
|
||||
"TAG OK COMMAND completed\r\n");
|
||||
|
||||
List<ImapResponse> responses = parser.readStatusResponse("TAG", null, null, null);
|
||||
|
||||
|
@ -93,6 +94,19 @@ public class ImapResponseParserTest {
|
|||
assertEquals(asList("OK", "COMMAND completed"), responses.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadStatusResponseUntaggedHandlerGetsUntaggedOnly() throws Exception {
|
||||
ImapResponseParser parser = createParser(
|
||||
"* UNTAGGED\r\n" +
|
||||
"A2 OK COMMAND completed\r\n");
|
||||
TestUntaggedHandler untaggedHandler = new TestUntaggedHandler();
|
||||
|
||||
parser.readStatusResponse("A2", null, null, untaggedHandler);
|
||||
|
||||
assertEquals(1, untaggedHandler.responses.size());
|
||||
assertEquals(asList("UNTAGGED"), untaggedHandler.responses.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadStatusResponseSkippingWrongTag() throws Exception {
|
||||
ImapResponseParser parser = createParser("* UNTAGGED\r\n" +
|
||||
|
@ -113,6 +127,23 @@ public class ImapResponseParserTest {
|
|||
assertEquals(responses.get(1), untaggedHandler.responses.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadStatusResponseUntaggedHandlerStillCalledOnNegativeReply() throws Exception {
|
||||
ImapResponseParser parser = createParser(
|
||||
"+ text\r\n" +
|
||||
"A2 NO Bad response\r\n");
|
||||
TestUntaggedHandler untaggedHandler = new TestUntaggedHandler();
|
||||
|
||||
try {
|
||||
List<ImapResponse> responses = parser.readStatusResponse("A2", null, null, untaggedHandler);
|
||||
} catch (NegativeImapResponseException e) {
|
||||
}
|
||||
|
||||
assertEquals(1, untaggedHandler.responses.size());
|
||||
assertEquals(asList("text"), untaggedHandler.responses.get(0));
|
||||
|
||||
}
|
||||
|
||||
@Test(expected = NegativeImapResponseException.class)
|
||||
public void testReadStatusResponseWithErrorResponse() throws Exception {
|
||||
ImapResponseParser parser = createParser("* COMMAND BAR BAZ\r\nTAG ERROR COMMAND errored\r\n");
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.net.ConnectivityManager;
|
|||
|
||||
import com.fsck.k9.mail.Folder;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
import org.junit.Before;
|
||||
|
@ -44,8 +45,9 @@ public class ImapStoreTest {
|
|||
storeConfig = createStoreConfig();
|
||||
TrustedSocketFactory trustedSocketFactory = mock(TrustedSocketFactory.class);
|
||||
ConnectivityManager connectivityManager = mock(ConnectivityManager.class);
|
||||
OAuth2TokenProvider oauth2TokenProvider = mock(OAuth2TokenProvider.class);
|
||||
|
||||
imapStore = new TestImapStore(storeConfig, trustedSocketFactory, connectivityManager);
|
||||
imapStore = new TestImapStore(storeConfig, trustedSocketFactory, connectivityManager, oauth2TokenProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -313,8 +315,8 @@ public class ImapStoreTest {
|
|||
private Deque<ImapConnection> imapConnections = new ArrayDeque<>();
|
||||
|
||||
public TestImapStore(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
|
||||
ConnectivityManager connectivityManager) throws MessagingException {
|
||||
super(storeConfig, trustedSocketFactory, connectivityManager);
|
||||
ConnectivityManager connectivityManager, OAuth2TokenProvider oauth2TokenProvider) throws MessagingException {
|
||||
super(storeConfig, trustedSocketFactory, connectivityManager, oauth2TokenProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -16,6 +16,88 @@ import static org.junit.Assert.assertNull;
|
|||
|
||||
|
||||
public class ImapStoreUriTest {
|
||||
@Test
|
||||
public void testDecodeStoreUriImapNoAuth() {
|
||||
String uri = "imap://user:pass@server/";
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals("pass", settings.password);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapNoPassword() {
|
||||
String uri = "imap://user:@server/";
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals(null, settings.password);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapPlainNoPassword() {
|
||||
String uri = "imap://PLAIN:user:@server/";
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals(null, settings.password);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapExternalAuth() {
|
||||
String uri = "imap://EXTERNAL:user:clientCertAlias@server/";
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.EXTERNAL, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals(null, settings.password);
|
||||
assertEquals("clientCertAlias", settings.clientCertificateAlias);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapXOAuth2() {
|
||||
String uri = "imap://XOAUTH2:user:@server/";
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.XOAUTH2, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals(null, settings.password);
|
||||
assertEquals(null, settings.clientCertificateAlias);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapSSL() {
|
||||
String uri = "imap+tls+://PLAIN:user:pass@server/";
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(ConnectionSecurity.STARTTLS_REQUIRED, settings.connectionSecurity);
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals("pass", settings.password);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapTLS() {
|
||||
String uri = "imap+ssl+://PLAIN:user:pass@server/";
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(ConnectionSecurity.SSL_TLS_REQUIRED, settings.connectionSecurity);
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals("pass", settings.password);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapAllExtras() {
|
||||
String uri = "imap://PLAIN:user:pass@server:143/0%7CcustomPathPrefix";
|
||||
|
|
|
@ -5,28 +5,38 @@ import java.io.IOException;
|
|||
import java.net.InetAddress;
|
||||
|
||||
import com.fsck.k9.mail.AuthType;
|
||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
||||
import com.fsck.k9.mail.CertificateValidationException;
|
||||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.ServerSettings.Type;
|
||||
import com.fsck.k9.mail.XOAuth2ChallengeParserTest;
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import com.fsck.k9.mail.helpers.TestMessageBuilder;
|
||||
import com.fsck.k9.mail.helpers.TestTrustedSocketFactory;
|
||||
import com.fsck.k9.mail.internet.MimeMessage;
|
||||
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.StoreConfig;
|
||||
import com.fsck.k9.mail.transport.mockServer.MockSmtpServer;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InOrder;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
import static junit.framework.Assert.fail;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -41,25 +51,29 @@ public class SmtpTransportTest {
|
|||
|
||||
|
||||
private TrustedSocketFactory socketFactory;
|
||||
private OAuth2TokenProvider oAuth2TokenProvider;
|
||||
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
public void before() throws AuthenticationFailedException {
|
||||
socketFactory = new TestTrustedSocketFactory();
|
||||
oAuth2TokenProvider = mock(OAuth2TokenProvider.class);
|
||||
when(oAuth2TokenProvider.getToken(eq(USERNAME), anyInt()))
|
||||
.thenReturn("oldToken").thenReturn("newToken");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void SmtpTransport_withValidTransportUri() throws Exception {
|
||||
StoreConfig storeConfig = createStoreConfigWithTransportUri("smtp://user:password:CRAM_MD5@server:123456");
|
||||
|
||||
new SmtpTransport(storeConfig, socketFactory);
|
||||
new SmtpTransport(storeConfig, socketFactory, oAuth2TokenProvider);
|
||||
}
|
||||
|
||||
@Test(expected = MessagingException.class)
|
||||
public void SmtpTransport_withInvalidTransportUri_shouldThrow() throws Exception {
|
||||
StoreConfig storeConfig = createStoreConfigWithTransportUri("smpt://");
|
||||
|
||||
new SmtpTransport(storeConfig, socketFactory);
|
||||
new SmtpTransport(storeConfig, socketFactory, oAuth2TokenProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -174,6 +188,205 @@ public class SmtpTransportTest {
|
|||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
transport.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldThrowOn401Response() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("334 "+ XOAuth2ChallengeParserTest.STATUS_401_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
assertEquals(
|
||||
"Negative SMTP reply: 535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68",
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME);
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldInvalidateAndRetryOn400Response() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("334 "+ XOAuth2ChallengeParserTest.STATUS_400_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
transport.open();
|
||||
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldInvalidateAndRetryOnInvalidJsonResponse() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("334 "+ XOAuth2ChallengeParserTest.INVALID_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
transport.open();
|
||||
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldInvalidateAndRetryOnMissingStatusJsonResponse() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("334 "+ XOAuth2ChallengeParserTest.MISSING_STATUS_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
transport.open();
|
||||
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldThrowOnMultipleFailure() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("334 " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=");
|
||||
server.output("334 " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
assertEquals(
|
||||
"Negative SMTP reply: 535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68",
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldThrowOnFailure_fetchingToken() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
when(oAuth2TokenProvider.getToken(anyString(), anyInt())).thenThrow(new AuthenticationFailedException("Failed to fetch token"));
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
assertEquals("Failed to fetch token", e.getMessage());
|
||||
}
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void open_withoutXoauth2Extension_shouldThrow() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH PLAIN LOGIN");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (MessagingException e) {
|
||||
assertEquals("Authentication method XOAUTH2 is unavailable.", e.getMessage());
|
||||
}
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withAuthExternalExtension() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
|
@ -413,7 +626,7 @@ public class SmtpTransportTest {
|
|||
String uri = SmtpTransport.createUri(serverSettings);
|
||||
StoreConfig storeConfig = createStoreConfigWithTransportUri(uri);
|
||||
|
||||
return new TestSmtpTransport(storeConfig, socketFactory);
|
||||
return new TestSmtpTransport(storeConfig, socketFactory, oAuth2TokenProvider);
|
||||
}
|
||||
|
||||
private StoreConfig createStoreConfigWithTransportUri(String value) {
|
||||
|
@ -452,9 +665,9 @@ public class SmtpTransportTest {
|
|||
|
||||
|
||||
static class TestSmtpTransport extends SmtpTransport {
|
||||
TestSmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory)
|
||||
TestSmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oAuth2TokenProvider)
|
||||
throws MessagingException {
|
||||
super(storeConfig, trustedSocketFactory);
|
||||
super(storeConfig, trustedSocketFactory, oAuth2TokenProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
Loading…
Reference in a new issue