From 41b292ee9d40fa281019ab6fbc89a0c01fd6c512 Mon Sep 17 00:00:00 2001 From: wongk Date: Fri, 30 Dec 2011 14:51:48 -0500 Subject: [PATCH] Update EasStore for the new URI scheme. Implement the DeviceID for EAS. --- src/com/fsck/k9/K9.java | 85 +++--- src/com/fsck/k9/mail/Store.java | 6 + src/com/fsck/k9/mail/store/EasStore.java | 342 +++++++++++++++-------- 3 files changed, 262 insertions(+), 171 deletions(-) diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index 4ced5af40..1bbe0fcf4 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -5,6 +5,7 @@ import java.io.File; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.SynchronousQueue; @@ -14,6 +15,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -21,6 +23,7 @@ import android.net.Uri; import android.os.Environment; import android.os.Handler; import android.os.Looper; +import android.text.TextUtils; import android.text.format.Time; import android.util.Log; @@ -54,7 +57,7 @@ public class K9 extends Application { void initializeComponent(K9 application); } - public static Application app = null; + public static K9 app = null; public static File tempDirectory; public static final String LOG_TAG = "k9"; @@ -76,6 +79,7 @@ public class K9 extends Application { private static final FontSizes fontSizes = new FontSizes(); private static BACKGROUND_OPS backgroundOps = BACKGROUND_OPS.WHEN_CHECKED; + /** * Some log messages can be sent to a file, so that the logs * can be read using unprivileged access (eg. Terminal Emulator) @@ -91,7 +95,6 @@ public class K9 extends Application { **/ public static boolean DEVELOPER_MODE = true; - /** * If this is enabled there will be additional logging information sent to * Log.d, including protocol dumps. @@ -103,33 +106,26 @@ public class K9 extends Application { * Should K-9 log the conversation it has over the wire with * SMTP servers? */ - public static boolean DEBUG_PROTOCOL_SMTP = true; /** * Should K-9 log the conversation it has over the wire with * IMAP servers? */ - public static boolean DEBUG_PROTOCOL_IMAP = true; - /** * Should K-9 log the conversation it has over the wire with * POP3 servers? */ - public static boolean DEBUG_PROTOCOL_POP3 = true; /** * Should K-9 log the conversation it has over the wire with * WebDAV servers? */ - public static boolean DEBUG_PROTOCOL_WEBDAV = true; - - /** * If this is enabled than logging that normally hides sensitive information * like passwords will show that information. @@ -144,18 +140,15 @@ public class K9 extends Application { public static String ERROR_FOLDER_NAME = "K9mail-errors"; private static boolean mAnimations = true; - private static boolean mConfirmDelete = false; private static boolean mConfirmDeleteStarred = false; private static boolean mConfirmSpam = false; private static boolean mConfirmMarkAllAsRead = true; private static boolean mKeyguardPrivacy = false; - private static boolean mMessageListStars = true; private static boolean mMessageListCheckboxes = false; private static boolean mMessageListTouchable = false; private static int mMessageListPreviewLines = 2; - private static boolean mShowCorrespondentNames = true; private static boolean mShowContactName = false; private static boolean mChangeContactNameColor = false; @@ -163,7 +156,6 @@ public class K9 extends Application { private static boolean mMessageViewFixedWidthFont = false; private static boolean mMessageViewReturnToList = false; private static boolean mMessageViewShowNext = false; - private static boolean mGesturesEnabled = true; private static boolean mUseVolumeKeysForNavigation = false; private static boolean mUseVolumeKeysForListNavigation = false; @@ -179,11 +171,9 @@ public class K9 extends Application { private static String mQuietTimeEnds = null; private static boolean compactLayouts = false; private static String mAttachmentDefaultPath = ""; - - private static boolean useGalleryBugWorkaround = false; private static boolean galleryBuggy; - + private static String mDeviceId = null; /** * The MIME type(s) of attachments we're willing to view. @@ -238,10 +228,10 @@ public class K9 extends Application { public static final int MAX_ATTACHMENT_DOWNLOAD_SIZE = (128 * 1024 * 1024); - /* How many times should K-9 try to deliver a message before giving up + /** + * How many times should K-9 try to deliver a message before giving up * until the app is killed and restarted */ - public static int MAX_SEND_ATTEMPTS = 5; /** @@ -262,25 +252,18 @@ public class K9 extends Application { */ public static final int NOTIFICATION_LED_ON_TIME = 500; public static final int NOTIFICATION_LED_OFF_TIME = 2000; - public static final boolean NOTIFICATION_LED_WHILE_SYNCING = false; public static final int NOTIFICATION_LED_FAST_ON_TIME = 100; public static final int NOTIFICATION_LED_FAST_OFF_TIME = 100; - - public static final int NOTIFICATION_LED_BLINK_SLOW = 0; public static final int NOTIFICATION_LED_BLINK_FAST = 1; - - - public static final int NOTIFICATION_LED_SENDING_FAILURE_COLOR = 0xffff0000; // Must not conflict with an account number - public static final int FETCHING_EMAIL_NOTIFICATION = -5000; - public static final int SEND_FAILED_NOTIFICATION = -1500; + public static final int FETCHING_EMAIL_NOTIFICATION = -5000; + public static final int SEND_FAILED_NOTIFICATION = -1500; public static final int CONNECTIVITY_ID = -3; - public static class Intents { public static class EmailReceived { @@ -317,7 +300,6 @@ public class K9 extends Application { int acctLength = Preferences.getPreferences(context).getAvailableAccounts().size(); setServicesEnabled(context, acctLength > 0, null); - } private static void setServicesEnabled(Context context, boolean enabled, Integer wakeLockId) { @@ -356,7 +338,6 @@ public class K9 extends Application { */ MailService.actionReset(context, wakeLockId); } - } /** @@ -414,7 +395,6 @@ public class K9 extends Application { editor.putBoolean("quietTimeEnabled", mQuietTimeEnabled); editor.putString("quietTimeStarts", mQuietTimeStarts); editor.putString("quietTimeEnds", mQuietTimeEnds); - editor.putBoolean("startIntegratedInbox", mStartIntegratedInbox); editor.putBoolean("measureAccounts", mMeasureAccounts); editor.putBoolean("countSearchMessages", mCountSearchMessages); @@ -423,7 +403,6 @@ public class K9 extends Application { editor.putBoolean("messageListCheckboxes", mMessageListCheckboxes); editor.putBoolean("messageListTouchable", mMessageListTouchable); editor.putInt("messageListPreviewLines", mMessageListPreviewLines); - editor.putBoolean("showCorrespondentNames", mShowCorrespondentNames); editor.putBoolean("showContactName", mShowContactName); editor.putBoolean("changeRegisteredNameColor", mChangeContactNameColor); @@ -431,20 +410,17 @@ public class K9 extends Application { editor.putBoolean("messageViewFixedWidthFont", mMessageViewFixedWidthFont); editor.putBoolean("messageViewReturnToList", mMessageViewReturnToList); editor.putBoolean("messageViewShowNext", mMessageViewShowNext); - editor.putString("language", language); editor.putInt("theme", theme); editor.putBoolean("useGalleryBugWorkaround", useGalleryBugWorkaround); - editor.putBoolean("confirmDelete", mConfirmDelete); editor.putBoolean("confirmDeleteStarred", mConfirmDeleteStarred); editor.putBoolean("confirmSpam", mConfirmSpam); editor.putBoolean("confirmMarkAllAsRead", mConfirmMarkAllAsRead); - editor.putBoolean("keyguardPrivacy", mKeyguardPrivacy); - editor.putBoolean("compactLayouts", compactLayouts); editor.putString("attachmentdefaultpath", mAttachmentDefaultPath); + editor.putString("deviceId", mDeviceId); fontSizes.save(editor); } @@ -454,7 +430,6 @@ public class K9 extends Application { super.onCreate(); app = this; - galleryBuggy = checkForBuggyGallery(); Preferences prefs = Preferences.getPreferences(this); @@ -469,7 +444,6 @@ public class K9 extends Application { /* * Enable background sync of messages */ - setServicesEnabled(this); registerReceivers(); @@ -547,14 +521,11 @@ public class K9 extends Application { mMessageListCheckboxes = sprefs.getBoolean("messageListCheckboxes", false); mMessageListTouchable = sprefs.getBoolean("messageListTouchable", false); mMessageListPreviewLines = sprefs.getInt("messageListPreviewLines", 2); - mMobileOptimizedLayout = sprefs.getBoolean("mobileOptimizedLayout", false); mZoomControlsEnabled = sprefs.getBoolean("zoomControlsEnabled", false); - mQuietTimeEnabled = sprefs.getBoolean("quietTimeEnabled", false); mQuietTimeStarts = sprefs.getString("quietTimeStarts", "21:00"); mQuietTimeEnds = sprefs.getString("quietTimeEnds", "7:00"); - mShowCorrespondentNames = sprefs.getBoolean("showCorrespondentNames", true); mShowContactName = sprefs.getBoolean("showContactName", false); mChangeContactNameColor = sprefs.getBoolean("changeRegisteredNameColor", false); @@ -562,19 +533,15 @@ public class K9 extends Application { mMessageViewFixedWidthFont = sprefs.getBoolean("messageViewFixedWidthFont", false); mMessageViewReturnToList = sprefs.getBoolean("messageViewReturnToList", false); mMessageViewShowNext = sprefs.getBoolean("messageViewShowNext", false); - useGalleryBugWorkaround = sprefs.getBoolean("useGalleryBugWorkaround", K9.isGalleryBuggy()); - mConfirmDelete = sprefs.getBoolean("confirmDelete", false); mConfirmDeleteStarred = sprefs.getBoolean("confirmDeleteStarred", false); mConfirmSpam = sprefs.getBoolean("confirmSpam", false); mConfirmMarkAllAsRead = sprefs.getBoolean("confirmMarkAllAsRead", true); - - mKeyguardPrivacy = sprefs.getBoolean("keyguardPrivacy", false); - compactLayouts = sprefs.getBoolean("compactLayouts", false); mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString()); + mDeviceId = sprefs.getString("deviceId", null); fontSizes.load(sprefs); try { @@ -601,10 +568,8 @@ public class K9 extends Application { // Discard , as it means we're not running on a device with strict mode Log.v(K9.LOG_TAG, "Failed to turn on strict mode", e); } - } - /** * since Android invokes Application.onCreate() only after invoking all * other components' onCreate(), here is a way to notify interested @@ -738,7 +703,6 @@ public class K9 extends Application { mQuietTimeEnds = quietTimeEnds; } - public static boolean isQuietTime() { if (!mQuietTimeEnabled) { return false; @@ -760,7 +724,6 @@ public class K9 extends Application { return false; } - // 21:00 - 05:00 means we want to be quiet if it's after 9 or before 5 if (quietStarts > quietEnds) { // if it's 22:00 or 03:00 but not 8:00 @@ -781,8 +744,6 @@ public class K9 extends Application { return false; } - - public static boolean startIntegratedInbox() { return mStartIntegratedInbox; } @@ -1016,4 +977,26 @@ public class K9 extends Application { public static void setAttachmentDefaultPath(String attachmentDefaultPath) { K9.mAttachmentDefaultPath = attachmentDefaultPath; } + + /** + * Generates a uuid that is to identify this application on this device. + * The uuid is only generated once, and then persisted. + * @return the device Id + */ + public String getDeviceId() { + if (TextUtils.isEmpty(mDeviceId)) { + // Remove the dashes from the UUID, so that we have alphanumeric characters only. + // This increases the likelyhood of compatability with external systems that may + // use the ID. + mDeviceId = UUID.randomUUID().toString().replace("-", ""); + + // This particular setting is not editted through the settings view, but instead only + // set in this method. We need to commit it now to ensure it is persisted correctly. + final Preferences preferences = Preferences.getPreferences(this); + Editor editor = preferences.getPreferences().edit(); + editor.putString("deviceId", mDeviceId); + editor.commit(); + } + return mDeviceId; + } } diff --git a/src/com/fsck/k9/mail/Store.java b/src/com/fsck/k9/mail/Store.java index c06be7c44..13e3947c6 100644 --- a/src/com/fsck/k9/mail/Store.java +++ b/src/com/fsck/k9/mail/Store.java @@ -99,6 +99,7 @@ public abstract class Store { * @see ImapStore#decodeUri(String) * @see Pop3Store#decodeUri(String) * @see WebDavStore#decodeUri(String) + * @see EasStore#decodeUri(String) */ public static ServerSettings decodeStoreUri(String uri) { if (uri.startsWith("imap")) { @@ -107,6 +108,8 @@ public abstract class Store { return Pop3Store.decodeUri(uri); } else if (uri.startsWith("webdav")) { return WebDavStore.decodeUri(uri); + } else if (uri.startsWith("eas")) { + return EasStore.decodeUri(uri); } else { throw new IllegalArgumentException("Not a valid store URI"); } @@ -123,6 +126,7 @@ public abstract class Store { * @see ImapStore#createUri(ServerSettings) * @see Pop3Store#createUri(ServerSettings) * @see WebDavStore#createUri(ServerSettings) + * @see EasStore#createUri(ServerSettings) */ public static String createStoreUri(ServerSettings server) { if (ImapStore.STORE_TYPE.equals(server.type)) { @@ -131,6 +135,8 @@ public abstract class Store { return Pop3Store.createUri(server); } else if (WebDavStore.STORE_TYPE.equals(server.type)) { return WebDavStore.createUri(server); + } else if (EasStore.STORE_TYPE.equals(server.type)) { + return EasStore.createUri(server); } else { throw new IllegalArgumentException("Not a valid store URI"); } diff --git a/src/com/fsck/k9/mail/store/EasStore.java b/src/com/fsck/k9/mail/store/EasStore.java index a19ebca72..5d65da834 100644 --- a/src/com/fsck/k9/mail/store/EasStore.java +++ b/src/com/fsck/k9/mail/store/EasStore.java @@ -39,7 +39,6 @@ import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; -import android.content.Context; import android.os.PowerManager; import android.text.TextUtils; import android.util.Base64; @@ -51,6 +50,7 @@ import com.fsck.k9.controller.MessageRetrievalListener; import com.fsck.k9.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; import com.fsck.k9.mail.AuthenticationFailedException; +import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; @@ -58,6 +58,7 @@ import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.Pusher; +import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.Store; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.internet.MimeMessage; @@ -89,31 +90,32 @@ public class EasStore extends Store { // we sync the items any "collection" (emails in a folder). private static final String INITIAL_SYNC_KEY = "0"; - static private final String PING_COMMAND = "Ping"; - static private final String PROVISION_COMMAND = "Provision"; + private static final String PING_COMMAND = "Ping"; + private static final String PROVISION_COMMAND = "Provision"; + // Command timeout is the the time allowed for reading data from an open connection before an // IOException is thrown. After a small added allowance, our watch dog alarm goes off (allowing // us to detect a silently dropped connection). The allowance is defined below. - static private final int COMMAND_TIMEOUT = 30 * 1000; + private static final int COMMAND_TIMEOUT = 30 * 1000; // Connection timeout is the time given to connect to the server before reporting an IOException. - static private final int CONNECTION_TIMEOUT = 20 * 1000; + private static final int CONNECTION_TIMEOUT = 20 * 1000; // This needs to be long enough to send the longest reasonable message, without being so long // as to effectively "hang" sending of mail. The standard 30 second timeout isn't long enough // for pictures and the like. For now, we'll use 15 minutes, in the knowledge that any socket // failure would probably generate an Exception before timing out anyway. - public static final int SEND_MAIL_TIMEOUT = 15 * 60 * 1000; + private static final int SEND_MAIL_TIMEOUT = 15 * 60 * 1000; // MSFT's custom HTTP result code indicating the need to provision. - static private final int HTTP_NEED_PROVISIONING = 449; + private static final int HTTP_NEED_PROVISIONING = 449; // The EAS protocol Provision status for "we implement all of the policies". - static private final String PROVISION_STATUS_OK = "1"; + private static final String PROVISION_STATUS_OK = "1"; // The EAS protocol Provision status meaning "we partially implement the policies". - static private final String PROVISION_STATUS_PARTIAL = "2"; + private static final String PROVISION_STATUS_PARTIAL = "2"; - static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML"; - static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML"; + public static final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML"; + public static final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML"; private static final int IDLE_READ_TIMEOUT_INCREMENT = 5 * 60 * 1000; private static final int IDLE_FAILURE_COUNT_LIMIT = 10; @@ -122,77 +124,178 @@ public class EasStore extends Store { // The number of emails to fetch for each request to the server. private static final int EMAIL_WINDOW_SIZE = 10; + // The maximum length of the DeviceID parameter used by EAS is 32 characters. + private static final int MAX_DEVICE_ID_SIZE = 32; - public String mProtocolVersion; - public Double mProtocolVersionDouble; - protected String mDeviceId = null; - protected String mDeviceType = "Android"; - private String mCmdString = null; + /** + * Decodes a EasStore URI. + * + *

Possible forms:

+ *
+     * eas://user:password@server:port CONNECTION_SECURITY_NONE
+     * eas+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+     * eas+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+     * eas+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+     * eas+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+     * 
+ */ + public static ServerSettings decodeUri(String uri) { + String host; + int port; + ConnectionSecurity connectionSecurity; + String username = null; + String password = null; - private final URI mUri; /* Stores the Uniform Resource Indicator with all connection info */ - private String mHost; /* Stores the host name for the server */ - private String mUsername; /* Stores the username for authentications */ - private String mPassword; /* Stores the password for authentications */ + URI easUri; + try { + easUri = new URI(uri); + } catch (URISyntaxException use) { + throw new IllegalArgumentException("Invalid EasStore URI", use); + } + String scheme = easUri.getScheme(); + if (scheme.equals("eas")) { + connectionSecurity = ConnectionSecurity.NONE; + } else if (scheme.equals("eas+ssl")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL; + } else if (scheme.equals("eas+ssl+")) { + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; + } else if (scheme.equals("eas+tls")) { + connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL; + } else if (scheme.equals("eas+tls+")) { + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; + } else { + throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); + } + + host = easUri.getHost(); + if (host.startsWith("http")) { + String[] hostParts = host.split("://", 2); + if (hostParts.length > 1) { + host = hostParts[1]; + } + } + + port = easUri.getPort(); + + if (easUri.getUserInfo() != null) { + try { + String[] userInfoParts = easUri.getUserInfo().split(":"); + + username = URLDecoder.decode(userInfoParts[0], "UTF-8"); + + if (userInfoParts.length > 1) { + password = URLDecoder.decode(userInfoParts[1], "UTF-8"); + } + } catch (UnsupportedEncodingException enc) { + // This shouldn't happen since the encoding is hardcoded to UTF-8 + throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); + } + } + + return new ServerSettings(STORE_TYPE, host, port, connectionSecurity, null, username, password); + } + + /** + * Creates a EasStore URI with the supplied settings. + * + * @param server + * The {@link ServerSettings} object that holds the server settings. + * + * @return A EasStore URI that holds the same information as the {@code server} parameter. + * + * @see Account#getStoreUri() + * @see EasStore#decodeUri(String) + */ + public static String createUri(ServerSettings server) { + String userEnc; + String passwordEnc; + try { + userEnc = URLEncoder.encode(server.username, "UTF-8"); + passwordEnc = (server.password != null) ? + URLEncoder.encode(server.password, "UTF-8") : ""; + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Could not encode username or password", e); + } + String userInfo = userEnc + ":" + passwordEnc; + + String scheme; + switch (server.connectionSecurity) { + case SSL_TLS_OPTIONAL: + scheme = "eas+ssl"; + break; + case SSL_TLS_REQUIRED: + scheme = "eas+ssl+"; + break; + case STARTTLS_OPTIONAL: + scheme = "eas+tls"; + break; + case STARTTLS_REQUIRED: + scheme = "eas+tls+"; + break; + default: + case NONE: + scheme = "eas"; + break; + } + + try { + return new URI(scheme, userInfo, server.host, server.port, null, + null, null).toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create EasStore URI", e); + } + } + + // The following members are set during the first contact with the Exchange server, before + // we provision or send any other requests. They are synchronized by mInitializationLock. + private String mProtocolVersion = null; + private Double mProtocolVersionDouble = null; + private String mDeviceId = null; + private Object mInitializationLock = new Object(); + private final String mDeviceType = "Android"; + + private String mHost; + private String mUsername; + private String mPassword; private short mConnectionSecurity; private boolean mSecure; private HttpClient mHttpClient = null; private String mAuthString = null; + private String mCmdString = null; private HashMap mFolderList = new HashMap(); - /** - * eas://user:password@server:port CONNECTION_SECURITY_NONE - * eas+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL - * eas+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED - * eas+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL - * eas+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED - */ public EasStore(Account account) throws MessagingException { super(account); + ServerSettings settings; try { - mUri = new URI(mAccount.getStoreUri()); - } catch (URISyntaxException use) { - throw new MessagingException("Invalid WebDavStore URI", use); + settings = decodeUri(mAccount.getStoreUri()); + } catch (IllegalArgumentException e) { + throw new MessagingException("Error while decoding store URI", e); } + + mHost = settings.host; + mUsername = settings.username; + mPassword = settings.password; - String scheme = mUri.getScheme(); - if (scheme.equals("eas")) { + switch (settings.connectionSecurity) { + case NONE: mConnectionSecurity = CONNECTION_SECURITY_NONE; - } else if (scheme.equals("eas+ssl")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; - } else if (scheme.equals("eas+ssl+")) { - mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; - } else if (scheme.equals("eas+tls")) { + break; + case STARTTLS_OPTIONAL: mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; - } else if (scheme.equals("eas+tls+")) { + break; + case STARTTLS_REQUIRED: mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; - } else { - throw new MessagingException("Unsupported protocol"); - } - - mHost = mUri.getHost(); - if (mHost.startsWith("http")) { - String[] hostParts = mHost.split("://", 2); - if (hostParts.length > 1) { - mHost = hostParts[1]; - } - } - - if (mUri.getUserInfo() != null) { - try { - String[] userInfoParts = mUri.getUserInfo().split(":"); - - mUsername = URLDecoder.decode(userInfoParts[0], "UTF-8"); - - if (userInfoParts.length > 1) { - mPassword = URLDecoder.decode(userInfoParts[1], "UTF-8"); - } - } catch (UnsupportedEncodingException enc) { - // This shouldn't happen since the encoding is hardcoded to UTF-8 - Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc); - } + break; + case SSL_TLS_OPTIONAL: + mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; + break; + case SSL_TLS_REQUIRED: + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + break; } mSecure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED; @@ -218,37 +321,18 @@ public class EasStore extends Store { @Override public void checkSettings() throws MessagingException { - boolean ssl = true; - - boolean trustCertificates = true; - - validateAccount( - mHost, - mUsername, - mPassword, - mUri.getPort(), - ssl, - trustCertificates, - K9.app); + validateAccount(); } - public void validateAccount(String hostAddress, String userName, String password, int port, - boolean ssl, boolean trustCertificates, Context context) throws MessagingException { + public void validateAccount() throws MessagingException { try { - Log.i(K9.LOG_TAG, "Testing EAS: " + hostAddress + ", " + userName + ", ssl = " + (ssl ? "1" : "0")); + Log.i(K9.LOG_TAG, "Testing EAS: " + mHost + ", " + mUsername + ", ssl = " + (mSecure ? "1" : "0")); -// Account account = Preferences.getPreferences(context).newAccount(); -// account.setName("%TestAccount%"); -// account.setStoreUri(mUri.toString()); EasStore svc = new EasStore(mAccount); - svc.mHost = hostAddress; - svc.mUsername = userName; - svc.mPassword = password; -// svc.mSsl = ssl; -// svc.mTrustSsl = trustCertificates; - // We mustn't use the "real" device id or we'll screw up current accounts - // Any string will do, but we'll go for "validate" + // We musn't use the "real" device id or we'll screw up current accounts. + // Any string will do, but we'll go for "validate". svc.mDeviceId = "validate"; + HttpResponse resp = svc.sendHttpClientOptions(); reclaimConnection(resp); int code = resp.getStatusLine().getStatusCode(); @@ -319,7 +403,7 @@ public class EasStore extends Store { } private String getPolicyType() { - return (getProtocolVersion() >= + return (getProtocolVersionDouble() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE; } @@ -424,7 +508,7 @@ public class EasStore extends Store { return (code == HTTP_NEED_PROVISIONING) || (code == HttpStatus.SC_FORBIDDEN); } - private void setupProtocolVersion(EasStore service, Header versionHeader) + private static void setupProtocolVersion(EasStore service, Header versionHeader) throws MessagingException { // The string is a comma separated list of EAS versions in ascending order // e.g. 1.0,2.0,2.5,12.0,12.1 @@ -445,13 +529,15 @@ public class EasStore extends Store { Log.w(K9.LOG_TAG, "No supported EAS versions: " + supportedVersions); throw new MessagingException("MessagingException.PROTOCOL_VERSION_UNSUPPORTED"); } else { - service.mProtocolVersion = ourVersion; - service.mProtocolVersionDouble = Double.parseDouble(ourVersion); + synchronized (service.mInitializationLock) { + service.mProtocolVersion = ourVersion; + service.mProtocolVersionDouble = Double.parseDouble(ourVersion); + } } } - private Double getProtocolVersion() { - synchronized (mProtocolVersionDouble) { + private Double getProtocolVersionDouble() { + synchronized (mInitializationLock) { if (mProtocolVersionDouble == null) { try { init(); @@ -553,7 +639,9 @@ public class EasStore extends Store { */ private void setHeaders(HttpRequestBase method, boolean usePolicyKey) { method.setHeader("Authorization", mAuthString); - method.setHeader("MS-ASProtocolVersion", mProtocolVersion); + synchronized (mInitializationLock) { + method.setHeader("MS-ASProtocolVersion", mProtocolVersion); + } method.setHeader("Connection", "keep-alive"); method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION); if (usePolicyKey) { @@ -598,8 +686,11 @@ public class EasStore extends Store { String safeUserName = URLEncoder.encode(mUsername); String cs = mUsername + ':' + mPassword; mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP); - mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + - "&DeviceType=" + mDeviceType; + + synchronized (mInitializationLock) { + mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + + "&DeviceType=" + mDeviceType; + } } @Override @@ -622,30 +713,41 @@ public class EasStore extends Store { } private void init() throws IOException, MessagingException { - // Determine our protocol version, if we haven't already and save it in the Account - // Also re-check protocol version at least once a day (in case of upgrade) - boolean lastSyncTimeDayDue = false; - //lastSyncTimeDayDue = ((System.currentTimeMillis() - mMailbox.mSyncTime) > DAYS); - if (mProtocolVersion == null || lastSyncTimeDayDue) { - Log.d(K9.LOG_TAG, "Determine EAS protocol version"); - HttpResponse resp = sendHttpClientOptions(); - reclaimConnection(resp); - int code = resp.getStatusLine().getStatusCode(); - Log.d(K9.LOG_TAG, "OPTIONS response: " + code); - if (code == HttpStatus.SC_OK) { - Header header = resp.getFirstHeader("MS-ASProtocolCommands"); - Log.d(K9.LOG_TAG, header.getValue()); - header = resp.getFirstHeader("ms-asprotocolversions"); - try { - setupProtocolVersion(this, header); - } catch (MessagingException e) { - // Since we've already validated, this can't really happen - // But if it does, we'll rethrow this... + synchronized (mInitializationLock) { + // Get a unique ID to identify the device and application. + if (mDeviceId == null) { + mDeviceId = K9.app.getDeviceId(); + if (mDeviceId.length() > MAX_DEVICE_ID_SIZE) { + // This should not happen, since getDeviceId returns a UUID string with the dashes + // removed, which is always 32 characters. Best to be safe. + mDeviceId = mDeviceId.substring(0, MAX_DEVICE_ID_SIZE); + } + } + // Determine our protocol version, if we haven't already and save it in the Account + // Also re-check protocol version at least once a day (in case of upgrade) + boolean lastSyncTimeDayDue = false; + //lastSyncTimeDayDue = ((System.currentTimeMillis() - mMailbox.mSyncTime) > DAYS); + if (mProtocolVersion == null || lastSyncTimeDayDue) { + Log.d(K9.LOG_TAG, "Determine EAS protocol version"); + HttpResponse resp = sendHttpClientOptions(); + reclaimConnection(resp); + int code = resp.getStatusLine().getStatusCode(); + Log.d(K9.LOG_TAG, "OPTIONS response: " + code); + if (code == HttpStatus.SC_OK) { + Header header = resp.getFirstHeader("MS-ASProtocolCommands"); + Log.d(K9.LOG_TAG, header.getValue()); + header = resp.getFirstHeader("ms-asprotocolversions"); + try { + setupProtocolVersion(this, header); + } catch (MessagingException e) { + // Since we've already validated, this can't really happen + // But if it does, we'll rethrow this... + throw new IOException(); + } + } else { + Log.e(K9.LOG_TAG, "OPTIONS command failed; throwing IOException"); throw new IOException(); } - } else { - Log.e(K9.LOG_TAG, "OPTIONS command failed; throwing IOException"); - throw new IOException(); } } } @@ -1086,7 +1188,7 @@ public class EasStore extends Store { // Enable MimeSupport s.data(Tags.SYNC_MIME_SUPPORT, "2"); // Set the truncation amount for all classes. - if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { + if (getProtocolVersionDouble() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { s.start(Tags.BASE_BODY_PREFERENCE) // HTML for email; plain text for everything else. .data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_MIME);