summaryrefslogtreecommitdiffstats
path: root/src/com/android/email/mail
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/email/mail')
-rw-r--r--src/com/android/email/mail/Sender.java125
-rw-r--r--src/com/android/email/mail/Store.java226
-rw-r--r--src/com/android/email/mail/internet/AuthenticationCache.java162
-rw-r--r--src/com/android/email/mail/internet/OAuthAuthenticator.java191
-rw-r--r--src/com/android/email/mail/store/ImapConnection.java636
-rw-r--r--src/com/android/email/mail/store/ImapFolder.java1291
-rw-r--r--src/com/android/email/mail/store/ImapStore.java657
-rw-r--r--src/com/android/email/mail/store/Pop3Store.java833
-rw-r--r--src/com/android/email/mail/store/ServiceStore.java89
-rw-r--r--src/com/android/email/mail/store/imap/ImapConstants.java104
-rw-r--r--src/com/android/email/mail/store/imap/ImapElement.java120
-rw-r--r--src/com/android/email/mail/store/imap/ImapList.java235
-rw-r--r--src/com/android/email/mail/store/imap/ImapMemoryLiteral.java71
-rw-r--r--src/com/android/email/mail/store/imap/ImapResponse.java152
-rw-r--r--src/com/android/email/mail/store/imap/ImapResponseParser.java453
-rw-r--r--src/com/android/email/mail/store/imap/ImapSimpleString.java55
-rw-r--r--src/com/android/email/mail/store/imap/ImapString.java186
-rw-r--r--src/com/android/email/mail/store/imap/ImapTempFileLiteral.java123
-rw-r--r--src/com/android/email/mail/store/imap/ImapUtility.java126
-rw-r--r--src/com/android/email/mail/transport/DiscourseLogger.java119
-rw-r--r--src/com/android/email/mail/transport/MailTransport.java320
21 files changed, 0 insertions, 6274 deletions
diff --git a/src/com/android/email/mail/Sender.java b/src/com/android/email/mail/Sender.java
deleted file mode 100644
index 4e85d70fa..000000000
--- a/src/com/android/email/mail/Sender.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail;
-
-import android.content.Context;
-import android.content.res.XmlResourceParser;
-
-import com.android.email.R;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.mail.utils.LogUtils;
-
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-
-public abstract class Sender {
- protected static final int SOCKET_CONNECT_TIMEOUT = 10000;
-
- /**
- * Static named constructor. It should be overrode by extending class.
- * Because this method will be called through reflection, it can not be protected.
- */
- public static Sender newInstance(Account account) throws MessagingException {
- throw new MessagingException("Sender.newInstance: Unknown scheme in "
- + account.mDisplayName);
- }
-
- private static Sender instantiateSender(Context context, String className, Account account)
- throws MessagingException {
- Object o = null;
- try {
- Class<?> c = Class.forName(className);
- // and invoke "newInstance" class method and instantiate sender object.
- java.lang.reflect.Method m =
- c.getMethod("newInstance", Account.class, Context.class);
- o = m.invoke(null, account, context);
- } catch (Exception e) {
- LogUtils.d(Logging.LOG_TAG, String.format(
- "exception %s invoking method %s#newInstance(Account, Context) for %s",
- e.toString(), className, account.mDisplayName));
- throw new MessagingException("can not instantiate Sender for " + account.mDisplayName);
- }
- if (!(o instanceof Sender)) {
- throw new MessagingException(
- account.mDisplayName + ": " + className + " create incompatible object");
- }
- return (Sender) o;
- }
-
- /**
- * Find Sender implementation consulting with sender.xml file.
- */
- private static Sender findSender(Context context, int resourceId, Account account)
- throws MessagingException {
- Sender sender = null;
- try {
- XmlResourceParser xml = context.getResources().getXml(resourceId);
- int xmlEventType;
- HostAuth sendAuth = account.getOrCreateHostAuthSend(context);
- // walk through senders.xml file.
- while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
- if (xmlEventType == XmlResourceParser.START_TAG &&
- "sender".equals(xml.getName())) {
- String xmlScheme = xml.getAttributeValue(null, "scheme");
- if (sendAuth.mProtocol != null && sendAuth.mProtocol.startsWith(xmlScheme)) {
- // found sender entry whose scheme is matched with uri.
- // then load sender class.
- String className = xml.getAttributeValue(null, "class");
- sender = instantiateSender(context, className, account);
- }
- }
- }
- } catch (XmlPullParserException e) {
- // ignore
- } catch (IOException e) {
- // ignore
- }
- return sender;
- }
-
- /**
- * Get an instance of a mail sender for the given account. The account must be valid (i.e. has
- * at least an outgoing server name).
- *
- * @param context the caller's context
- * @param account the account of the sender.
- * @return an initialized sender of the appropriate class
- * @throws MessagingException If the sender cannot be obtained or if the account is invalid.
- */
- public synchronized static Sender getInstance(Context context, Account account)
- throws MessagingException {
- Context appContext = context.getApplicationContext();
- Sender sender = findSender(appContext, R.xml.senders_product, account);
- if (sender == null) {
- sender = findSender(appContext, R.xml.senders, account);
- }
- if (sender == null) {
- throw new MessagingException("Cannot find sender for account " + account.mDisplayName);
- }
- return sender;
- }
-
- public abstract void open() throws MessagingException;
-
- public abstract void sendMessage(long messageId) throws MessagingException;
-
- public abstract void close() throws MessagingException;
-}
diff --git a/src/com/android/email/mail/Store.java b/src/com/android/email/mail/Store.java
deleted file mode 100644
index 377b59448..000000000
--- a/src/com/android/email/mail/Store.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source P-roject
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail;
-
-import android.content.Context;
-import android.os.Bundle;
-
-import com.android.email.R;
-import com.android.email.mail.store.ImapStore;
-import com.android.email.mail.store.Pop3Store;
-import com.android.email.mail.store.ServiceStore;
-import com.android.email.mail.transport.MailTransport;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.mail.Folder;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.mail.utils.LogUtils;
-import com.google.common.annotations.VisibleForTesting;
-
-import java.lang.reflect.Method;
-import java.util.HashMap;
-
-/**
- * Store is the legacy equivalent of the Account class
- */
-public abstract class Store {
- /**
- * A global suggestion to Store implementors on how much of the body
- * should be returned on FetchProfile.Item.BODY_SANE requests. We'll use 125k now.
- */
- public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (125 * 1024);
-
- @VisibleForTesting
- static final HashMap<HostAuth, Store> sStores = new HashMap<HostAuth, Store>();
- protected Context mContext;
- protected Account mAccount;
- protected MailTransport mTransport;
- protected String mUsername;
- protected String mPassword;
-
- static final HashMap<String, Class<? extends Store>> sStoreClasses =
- new HashMap<String, Class<? extends Store>>();
-
- /**
- * Static named constructor. It should be overrode by extending class.
- * Because this method will be called through reflection, it can not be protected.
- */
- static Store newInstance(Account account) throws MessagingException {
- throw new MessagingException("Store#newInstance: Unknown scheme in "
- + account.mDisplayName);
- }
-
- /**
- * Get an instance of a mail store for the given account. The account must be valid (i.e. has
- * at least an incoming server name).
- *
- * NOTE: The internal algorithm used to find a cached store depends upon the account's
- * HostAuth row. If this ever changes (e.g. such as the user updating the
- * host name or port), we will leak entries. This should not be typical, so, it is not
- * a critical problem. However, it is something we should consider fixing.
- *
- * @param account The account of the store.
- * @param context For all the usual context-y stuff
- * @return an initialized store of the appropriate class
- * @throws MessagingException If the store cannot be obtained or if the account is invalid.
- */
- public synchronized static Store getInstance(Account account, Context context)
- throws MessagingException {
- if (sStores.isEmpty()) {
- sStoreClasses.put(context.getString(R.string.protocol_pop3), Pop3Store.class);
- sStoreClasses.put(context.getString(R.string.protocol_legacy_imap), ImapStore.class);
- }
- HostAuth hostAuth = account.getOrCreateHostAuthRecv(context);
- // An existing account might have been deleted
- if (hostAuth == null) return null;
- if (!account.isTemporary()) {
- Store store = sStores.get(hostAuth);
- if (store == null) {
- store = createInstanceInternal(account, context, true);
- } else {
- // Make sure the account object is up to date (according to the caller, at least)
- store.mAccount = account;
- }
- return store;
- } else {
- return createInstanceInternal(account, context, false);
- }
- }
-
- private synchronized static Store createInstanceInternal(final Account account,
- final Context context, final boolean cacheInstance)
- throws MessagingException {
- Context appContext = context.getApplicationContext();
- final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context);
- Class<? extends Store> klass = sStoreClasses.get(hostAuth.mProtocol);
- if (klass == null) {
- klass = ServiceStore.class;
- }
- final Store store;
- try {
- // invoke "newInstance" class method
- Method m = klass.getMethod("newInstance", Account.class, Context.class);
- store = (Store)m.invoke(null, account, appContext);
- } catch (Exception e) {
- LogUtils.d(Logging.LOG_TAG, String.format(
- "exception %s invoking method %s#newInstance(Account, Context) for %s",
- e.toString(), klass.getName(), account.mDisplayName));
- throw new MessagingException("Can't instantiate Store for " + account.mDisplayName);
- }
- // Don't cache this unless it's we've got a saved HostAuth
- if (hostAuth.mId != EmailContent.NOT_SAVED && cacheInstance) {
- sStores.put(hostAuth, store);
- }
- return store;
- }
-
- /**
- * Delete the mail store associated with the given account. The account must be valid (i.e. has
- * at least an incoming server name).
- *
- * The store should have been notified already by calling delete(), and the caller should
- * also take responsibility for deleting the matching LocalStore, etc.
- *
- * @throws MessagingException If the store cannot be removed or if the account is invalid.
- */
- public synchronized static Store removeInstance(Account account, Context context)
- throws MessagingException {
- return sStores.remove(HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv));
- }
-
- /**
- * Some protocols require that a sent message be copied (uploaded) into the Sent folder
- * while others can take care of it automatically (ideally, on the server). This function
- * allows a given store to indicate which mode(s) it supports.
- * @return true if the store requires an upload into "sent", false if this happens automatically
- * for any sent message.
- */
- public boolean requireCopyMessageToSentFolder() {
- return true;
- }
-
- public Folder getFolder(String name) throws MessagingException {
- return null;
- }
-
- /**
- * Updates the local list of mailboxes according to what is located on the remote server.
- * <em>Note: This does not perform folder synchronization and it will not remove mailboxes
- * that are stored locally but not remotely.</em>
- * @return The set of remote folders
- * @throws MessagingException If there was a problem connecting to the remote server
- */
- public Folder[] updateFolders() throws MessagingException {
- return null;
- }
-
- public abstract Bundle checkSettings() throws MessagingException;
-
- /**
- * Handle discovery of account settings using only the user's email address and password
- * @param context the context of the caller
- * @param emailAddress the email address of the exchange user
- * @param password the password of the exchange user
- * @return a Bundle containing an error code and a HostAuth (if successful)
- * @throws MessagingException
- */
- public Bundle autoDiscover(Context context, String emailAddress, String password)
- throws MessagingException {
- return null;
- }
-
- /**
- * Updates the fields within the given mailbox. Only the fields that are important to
- * non-EAS accounts are modified.
- */
- protected static void updateMailbox(Mailbox mailbox, long accountId, String mailboxPath,
- char delimiter, boolean selectable, int type) {
- mailbox.mAccountKey = accountId;
- mailbox.mDelimiter = delimiter;
- String displayPath = mailboxPath;
- int pathIndex = mailboxPath.lastIndexOf(delimiter);
- if (pathIndex > 0) {
- displayPath = mailboxPath.substring(pathIndex + 1);
- }
- mailbox.mDisplayName = displayPath;
- if (selectable) {
- mailbox.mFlags = Mailbox.FLAG_HOLDS_MAIL | Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
- }
- mailbox.mFlagVisible = true;
- //mailbox.mParentKey;
- //mailbox.mParentServerId;
- mailbox.mServerId = mailboxPath;
- //mailbox.mServerId;
- //mailbox.mSyncFrequency;
- //mailbox.mSyncKey;
- //mailbox.mSyncLookback;
- //mailbox.mSyncTime;
- mailbox.mType = type;
- //box.mUnreadCount;
- }
-
- public void closeConnections() {
- // Base implementation does nothing.
- }
-
- public Account getAccount() {
- return mAccount;
- }
-}
diff --git a/src/com/android/email/mail/internet/AuthenticationCache.java b/src/com/android/email/mail/internet/AuthenticationCache.java
deleted file mode 100644
index 21508af35..000000000
--- a/src/com/android/email/mail/internet/AuthenticationCache.java
+++ /dev/null
@@ -1,162 +0,0 @@
-package com.android.email.mail.internet;
-
-import android.content.Context;
-import android.text.format.DateUtils;
-
-import com.android.email.mail.internet.OAuthAuthenticator.AuthenticationResult;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.mail.AuthenticationFailedException;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.Credential;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.mail.utils.LogUtils;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-public class AuthenticationCache {
- private static AuthenticationCache sCache;
-
- // Threshold for refreshing a token. If the token is expected to expire within this amount of
- // time, we won't even bother attempting to use it and will simply force a refresh.
- private static final long EXPIRATION_THRESHOLD = 5 * DateUtils.MINUTE_IN_MILLIS;
-
- private final Map<Long, CacheEntry> mCache;
- private final OAuthAuthenticator mAuthenticator;
-
- private class CacheEntry {
- CacheEntry(long accountId, String providerId, String accessToken, String refreshToken,
- long expirationTime) {
- mAccountId = accountId;
- mProviderId = providerId;
- mAccessToken = accessToken;
- mRefreshToken = refreshToken;
- mExpirationTime = expirationTime;
- }
-
- final long mAccountId;
- String mProviderId;
- String mAccessToken;
- String mRefreshToken;
- long mExpirationTime;
- }
-
- public static AuthenticationCache getInstance() {
- synchronized (AuthenticationCache.class) {
- if (sCache == null) {
- sCache = new AuthenticationCache();
- }
- return sCache;
- }
- }
-
- private AuthenticationCache() {
- mCache = new HashMap<Long, CacheEntry>();
- mAuthenticator = new OAuthAuthenticator();
- }
-
- // Gets an access token for the given account. This may be whatever is currently cached, or
- // it may query the server to get a new one if the old one is expired or nearly expired.
- public String retrieveAccessToken(Context context, Account account) throws
- MessagingException, IOException {
- // Currently, we always use the same OAuth info for both sending and receiving.
- // If we start to allow different credential objects for sending and receiving, this
- // will need to be updated.
- CacheEntry entry = null;
- synchronized (mCache) {
- entry = getEntry(context, account);
- }
- synchronized (entry) {
- final long actualExpiration = entry.mExpirationTime - EXPIRATION_THRESHOLD;
- if (System.currentTimeMillis() > actualExpiration) {
- // This access token is pretty close to end of life. Don't bother trying to use it,
- // it might just time out while we're trying to sync. Go ahead and refresh it
- // immediately.
- refreshEntry(context, entry);
- }
- return entry.mAccessToken;
- }
- }
-
- public String refreshAccessToken(Context context, Account account) throws
- MessagingException, IOException {
- CacheEntry entry = getEntry(context, account);
- synchronized (entry) {
- refreshEntry(context, entry);
- return entry.mAccessToken;
- }
- }
-
- private CacheEntry getEntry(Context context, Account account) {
- CacheEntry entry;
- if (account.isSaved() && !account.isTemporary()) {
- entry = mCache.get(account.mId);
- if (entry == null) {
- LogUtils.d(Logging.LOG_TAG, "initializing entry from database");
- final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context);
- final Credential credential = hostAuth.getOrCreateCredential(context);
- entry = new CacheEntry(account.mId, credential.mProviderId, credential.mAccessToken,
- credential.mRefreshToken, credential.mExpiration);
- mCache.put(account.mId, entry);
- }
- } else {
- // This account is temporary, just create a temporary entry. Don't store
- // it in the cache, it won't be findable because we don't yet have an account Id.
- final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context);
- final Credential credential = hostAuth.getCredential(context);
- entry = new CacheEntry(account.mId, credential.mProviderId, credential.mAccessToken,
- credential.mRefreshToken, credential.mExpiration);
- }
- return entry;
- }
-
- private void refreshEntry(Context context, CacheEntry entry) throws
- IOException, MessagingException {
- LogUtils.d(Logging.LOG_TAG, "AuthenticationCache refreshEntry %d", entry.mAccountId);
- try {
- final AuthenticationResult result = mAuthenticator.requestRefresh(context,
- entry.mProviderId, entry.mRefreshToken);
- // Don't set the refresh token here, it's not returned by the refresh response,
- // so setting it here would make it blank.
- entry.mAccessToken = result.mAccessToken;
- entry.mExpirationTime = result.mExpiresInSeconds * DateUtils.SECOND_IN_MILLIS +
- System.currentTimeMillis();
- saveEntry(context, entry);
- } catch (AuthenticationFailedException e) {
- // This is fatal. Clear the tokens and rethrow the exception.
- LogUtils.d(Logging.LOG_TAG, "authentication failed, clearning");
- clearEntry(context, entry);
- throw e;
- } catch (MessagingException e) {
- LogUtils.d(Logging.LOG_TAG, "messaging exception");
- throw e;
- } catch (IOException e) {
- LogUtils.d(Logging.LOG_TAG, "IO exception");
- throw e;
- }
- }
-
- private void saveEntry(Context context, CacheEntry entry) {
- LogUtils.d(Logging.LOG_TAG, "saveEntry");
-
- final Account account = Account.restoreAccountWithId(context, entry.mAccountId);
- final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context);
- final Credential cred = hostAuth.getOrCreateCredential(context);
- cred.mProviderId = entry.mProviderId;
- cred.mAccessToken = entry.mAccessToken;
- cred.mRefreshToken = entry.mRefreshToken;
- cred.mExpiration = entry.mExpirationTime;
- cred.update(context, cred.toContentValues());
- }
-
- private void clearEntry(Context context, CacheEntry entry) {
- LogUtils.d(Logging.LOG_TAG, "clearEntry");
- entry.mAccessToken = "";
- entry.mRefreshToken = "";
- entry.mExpirationTime = 0;
- saveEntry(context, entry);
- mCache.remove(entry.mAccountId);
- }
-}
diff --git a/src/com/android/email/mail/internet/OAuthAuthenticator.java b/src/com/android/email/mail/internet/OAuthAuthenticator.java
deleted file mode 100644
index c3e255b5c..000000000
--- a/src/com/android/email/mail/internet/OAuthAuthenticator.java
+++ /dev/null
@@ -1,191 +0,0 @@
-package com.android.email.mail.internet;
-
-import android.content.Context;
-import android.text.format.DateUtils;
-
-import com.android.email.activity.setup.AccountSettingsUtils;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.VendorPolicyLoader.OAuthProvider;
-import com.android.emailcommon.mail.AuthenticationFailedException;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.mail.utils.LogUtils;
-
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.message.BasicNameValuePair;
-import org.apache.http.params.BasicHttpParams;
-import org.apache.http.params.HttpConnectionParams;
-import org.apache.http.params.HttpParams;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class OAuthAuthenticator {
- private static final String TAG = Logging.LOG_TAG;
-
- public static final String OAUTH_REQUEST_CODE = "code";
- public static final String OAUTH_REQUEST_REFRESH_TOKEN = "refresh_token";
- public static final String OAUTH_REQUEST_CLIENT_ID = "client_id";
- public static final String OAUTH_REQUEST_CLIENT_SECRET = "client_secret";
- public static final String OAUTH_REQUEST_REDIRECT_URI = "redirect_uri";
- public static final String OAUTH_REQUEST_GRANT_TYPE = "grant_type";
-
- public static final String JSON_ACCESS_TOKEN = "access_token";
- public static final String JSON_REFRESH_TOKEN = "refresh_token";
- public static final String JSON_EXPIRES_IN = "expires_in";
-
-
- private static final long CONNECTION_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
- private static final long COMMAND_TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS;
-
- final HttpClient mClient;
-
- public static class AuthenticationResult {
- public AuthenticationResult(final String accessToken, final String refreshToken,
- final int expiresInSeconds) {
- mAccessToken = accessToken;
- mRefreshToken = refreshToken;
- mExpiresInSeconds = expiresInSeconds;
- }
-
- @Override
- public String toString() {
- return "result access " + (mAccessToken==null?"null":"[REDACTED]") +
- " refresh " + (mRefreshToken==null?"null":"[REDACTED]") +
- " expiresInSeconds " + mExpiresInSeconds;
- }
-
- public final String mAccessToken;
- public final String mRefreshToken;
- public final int mExpiresInSeconds;
- }
-
- public OAuthAuthenticator() {
- final HttpParams params = new BasicHttpParams();
- HttpConnectionParams.setConnectionTimeout(params, (int)(CONNECTION_TIMEOUT));
- HttpConnectionParams.setSoTimeout(params, (int)(COMMAND_TIMEOUT));
- HttpConnectionParams.setSocketBufferSize(params, 8192);
- mClient = new DefaultHttpClient(params);
- }
-
- public AuthenticationResult requestAccess(final Context context, final String providerId,
- final String code) throws MessagingException, IOException {
- final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId);
- if (provider == null) {
- LogUtils.e(TAG, "invalid provider %s", providerId);
- // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
- // exception, this will at least give the user a heads up to set up their account again.
- throw new AuthenticationFailedException("Invalid provider" + providerId);
- }
-
- final HttpPost post = new HttpPost(provider.tokenEndpoint);
- post.setHeader("Content-Type", "application/x-www-form-urlencoded");
- final List<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>();
- nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CODE, code));
- nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId));
- nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret));
- nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REDIRECT_URI, provider.redirectUri));
- nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "authorization_code"));
- try {
- post.setEntity(new UrlEncodedFormEntity(nvp));
- } catch (UnsupportedEncodingException e) {
- LogUtils.e(TAG, e, "unsupported encoding");
- // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
- // exception, this will at least give the user a heads up to set up their account again.
- throw new AuthenticationFailedException("Unsupported encoding", e);
- }
-
- return doRequest(post);
- }
-
- public AuthenticationResult requestRefresh(final Context context, final String providerId,
- final String refreshToken) throws MessagingException, IOException {
- final OAuthProvider provider = AccountSettingsUtils.findOAuthProvider(context, providerId);
- if (provider == null) {
- LogUtils.e(TAG, "invalid provider %s", providerId);
- // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
- // exception, this will at least give the user a heads up to set up their account again.
- throw new AuthenticationFailedException("Invalid provider" + providerId);
- }
- final HttpPost post = new HttpPost(provider.refreshEndpoint);
- post.setHeader("Content-Type", "application/x-www-form-urlencoded");
- final List<BasicNameValuePair> nvp = new ArrayList<BasicNameValuePair>();
- nvp.add(new BasicNameValuePair(OAUTH_REQUEST_REFRESH_TOKEN, refreshToken));
- nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_ID, provider.clientId));
- nvp.add(new BasicNameValuePair(OAUTH_REQUEST_CLIENT_SECRET, provider.clientSecret));
- nvp.add(new BasicNameValuePair(OAUTH_REQUEST_GRANT_TYPE, "refresh_token"));
- try {
- post.setEntity(new UrlEncodedFormEntity(nvp));
- } catch (UnsupportedEncodingException e) {
- LogUtils.e(TAG, e, "unsupported encoding");
- // This shouldn't happen, but if it does, it's a fatal. Throw an authentication failed
- // exception, this will at least give the user a heads up to set up their account again.
- throw new AuthenticationFailedException("Unsuported encoding", e);
- }
-
- return doRequest(post);
- }
-
- private AuthenticationResult doRequest(HttpPost post) throws MessagingException,
- IOException {
- final HttpResponse response;
- response = mClient.execute(post);
- final int status = response.getStatusLine().getStatusCode();
- if (status == HttpStatus.SC_OK) {
- return parseResponse(response);
- } else if (status == HttpStatus.SC_FORBIDDEN || status == HttpStatus.SC_UNAUTHORIZED ||
- status == HttpStatus.SC_BAD_REQUEST) {
- LogUtils.e(TAG, "HTTP Authentication error getting oauth tokens %d", status);
- // This is fatal, and we probably should clear our tokens after this.
- throw new AuthenticationFailedException("Auth error getting auth token");
- } else {
- LogUtils.e(TAG, "HTTP Error %d getting oauth tokens", status);
- // This is probably a transient error, we can try again later.
- throw new MessagingException("HTTPError " + status + " getting oauth token");
- }
- }
-
- private AuthenticationResult parseResponse(HttpResponse response) throws IOException,
- MessagingException {
- final BufferedReader reader = new BufferedReader(new InputStreamReader(
- response.getEntity().getContent(), "UTF-8"));
- final StringBuilder builder = new StringBuilder();
- for (String line = null; (line = reader.readLine()) != null;) {
- builder.append(line).append("\n");
- }
- try {
- final JSONObject jsonResult = new JSONObject(builder.toString());
- final String accessToken = jsonResult.getString(JSON_ACCESS_TOKEN);
- final String expiresIn = jsonResult.getString(JSON_EXPIRES_IN);
- final String refreshToken;
- if (jsonResult.has(JSON_REFRESH_TOKEN)) {
- refreshToken = jsonResult.getString(JSON_REFRESH_TOKEN);
- } else {
- refreshToken = null;
- }
- try {
- int expiresInSeconds = Integer.valueOf(expiresIn);
- return new AuthenticationResult(accessToken, refreshToken, expiresInSeconds);
- } catch (NumberFormatException e) {
- LogUtils.e(TAG, e, "Invalid expiration %s", expiresIn);
- // This indicates a server error, we can try again later.
- throw new MessagingException("Invalid number format", e);
- }
- } catch (JSONException e) {
- LogUtils.e(TAG, e, "Invalid JSON");
- // This indicates a server error, we can try again later.
- throw new MessagingException("Invalid JSON", e);
- }
- }
-}
-
diff --git a/src/com/android/email/mail/store/ImapConnection.java b/src/com/android/email/mail/store/ImapConnection.java
deleted file mode 100644
index 3e71774fb..000000000
--- a/src/com/android/email/mail/store/ImapConnection.java
+++ /dev/null
@@ -1,636 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store;
-
-import android.text.TextUtils;
-import android.util.Base64;
-
-import com.android.email.DebugUtils;
-import com.android.email.mail.internet.AuthenticationCache;
-import com.android.email.mail.store.ImapStore.ImapException;
-import com.android.email.mail.store.imap.ImapConstants;
-import com.android.email.mail.store.imap.ImapList;
-import com.android.email.mail.store.imap.ImapResponse;
-import com.android.email.mail.store.imap.ImapResponseParser;
-import com.android.email.mail.store.imap.ImapUtility;
-import com.android.email.mail.transport.DiscourseLogger;
-import com.android.email.mail.transport.MailTransport;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.mail.AuthenticationFailedException;
-import com.android.emailcommon.mail.CertificateValidationException;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.mail.utils.LogUtils;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import javax.net.ssl.SSLException;
-
-/**
- * A cacheable class that stores the details for a single IMAP connection.
- */
-class ImapConnection {
- // Always check in FALSE
- private static final boolean DEBUG_FORCE_SEND_ID = false;
-
- /** ID capability per RFC 2971*/
- public static final int CAPABILITY_ID = 1 << 0;
- /** NAMESPACE capability per RFC 2342 */
- public static final int CAPABILITY_NAMESPACE = 1 << 1;
- /** STARTTLS capability per RFC 3501 */
- public static final int CAPABILITY_STARTTLS = 1 << 2;
- /** UIDPLUS capability per RFC 4315 */
- public static final int CAPABILITY_UIDPLUS = 1 << 3;
-
- /** The capabilities supported; a set of CAPABILITY_* values. */
- private int mCapabilities;
- static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
- MailTransport mTransport;
- private ImapResponseParser mParser;
- private ImapStore mImapStore;
- private String mLoginPhrase;
- private String mAccessToken;
- private String mIdPhrase = null;
-
- /** # of command/response lines to log upon crash. */
- private static final int DISCOURSE_LOGGER_SIZE = 64;
- private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
- /**
- * Next tag to use. All connections associated to the same ImapStore instance share the same
- * counter to make tests simpler.
- * (Some of the tests involve multiple connections but only have a single counter to track the
- * tag.)
- */
- private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
-
- // Keep others from instantiating directly
- ImapConnection(ImapStore store) {
- setStore(store);
- }
-
- void setStore(ImapStore store) {
- // TODO: maybe we should throw an exception if the connection is not closed here,
- // if it's not currently closed, then we won't reopen it, so if the credentials have
- // changed, the connection will not be reestablished.
- mImapStore = store;
- mLoginPhrase = null;
- }
-
- /**
- * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
- * username and password, or an OAUTH authentication string, with username and access token.
- * Currently, these are the only two auth mechanisms supported.
- *
- * @throws IOException
- * @throws AuthenticationFailedException
- * @return the login command string to sent to the IMAP server
- */
- String getLoginPhrase() throws MessagingException, IOException {
- // build the LOGIN string once (instead of over-and-over again.)
- if (mImapStore.getUseOAuth()) {
- // We'll recreate the login phrase if it's null, or if the access token
- // has changed.
- final String accessToken = AuthenticationCache.getInstance().retrieveAccessToken(
- mImapStore.getContext(), mImapStore.getAccount());
- if (mLoginPhrase == null || !TextUtils.equals(mAccessToken, accessToken)) {
- mAccessToken = accessToken;
- final String oauthCode = "user=" + mImapStore.getUsername() + '\001' +
- "auth=Bearer " + mAccessToken + '\001' + '\001';
- mLoginPhrase = ImapConstants.AUTHENTICATE + " " + ImapConstants.XOAUTH2 + " " +
- Base64.encodeToString(oauthCode.getBytes(), Base64.NO_WRAP);
- }
- } else {
- if (mLoginPhrase == null) {
- if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
- // build the LOGIN string once (instead of over-and-over again.)
- // apply the quoting here around the built-up password
- mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " "
- + ImapUtility.imapQuoted(mImapStore.getPassword());
- }
- }
- }
- return mLoginPhrase;
- }
-
- void open() throws IOException, MessagingException {
- if (mTransport != null && mTransport.isOpen()) {
- return;
- }
-
- try {
- // copy configuration into a clean transport, if necessary
- if (mTransport == null) {
- mTransport = mImapStore.cloneTransport();
- }
-
- mTransport.open();
-
- createParser();
-
- // BANNER
- mParser.readResponse();
-
- // CAPABILITY
- ImapResponse capabilities = queryCapabilities();
-
- boolean hasStartTlsCapability =
- capabilities.contains(ImapConstants.STARTTLS);
-
- // TLS
- ImapResponse newCapabilities = doStartTls(hasStartTlsCapability);
- if (newCapabilities != null) {
- capabilities = newCapabilities;
- }
-
- // NOTE: An IMAP response MUST be processed before issuing any new IMAP
- // requests. Subsequent requests may destroy previous response data. As
- // such, we save away capability information here for future use.
- setCapabilities(capabilities);
- String capabilityString = capabilities.flatten();
-
- // ID
- doSendId(isCapable(CAPABILITY_ID), capabilityString);
-
- // LOGIN
- doLogin();
-
- // NAMESPACE (only valid in the Authenticated state)
- doGetNamespace(isCapable(CAPABILITY_NAMESPACE));
-
- // Gets the path separator from the server
- doGetPathSeparator();
-
- mImapStore.ensurePrefixIsValid();
- } catch (SSLException e) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, e, "SSLException");
- }
- throw new CertificateValidationException(e.getMessage(), e);
- } catch (IOException ioe) {
- // NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot
- // of other code here that catches IOException and I don't want to break it.
- // This catch is only here to enhance logging of connection-time issues.
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ioe, "IOException");
- }
- throw ioe;
- } finally {
- destroyResponses();
- }
- }
-
- /**
- * Closes the connection and releases all resources. This connection can not be used again
- * until {@link #setStore(ImapStore)} is called.
- */
- void close() {
- if (mTransport != null) {
- mTransport.close();
- mTransport = null;
- }
- destroyResponses();
- mParser = null;
- mImapStore = null;
- }
-
- /**
- * Returns whether or not the specified capability is supported by the server.
- */
- private boolean isCapable(int capability) {
- return (mCapabilities & capability) != 0;
- }
-
- /**
- * Sets the capability flags according to the response provided by the server.
- * Note: We only set the capability flags that we are interested in. There are many IMAP
- * capabilities that we do not track.
- */
- private void setCapabilities(ImapResponse capabilities) {
- if (capabilities.contains(ImapConstants.ID)) {
- mCapabilities |= CAPABILITY_ID;
- }
- if (capabilities.contains(ImapConstants.NAMESPACE)) {
- mCapabilities |= CAPABILITY_NAMESPACE;
- }
- if (capabilities.contains(ImapConstants.UIDPLUS)) {
- mCapabilities |= CAPABILITY_UIDPLUS;
- }
- if (capabilities.contains(ImapConstants.STARTTLS)) {
- mCapabilities |= CAPABILITY_STARTTLS;
- }
- }
-
- /**
- * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
- * set it to {@link #mParser}.
- *
- * If we already have an {@link ImapResponseParser}, we
- * {@link #destroyResponses()} and throw it away.
- */
- private void createParser() {
- destroyResponses();
- mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse);
- }
-
- void destroyResponses() {
- if (mParser != null) {
- mParser.destroyResponses();
- }
- }
-
- boolean isTransportOpenForTest() {
- return mTransport != null && mTransport.isOpen();
- }
-
- ImapResponse readResponse() throws IOException, MessagingException {
- return mParser.readResponse();
- }
-
- /**
- * Send a single command to the server. The command will be preceded by an IMAP command
- * tag and followed by \r\n (caller need not supply them).
- *
- * @param command The command to send to the server
- * @param sensitive If true, the command will not be logged
- * @return Returns the command tag that was sent
- */
- String sendCommand(String command, boolean sensitive)
- throws MessagingException, IOException {
- LogUtils.d(Logging.LOG_TAG, "sendCommand %s", (sensitive ? IMAP_REDACTED_LOG : command));
- open();
- return sendCommandInternal(command, sensitive);
- }
-
- String sendCommandInternal(String command, boolean sensitive)
- throws MessagingException, IOException {
- if (mTransport == null) {
- throw new IOException("Null transport");
- }
- String tag = Integer.toString(mNextCommandTag.incrementAndGet());
- String commandToSend = tag + " " + command;
- mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
- mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
- return tag;
- }
-
- /**
- * Send a single, complex command to the server. The command will be preceded by an IMAP
- * command tag and followed by \r\n (caller need not supply them). After each piece of the
- * command, a response will be read which MUST be a continuation request.
- *
- * @param commands An array of Strings comprising the command to be sent to the server
- * @return Returns the command tag that was sent
- */
- String sendComplexCommand(List<String> commands, boolean sensitive) throws MessagingException,
- IOException {
- open();
- String tag = Integer.toString(mNextCommandTag.incrementAndGet());
- int len = commands.size();
- for (int i = 0; i < len; i++) {
- String commandToSend = commands.get(i);
- // The first part of the command gets the tag
- if (i == 0) {
- commandToSend = tag + " " + commandToSend;
- } else {
- // Otherwise, read the response from the previous part of the command
- ImapResponse response = readResponse();
- // If it isn't a continuation request, that's an error
- if (!response.isContinuationRequest()) {
- throw new MessagingException("Expected continuation request");
- }
- }
- // Send the command
- mTransport.writeLine(commandToSend, null);
- mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
- }
- return tag;
- }
-
- List<ImapResponse> executeSimpleCommand(String command) throws IOException, MessagingException {
- return executeSimpleCommand(command, false);
- }
-
- /**
- * Read and return all of the responses from the most recent command sent to the server
- *
- * @return a list of ImapResponses
- * @throws IOException
- * @throws MessagingException
- */
- List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
- final List<ImapResponse> responses = new ArrayList<ImapResponse>();
- ImapResponse response;
- do {
- response = mParser.readResponse();
- responses.add(response);
- } while (!response.isTagged());
-
- if (!response.isOk()) {
- final String toString = response.toString();
- final String alert = response.getAlertTextOrEmpty().getString();
- final String responseCode = response.getResponseCodeOrEmpty().getString();
- destroyResponses();
-
- // if the response code indicates an error occurred within the server, indicate that
- if (ImapConstants.UNAVAILABLE.equals(responseCode)) {
- throw new MessagingException(MessagingException.SERVER_ERROR, alert);
- }
-
- throw new ImapException(toString, alert, responseCode);
- }
- return responses;
- }
-
- /**
- * Execute a simple command at the server, a simple command being one that is sent in a single
- * line of text
- *
- * @param command the command to send to the server
- * @param sensitive whether the command should be redacted in logs (used for login)
- * @return a list of ImapResponses
- * @throws IOException
- * @throws MessagingException
- */
- List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
- throws IOException, MessagingException {
- // TODO: It may be nice to catch IOExceptions and close the connection here.
- // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
- sendCommand(command, sensitive);
- return getCommandResponses();
- }
-
- /**
- * Execute a complex command at the server, a complex command being one that must be sent in
- * multiple lines due to the use of string literals
- *
- * @param commands a list of strings that comprise the command to be sent to the server
- * @param sensitive whether the command should be redacted in logs (used for login)
- * @return a list of ImapResponses
- * @throws IOException
- * @throws MessagingException
- */
- List<ImapResponse> executeComplexCommand(List<String> commands, boolean sensitive)
- throws IOException, MessagingException {
- sendComplexCommand(commands, sensitive);
- return getCommandResponses();
- }
-
- /**
- * Query server for capabilities.
- */
- private ImapResponse queryCapabilities() throws IOException, MessagingException {
- ImapResponse capabilityResponse = null;
- for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
- if (r.is(0, ImapConstants.CAPABILITY)) {
- capabilityResponse = r;
- break;
- }
- }
- if (capabilityResponse == null) {
- throw new MessagingException("Invalid CAPABILITY response received");
- }
- return capabilityResponse;
- }
-
- /**
- * Sends client identification information to the IMAP server per RFC 2971. If
- * the server does not support the ID command, this will perform no operation.
- *
- * Interoperability hack: Never send ID to *.secureserver.net, which sends back a
- * malformed response that our parser can't deal with.
- */
- private void doSendId(boolean hasIdCapability, String capabilities)
- throws MessagingException {
- if (!hasIdCapability) return;
-
- // Never send ID to *.secureserver.net
- String host = mTransport.getHost();
- if (host.toLowerCase().endsWith(".secureserver.net")) return;
-
- // Assign user-agent string (for RFC2971 ID command)
- String mUserAgent =
- ImapStore.getImapId(mImapStore.getContext(), mImapStore.getUsername(), host,
- capabilities);
-
- if (mUserAgent != null) {
- mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
- } else if (DEBUG_FORCE_SEND_ID) {
- mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
- }
- // else: mIdPhrase = null, no ID will be emitted
-
- // Send user-agent in an RFC2971 ID command
- if (mIdPhrase != null) {
- try {
- executeSimpleCommand(mIdPhrase);
- } catch (ImapException ie) {
- // Log for debugging, but this is not a fatal problem.
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
- }
- } catch (IOException ioe) {
- // Special case to handle malformed OK responses and ignore them.
- // A true IOException will recur on the following login steps
- // This can go away after the parser is fixed - see bug 2138981
- }
- }
- }
-
- /**
- * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
- * explicitly sets a namespace (using setup UI) or if the server does not support the
- * namespace command, this will perform no operation.
- */
- private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
- // user did not specify a hard-coded prefix; try to get it from the server
- if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) {
- List<ImapResponse> responseList = Collections.emptyList();
-
- try {
- responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
- } catch (ImapException ie) {
- // Log for debugging, but this is not a fatal problem.
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
- }
- } catch (IOException ioe) {
- // Special case to handle malformed OK responses and ignore them.
- }
-
- for (ImapResponse response: responseList) {
- if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
- ImapList namespaceList = response.getListOrEmpty(1);
- ImapList namespace = namespaceList.getListOrEmpty(0);
- String namespaceString = namespace.getStringOrEmpty(0).getString();
- if (!TextUtils.isEmpty(namespaceString)) {
- mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null));
- mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString());
- }
- }
- }
- }
- }
-
- /**
- * Logs into the IMAP server
- */
- private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
- try {
- if (mImapStore.getUseOAuth()) {
- // SASL authentication can take multiple steps. Currently the only SASL
- // authentication supported is OAuth.
- doSASLAuth();
- } else {
- executeSimpleCommand(getLoginPhrase(), true);
- }
- } catch (ImapException ie) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
- }
-
- final String code = ie.getResponseCode();
- final String alertText = ie.getAlertText();
-
- // if the response code indicates expired or bad credentials, throw a special exception
- if (ImapConstants.AUTHENTICATIONFAILED.equals(code) ||
- ImapConstants.EXPIRED.equals(code)) {
- throw new AuthenticationFailedException(alertText, ie);
- }
-
- throw new MessagingException(alertText, ie);
- }
- }
-
- /**
- * Performs an SASL authentication. Currently, the only type of SASL authentication supported
- * is OAuth.
- * @throws MessagingException
- * @throws IOException
- */
- private void doSASLAuth() throws MessagingException, IOException {
- LogUtils.d(Logging.LOG_TAG, "doSASLAuth");
- ImapResponse response = getOAuthResponse();
- if (!response.isOk()) {
- // Failed to authenticate. This may be just due to an expired token.
- LogUtils.d(Logging.LOG_TAG, "failed to authenticate, retrying");
- destroyResponses();
- // Clear the login phrase, this will force us to refresh the auth token.
- mLoginPhrase = null;
- // Close the transport so that we'll retry the authentication.
- if (mTransport != null) {
- mTransport.close();
- mTransport = null;
- }
- response = getOAuthResponse();
- if (!response.isOk()) {
- LogUtils.d(Logging.LOG_TAG, "failed to authenticate, giving up");
- destroyResponses();
- throw new AuthenticationFailedException("OAuth failed after refresh");
- }
- }
- }
-
- private ImapResponse getOAuthResponse() throws IOException, MessagingException {
- ImapResponse response;
- sendCommandInternal(getLoginPhrase(), true);
- do {
- response = mParser.readResponse();
- } while (!response.isTagged() && !response.isContinuationRequest());
-
- if (response.isContinuationRequest()) {
- // SASL allows for a challenge/response type authentication, so if it doesn't yet have
- // enough info, it will send back a continuation request.
- // Currently, the only type of authentication we support is OAuth. The only case where
- // it will send a continuation request is when we fail to authenticate. We need to
- // reply with a CR/LF, and it will then return with a NO response.
- sendCommandInternal("", true);
- response = readResponse();
- }
-
- // if the response code indicates an error occurred within the server, indicate that
- final String responseCode = response.getResponseCodeOrEmpty().getString();
- if (ImapConstants.UNAVAILABLE.equals(responseCode)) {
- final String alert = response.getAlertTextOrEmpty().getString();
- throw new MessagingException(MessagingException.SERVER_ERROR, alert);
- }
-
- return response;
- }
-
- /**
- * Gets the path separator per the LIST command in RFC 3501. If the path separator
- * was obtained while obtaining the namespace or there is no prefix defined, this
- * will perform no operation.
- */
- private void doGetPathSeparator() throws MessagingException {
- // user did not specify a hard-coded prefix; try to get it from the server
- if (mImapStore.isUserPrefixSet()) {
- List<ImapResponse> responseList = Collections.emptyList();
-
- try {
- responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
- } catch (ImapException ie) {
- // Log for debugging, but this is not a fatal problem.
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
- }
- } catch (IOException ioe) {
- // Special case to handle malformed OK responses and ignore them.
- }
-
- for (ImapResponse response: responseList) {
- if (response.isDataResponse(0, ImapConstants.LIST)) {
- mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString());
- }
- }
- }
- }
-
- /**
- * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
- * to use TLS or the server does not support the TLS capability, this will perform
- * no operation.
- */
- private ImapResponse doStartTls(boolean hasStartTlsCapability)
- throws IOException, MessagingException {
- if (mTransport.canTryTlsSecurity()) {
- if (hasStartTlsCapability) {
- // STARTTLS
- executeSimpleCommand(ImapConstants.STARTTLS);
-
- mTransport.reopenTls();
- createParser();
- // Per RFC requirement (3501-6.2.1) gather new capabilities
- return(queryCapabilities());
- } else {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, "TLS not supported but required");
- }
- throw new MessagingException(MessagingException.TLS_REQUIRED);
- }
- }
- return null;
- }
-
- /** @see DiscourseLogger#logLastDiscourse() */
- void logLastDiscourse() {
- mDiscourse.logLastDiscourse();
- }
-}
diff --git a/src/com/android/email/mail/store/ImapFolder.java b/src/com/android/email/mail/store/ImapFolder.java
deleted file mode 100644
index 3a9081131..000000000
--- a/src/com/android/email/mail/store/ImapFolder.java
+++ /dev/null
@@ -1,1291 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.Base64DataException;
-
-import com.android.email.DebugUtils;
-import com.android.email.mail.store.ImapStore.ImapException;
-import com.android.email.mail.store.ImapStore.ImapMessage;
-import com.android.email.mail.store.imap.ImapConstants;
-import com.android.email.mail.store.imap.ImapElement;
-import com.android.email.mail.store.imap.ImapList;
-import com.android.email.mail.store.imap.ImapResponse;
-import com.android.email.mail.store.imap.ImapString;
-import com.android.email.mail.store.imap.ImapUtility;
-import com.android.email.service.ImapService;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.internet.BinaryTempFileBody;
-import com.android.emailcommon.internet.MimeBodyPart;
-import com.android.emailcommon.internet.MimeHeader;
-import com.android.emailcommon.internet.MimeMultipart;
-import com.android.emailcommon.internet.MimeUtility;
-import com.android.emailcommon.mail.AuthenticationFailedException;
-import com.android.emailcommon.mail.Body;
-import com.android.emailcommon.mail.FetchProfile;
-import com.android.emailcommon.mail.Flag;
-import com.android.emailcommon.mail.Folder;
-import com.android.emailcommon.mail.Message;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.mail.Part;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.service.SearchParams;
-import com.android.emailcommon.utility.CountingOutputStream;
-import com.android.emailcommon.utility.EOLConvertingOutputStream;
-import com.android.emailcommon.utility.Utility;
-import com.android.mail.utils.LogUtils;
-import com.google.common.annotations.VisibleForTesting;
-
-import org.apache.commons.io.IOUtils;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.TimeZone;
-
-class ImapFolder extends Folder {
- private final static Flag[] PERMANENT_FLAGS =
- { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED };
- private static final int COPY_BUFFER_SIZE = 16*1024;
-
- private final ImapStore mStore;
- private final String mName;
- private int mMessageCount = -1;
- private ImapConnection mConnection;
- private OpenMode mMode;
- private boolean mExists;
- /** The local mailbox associated with this remote folder */
- Mailbox mMailbox;
- /** A set of hashes that can be used to track dirtiness */
- Object mHash[];
-
- /*package*/ ImapFolder(ImapStore store, String name) {
- mStore = store;
- mName = name;
- }
-
- private void destroyResponses() {
- if (mConnection != null) {
- mConnection.destroyResponses();
- }
- }
-
- @Override
- public void open(OpenMode mode)
- throws MessagingException {
- try {
- if (isOpen()) {
- if (mMode == mode) {
- // Make sure the connection is valid.
- // If it's not we'll close it down and continue on to get a new one.
- try {
- mConnection.executeSimpleCommand(ImapConstants.NOOP);
- return;
-
- } catch (IOException ioe) {
- ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- } else {
- // Return the connection to the pool, if exists.
- close(false);
- }
- }
- synchronized (this) {
- mConnection = mStore.getConnection();
- }
- // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
- // $MDNSent)
- // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
- // NonJunk $MDNSent \*)] Flags permitted.
- // * 23 EXISTS
- // * 0 RECENT
- // * OK [UIDVALIDITY 1125022061] UIDs valid
- // * OK [UIDNEXT 57576] Predicted next UID
- // 2 OK [READ-WRITE] Select completed.
- try {
- doSelect();
- } catch (IOException ioe) {
- throw ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- } catch (AuthenticationFailedException e) {
- // Don't cache this connection, so we're forced to try connecting/login again
- mConnection = null;
- close(false);
- throw e;
- } catch (MessagingException e) {
- mExists = false;
- close(false);
- throw e;
- }
- }
-
- @Override
- @VisibleForTesting
- public boolean isOpen() {
- return mExists && mConnection != null;
- }
-
- @Override
- public OpenMode getMode() {
- return mMode;
- }
-
- @Override
- public void close(boolean expunge) {
- // TODO implement expunge
- mMessageCount = -1;
- synchronized (this) {
- mStore.poolConnection(mConnection);
- mConnection = null;
- }
- }
-
- @Override
- public String getName() {
- return mName;
- }
-
- @Override
- public boolean exists() throws MessagingException {
- if (mExists) {
- return true;
- }
- /*
- * This method needs to operate in the unselected mode as well as the selected mode
- * so we must get the connection ourselves if it's not there. We are specifically
- * not calling checkOpen() since we don't care if the folder is open.
- */
- ImapConnection connection = null;
- synchronized(this) {
- if (mConnection == null) {
- connection = mStore.getConnection();
- } else {
- connection = mConnection;
- }
- }
- try {
- connection.executeSimpleCommand(String.format(Locale.US,
- ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")",
- ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
- mExists = true;
- return true;
-
- } catch (MessagingException me) {
- // Treat IOERROR messaging exception as IOException
- if (me.getExceptionType() == MessagingException.IOERROR) {
- throw me;
- }
- return false;
-
- } catch (IOException ioe) {
- throw ioExceptionHandler(connection, ioe);
-
- } finally {
- connection.destroyResponses();
- if (mConnection == null) {
- mStore.poolConnection(connection);
- }
- }
- }
-
- // IMAP supports folder creation
- @Override
- public boolean canCreate(FolderType type) {
- return true;
- }
-
- @Override
- public boolean create(FolderType type) throws MessagingException {
- /*
- * This method needs to operate in the unselected mode as well as the selected mode
- * so we must get the connection ourselves if it's not there. We are specifically
- * not calling checkOpen() since we don't care if the folder is open.
- */
- ImapConnection connection = null;
- synchronized(this) {
- if (mConnection == null) {
- connection = mStore.getConnection();
- } else {
- connection = mConnection;
- }
- }
- try {
- connection.executeSimpleCommand(String.format(Locale.US,
- ImapConstants.CREATE + " \"%s\"",
- ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
- return true;
-
- } catch (MessagingException me) {
- return false;
-
- } catch (IOException ioe) {
- throw ioExceptionHandler(connection, ioe);
-
- } finally {
- connection.destroyResponses();
- if (mConnection == null) {
- mStore.poolConnection(connection);
- }
- }
- }
-
- @Override
- public void copyMessages(Message[] messages, Folder folder,
- MessageUpdateCallbacks callbacks) throws MessagingException {
- checkOpen();
- try {
- List<ImapResponse> responseList = mConnection.executeSimpleCommand(
- String.format(Locale.US, ImapConstants.UID_COPY + " %s \"%s\"",
- ImapStore.joinMessageUids(messages),
- ImapStore.encodeFolderName(folder.getName(), mStore.mPathPrefix)));
- // Build a message map for faster UID matching
- HashMap<String, Message> messageMap = new HashMap<String, Message>();
- boolean handledUidPlus = false;
- for (Message m : messages) {
- messageMap.put(m.getUid(), m);
- }
- // Process response to get the new UIDs
- for (ImapResponse response : responseList) {
- // All "BAD" responses are bad. Only "NO", tagged responses are bad.
- if (response.isBad() || (response.isNo() && response.isTagged())) {
- String responseText = response.getStatusResponseTextOrEmpty().getString();
- throw new MessagingException(responseText);
- }
- // Skip untagged responses; they're just status
- if (!response.isTagged()) {
- continue;
- }
- // No callback provided to report of UID changes; nothing more to do here
- // NOTE: We check this here to catch any server errors
- if (callbacks == null) {
- continue;
- }
- ImapList copyResponse = response.getListOrEmpty(1);
- String responseCode = copyResponse.getStringOrEmpty(0).getString();
- if (ImapConstants.COPYUID.equals(responseCode)) {
- handledUidPlus = true;
- String origIdSet = copyResponse.getStringOrEmpty(2).getString();
- String newIdSet = copyResponse.getStringOrEmpty(3).getString();
- String[] origIdArray = ImapUtility.getImapSequenceValues(origIdSet);
- String[] newIdArray = ImapUtility.getImapSequenceValues(newIdSet);
- // There has to be a 1:1 mapping between old and new IDs
- if (origIdArray.length != newIdArray.length) {
- throw new MessagingException("Set length mis-match; orig IDs \"" +
- origIdSet + "\" new IDs \"" + newIdSet + "\"");
- }
- for (int i = 0; i < origIdArray.length; i++) {
- final String id = origIdArray[i];
- final Message m = messageMap.get(id);
- if (m != null) {
- callbacks.onMessageUidChange(m, newIdArray[i]);
- }
- }
- }
- }
- // If the server doesn't support UIDPLUS, try a different way to get the new UID(s)
- if (callbacks != null && !handledUidPlus) {
- final ImapFolder newFolder = (ImapFolder)folder;
- try {
- // Temporarily select the destination folder
- newFolder.open(OpenMode.READ_WRITE);
- // Do the search(es) ...
- for (Message m : messages) {
- final String searchString =
- "HEADER Message-Id \"" + m.getMessageId() + "\"";
- final String[] newIdArray = newFolder.searchForUids(searchString);
- if (newIdArray.length == 1) {
- callbacks.onMessageUidChange(m, newIdArray[0]);
- }
- }
- } catch (MessagingException e) {
- // Log, but, don't abort; failures here don't need to be propagated
- LogUtils.d(Logging.LOG_TAG, "Failed to find message", e);
- } finally {
- newFolder.close(false);
- }
- // Re-select the original folder
- doSelect();
- }
- } catch (IOException ioe) {
- throw ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- }
-
- @Override
- public int getMessageCount() {
- return mMessageCount;
- }
-
- @Override
- public int getUnreadMessageCount() throws MessagingException {
- checkOpen();
- try {
- int unreadMessageCount = 0;
- final List<ImapResponse> responses = mConnection.executeSimpleCommand(
- String.format(Locale.US,
- ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")",
- ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
- // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292)
- for (ImapResponse response : responses) {
- if (response.isDataResponse(0, ImapConstants.STATUS)) {
- unreadMessageCount = response.getListOrEmpty(2)
- .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero();
- }
- }
- return unreadMessageCount;
- } catch (IOException ioe) {
- throw ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- }
-
- @Override
- public void delete(boolean recurse) {
- throw new Error("ImapStore.delete() not yet implemented");
- }
-
- String[] getSearchUids(List<ImapResponse> responses) {
- // S: * SEARCH 2 3 6
- final ArrayList<String> uids = new ArrayList<String>();
- for (ImapResponse response : responses) {
- if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
- continue;
- }
- // Found SEARCH response data
- for (int i = 1; i < response.size(); i++) {
- ImapString s = response.getStringOrEmpty(i);
- if (s.isString()) {
- uids.add(s.getString());
- }
- }
- }
- return uids.toArray(Utility.EMPTY_STRINGS);
- }
-
- String[] searchForUids(String searchCriteria) throws MessagingException {
- return searchForUids(searchCriteria, true);
- }
-
- /**
- * I'm not a fan of having a parameter that determines whether to throw exceptions or
- * consume them, but getMessage() for a date range needs to differentiate between
- * a failure and just a legitimate empty result.
- * See b/11183568.
- * TODO:
- * Either figure out how to make getMessage() with a date range work without this
- * exception information, or make all users of searchForUids() handle the ImapException.
- * It's too late in the release cycle to add this risk right now.
- */
- @VisibleForTesting
- String[] searchForUids(String searchCriteria, boolean swallowException)
- throws MessagingException {
- checkOpen();
- try {
- try {
- final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
- final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
- LogUtils.d(Logging.LOG_TAG, "searchForUids '" + searchCriteria + "' results: " +
- result.length);
- return result;
- } catch (ImapException me) {
- LogUtils.d(Logging.LOG_TAG, me, "ImapException in search: " + searchCriteria);
- if (swallowException) {
- return Utility.EMPTY_STRINGS; // Not found
- } else {
- throw me;
- }
- } catch (IOException ioe) {
- LogUtils.d(Logging.LOG_TAG, ioe, "IOException in search: " + searchCriteria);
- throw ioExceptionHandler(mConnection, ioe);
- }
- } finally {
- destroyResponses();
- }
- }
-
- @Override
- @VisibleForTesting
- public Message getMessage(String uid) throws MessagingException {
- checkOpen();
-
- final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
- for (int i = 0; i < uids.length; i++) {
- if (uids[i].equals(uid)) {
- return new ImapMessage(uid, this);
- }
- }
- return null;
- }
-
- @VisibleForTesting
- protected static boolean isAsciiString(String str) {
- int len = str.length();
- for (int i = 0; i < len; i++) {
- char c = str.charAt(i);
- if (c >= 128) return false;
- }
- return true;
- }
-
- /**
- * Retrieve messages based on search parameters. We search FROM, TO, CC, SUBJECT, and BODY
- * We send: SEARCH OR FROM "foo" (OR TO "foo" (OR CC "foo" (OR SUBJECT "foo" BODY "foo"))), but
- * with the additional CHARSET argument and sending "foo" as a literal (e.g. {3}<CRLF>foo}
- */
- @Override
- @VisibleForTesting
- public Message[] getMessages(SearchParams params, MessageRetrievalListener listener)
- throws MessagingException {
- List<String> commands = new ArrayList<String>();
- final String filter = params.mFilter;
- // All servers MUST accept US-ASCII, so we'll send this as the CHARSET unless we're really
- // dealing with a string that contains non-ascii characters
- String charset = "US-ASCII";
- if (!isAsciiString(filter)) {
- charset = "UTF-8";
- }
- // This is the length of the string in octets (bytes), formatted as a string literal {n}
- final String octetLength = "{" + filter.getBytes().length + "}";
- // Break the command up into pieces ending with the string literal length
- commands.add(ImapConstants.UID_SEARCH + " CHARSET " + charset + " OR FROM " + octetLength);
- commands.add(filter + " (OR TO " + octetLength);
- commands.add(filter + " (OR CC " + octetLength);
- commands.add(filter + " (OR SUBJECT " + octetLength);
- commands.add(filter + " BODY " + octetLength);
- commands.add(filter + ")))");
- return getMessagesInternal(complexSearchForUids(commands), listener);
- }
-
- /* package */ String[] complexSearchForUids(List<String> commands) throws MessagingException {
- checkOpen();
- try {
- try {
- return getSearchUids(mConnection.executeComplexCommand(commands, false));
- } catch (ImapException e) {
- return Utility.EMPTY_STRINGS; // not found;
- } catch (IOException ioe) {
- throw ioExceptionHandler(mConnection, ioe);
- }
- } finally {
- destroyResponses();
- }
- }
-
- @Override
- @VisibleForTesting
- public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
- throws MessagingException {
- if (start < 1 || end < 1 || end < start) {
- throw new MessagingException(String.format("Invalid range: %d %d", start, end));
- }
- LogUtils.d(Logging.LOG_TAG, "getMessages number " + start + " - " + end);
- return getMessagesInternal(
- searchForUids(String.format(Locale.US, "%d:%d NOT DELETED", start, end)), listener);
- }
-
- private String generateDateRangeCommand(final long startDate, final long endDate,
- boolean useQuotes)
- throws MessagingException {
- // Dates must be formatted like: 7-Feb-1994. Time info within a date is not
- // universally supported.
- // XXX can I limit the maximum number of results?
- final SimpleDateFormat formatter = new SimpleDateFormat("dd-LLL-yyyy", Locale.US);
- formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
- final String sinceDateStr = formatter.format(endDate);
-
- StringBuilder queryParam = new StringBuilder();
- queryParam.append( "1:* ");
- // If the caller requests a startDate of zero, then ignore the BEFORE parameter.
- // This makes sure that we can always query for the newest messages, even if our
- // time is different from the imap server's time.
- if (startDate != 0) {
- final String beforeDateStr = formatter.format(startDate);
- if (startDate < endDate) {
- throw new MessagingException(String.format("Invalid date range: %s - %s",
- sinceDateStr, beforeDateStr));
- }
- queryParam.append("BEFORE ");
- if (useQuotes) queryParam.append('\"');
- queryParam.append(beforeDateStr);
- if (useQuotes) queryParam.append('\"');
- queryParam.append(" ");
- }
- queryParam.append("SINCE ");
- if (useQuotes) queryParam.append('\"');
- queryParam.append(sinceDateStr);
- if (useQuotes) queryParam.append('\"');
-
- return queryParam.toString();
- }
-
- @Override
- @VisibleForTesting
- public Message[] getMessages(long startDate, long endDate, MessageRetrievalListener listener)
- throws MessagingException {
- String [] uids = null;
- String command = generateDateRangeCommand(startDate, endDate, false);
- LogUtils.d(Logging.LOG_TAG, "getMessages dateRange " + command.toString());
-
- try {
- uids = searchForUids(command.toString(), false);
- } catch (ImapException e) {
- // TODO: This is a last minute hack to make certain servers work. Some servers
- // demand that the date in the date range be surrounded by double quotes, other
- // servers won't accept that. So if we can an ImapException using one method,
- // try the other.
- // See b/11183568
- LogUtils.d(Logging.LOG_TAG, e, "query failed %s, trying alternate",
- command.toString());
- command = generateDateRangeCommand(startDate, endDate, true);
- try {
- uids = searchForUids(command, true);
- } catch (ImapException e2) {
- LogUtils.w(Logging.LOG_TAG, e2, "query failed %s, fatal", command);
- uids = null;
- }
- }
- return getMessagesInternal(uids, listener);
- }
-
- @Override
- @VisibleForTesting
- public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
- throws MessagingException {
- if (uids == null) {
- uids = searchForUids("1:* NOT DELETED");
- }
- return getMessagesInternal(uids, listener);
- }
-
- public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) {
- final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
- for (int i = 0; i < uids.length; i++) {
- final String uid = uids[i];
- final ImapMessage message = new ImapMessage(uid, this);
- messages.add(message);
- if (listener != null) {
- listener.messageRetrieved(message);
- }
- }
- return messages.toArray(Message.EMPTY_ARRAY);
- }
-
- @Override
- public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
- throws MessagingException {
- try {
- fetchInternal(messages, fp, listener);
- } catch (RuntimeException e) { // Probably a parser error.
- LogUtils.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage());
- if (mConnection != null) {
- mConnection.logLastDiscourse();
- }
- throw e;
- }
- }
-
- public void fetchInternal(Message[] messages, FetchProfile fp,
- MessageRetrievalListener listener) throws MessagingException {
- if (messages.length == 0) {
- return;
- }
- checkOpen();
- HashMap<String, Message> messageMap = new HashMap<String, Message>();
- for (Message m : messages) {
- messageMap.put(m.getUid(), m);
- }
-
- /*
- * Figure out what command we are going to run:
- * FLAGS - UID FETCH (FLAGS)
- * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
- * HEADER.FIELDS (date subject from content-type to cc)])
- * STRUCTURE - UID FETCH (BODYSTRUCTURE)
- * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
- * BODY - UID FETCH (BODY.PEEK[])
- * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
- */
-
- final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
-
- fetchFields.add(ImapConstants.UID);
- if (fp.contains(FetchProfile.Item.FLAGS)) {
- fetchFields.add(ImapConstants.FLAGS);
- }
- if (fp.contains(FetchProfile.Item.ENVELOPE)) {
- fetchFields.add(ImapConstants.INTERNALDATE);
- fetchFields.add(ImapConstants.RFC822_SIZE);
- fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
- }
- if (fp.contains(FetchProfile.Item.STRUCTURE)) {
- fetchFields.add(ImapConstants.BODYSTRUCTURE);
- }
-
- if (fp.contains(FetchProfile.Item.BODY_SANE)) {
- fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
- }
- if (fp.contains(FetchProfile.Item.BODY)) {
- fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
- }
-
- // TODO Why are we only fetching the first part given?
- final Part fetchPart = fp.getFirstPart();
- if (fetchPart != null) {
- final String[] partIds =
- fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
- // TODO Why can a single part have more than one Id? And why should we only fetch
- // the first id if there are more than one?
- if (partIds != null) {
- fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE
- + "[" + partIds[0] + "]");
- }
- }
-
- try {
- mConnection.sendCommand(String.format(Locale.US,
- ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages),
- Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
- ), false);
- ImapResponse response;
- do {
- response = null;
- try {
- response = mConnection.readResponse();
-
- if (!response.isDataResponse(1, ImapConstants.FETCH)) {
- continue; // Ignore
- }
- final ImapList fetchList = response.getListOrEmpty(2);
- final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID)
- .getString();
- if (TextUtils.isEmpty(uid)) continue;
-
- ImapMessage message = (ImapMessage) messageMap.get(uid);
- if (message == null) continue;
-
- if (fp.contains(FetchProfile.Item.FLAGS)) {
- final ImapList flags =
- fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
- for (int i = 0, count = flags.size(); i < count; i++) {
- final ImapString flag = flags.getStringOrEmpty(i);
- if (flag.is(ImapConstants.FLAG_DELETED)) {
- message.setFlagInternal(Flag.DELETED, true);
- } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
- message.setFlagInternal(Flag.ANSWERED, true);
- } else if (flag.is(ImapConstants.FLAG_SEEN)) {
- message.setFlagInternal(Flag.SEEN, true);
- } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
- message.setFlagInternal(Flag.FLAGGED, true);
- }
- }
- }
- if (fp.contains(FetchProfile.Item.ENVELOPE)) {
- final Date internalDate = fetchList.getKeyedStringOrEmpty(
- ImapConstants.INTERNALDATE).getDateOrNull();
- final int size = fetchList.getKeyedStringOrEmpty(
- ImapConstants.RFC822_SIZE).getNumberOrZero();
- final String header = fetchList.getKeyedStringOrEmpty(
- ImapConstants.BODY_BRACKET_HEADER, true).getString();
-
- message.setInternalDate(internalDate);
- message.setSize(size);
- message.parse(Utility.streamFromAsciiString(header));
- }
- if (fp.contains(FetchProfile.Item.STRUCTURE)) {
- ImapList bs = fetchList.getKeyedListOrEmpty(
- ImapConstants.BODYSTRUCTURE);
- if (!bs.isEmpty()) {
- try {
- parseBodyStructure(bs, message, ImapConstants.TEXT);
- } catch (MessagingException e) {
- if (Logging.LOGD) {
- LogUtils.v(Logging.LOG_TAG, e, "Error handling message");
- }
- message.setBody(null);
- }
- }
- }
- if (fp.contains(FetchProfile.Item.BODY)
- || fp.contains(FetchProfile.Item.BODY_SANE)) {
- // Body is keyed by "BODY[]...".
- // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
- // TODO Should we accept "RFC822" as well??
- ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
- InputStream bodyStream = body.getAsStream();
- message.parse(bodyStream);
- }
- if (fetchPart != null) {
- InputStream bodyStream =
- fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
- String encodings[] = fetchPart.getHeader(
- MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
-
- String contentTransferEncoding = null;
- if (encodings != null && encodings.length > 0) {
- contentTransferEncoding = encodings[0];
- } else {
- // According to http://tools.ietf.org/html/rfc2045#section-6.1
- // "7bit" is the default.
- contentTransferEncoding = "7bit";
- }
-
- try {
- // TODO Don't create 2 temp files.
- // decodeBody creates BinaryTempFileBody, but we could avoid this
- // if we implement ImapStringBody.
- // (We'll need to share a temp file. Protect it with a ref-count.)
- fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding,
- fetchPart.getSize(), listener));
- } catch(Exception e) {
- // TODO: Figure out what kinds of exceptions might actually be thrown
- // from here. This blanket catch-all is because we're not sure what to
- // do if we don't have a contentTransferEncoding, and we don't have
- // time to figure out what exceptions might be thrown.
- LogUtils.e(Logging.LOG_TAG, "Error fetching body %s", e);
- }
- }
-
- if (listener != null) {
- listener.messageRetrieved(message);
- }
- } finally {
- destroyResponses();
- }
- } while (!response.isTagged());
- } catch (IOException ioe) {
- throw ioExceptionHandler(mConnection, ioe);
- }
- }
-
- /**
- * Removes any content transfer encoding from the stream and returns a Body.
- * This code is taken/condensed from MimeUtility.decodeBody
- */
- private static Body decodeBody(InputStream in, String contentTransferEncoding, int size,
- MessageRetrievalListener listener) throws IOException {
- // Get a properly wrapped input stream
- in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
- BinaryTempFileBody tempBody = new BinaryTempFileBody();
- OutputStream out = tempBody.getOutputStream();
- try {
- byte[] buffer = new byte[COPY_BUFFER_SIZE];
- int n = 0;
- int count = 0;
- while (-1 != (n = in.read(buffer))) {
- out.write(buffer, 0, n);
- count += n;
- if (listener != null) {
- if (size == 0) {
- // We don't know how big the file is, so just fake it.
- listener.loadAttachmentProgress((int)Math.ceil(100 * (1-1.0/count)));
- } else {
- listener.loadAttachmentProgress(count * 100 / size);
- }
- }
- }
- } catch (Base64DataException bde) {
- String warning = "\n\n" + ImapService.getMessageDecodeErrorString();
- out.write(warning.getBytes());
- } finally {
- out.close();
- }
- return tempBody;
- }
-
- @Override
- public Flag[] getPermanentFlags() {
- return PERMANENT_FLAGS;
- }
-
- /**
- * Handle any untagged responses that the caller doesn't care to handle themselves.
- * @param responses
- */
- private void handleUntaggedResponses(List<ImapResponse> responses) {
- for (ImapResponse response : responses) {
- handleUntaggedResponse(response);
- }
- }
-
- /**
- * Handle an untagged response that the caller doesn't care to handle themselves.
- * @param response
- */
- private void handleUntaggedResponse(ImapResponse response) {
- if (response.isDataResponse(1, ImapConstants.EXISTS)) {
- mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
- }
- }
-
- private static void parseBodyStructure(ImapList bs, Part part, String id)
- throws MessagingException {
- if (bs.getElementOrNone(0).isList()) {
- /*
- * This is a multipart/*
- */
- MimeMultipart mp = new MimeMultipart();
- for (int i = 0, count = bs.size(); i < count; i++) {
- ImapElement e = bs.getElementOrNone(i);
- if (e.isList()) {
- /*
- * For each part in the message we're going to add a new BodyPart and parse
- * into it.
- */
- MimeBodyPart bp = new MimeBodyPart();
- if (id.equals(ImapConstants.TEXT)) {
- parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
-
- } else {
- parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
- }
- mp.addBodyPart(bp);
-
- } else {
- if (e.isString()) {
- mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
- }
- break; // Ignore the rest of the list.
- }
- }
- part.setBody(mp);
- } else {
- /*
- * This is a body. We need to add as much information as we can find out about
- * it to the Part.
- */
-
- /*
- body type
- body subtype
- body parameter parenthesized list
- body id
- body description
- body encoding
- body size
- */
-
- final ImapString type = bs.getStringOrEmpty(0);
- final ImapString subType = bs.getStringOrEmpty(1);
- final String mimeType =
- (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
-
- final ImapList bodyParams = bs.getListOrEmpty(2);
- final ImapString cid = bs.getStringOrEmpty(3);
- final ImapString encoding = bs.getStringOrEmpty(5);
- final int size = bs.getStringOrEmpty(6).getNumberOrZero();
-
- if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
- // A body type of type MESSAGE and subtype RFC822
- // contains, immediately after the basic fields, the
- // envelope structure, body structure, and size in
- // text lines of the encapsulated message.
- // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
- // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
- /*
- * This will be caught by fetch and handled appropriately.
- */
- throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822
- + " not yet supported.");
- }
-
- /*
- * Set the content type with as much information as we know right now.
- */
- final StringBuilder contentType = new StringBuilder(mimeType);
-
- /*
- * If there are body params we might be able to get some more information out
- * of them.
- */
- for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
-
- // TODO We need to convert " into %22, but
- // because MimeUtility.getHeaderParameter doesn't recognize it,
- // we can't fix it for now.
- contentType.append(String.format(";\n %s=\"%s\"",
- bodyParams.getStringOrEmpty(i - 1).getString(),
- bodyParams.getStringOrEmpty(i).getString()));
- }
-
- part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
-
- // Extension items
- final ImapList bodyDisposition;
-
- if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
- // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
- // So, if it's not a list, use 10th element.
- // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
- bodyDisposition = bs.getListOrEmpty(9);
- } else {
- bodyDisposition = bs.getListOrEmpty(8);
- }
-
- final StringBuilder contentDisposition = new StringBuilder();
-
- if (bodyDisposition.size() > 0) {
- final String bodyDisposition0Str =
- bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
- if (!TextUtils.isEmpty(bodyDisposition0Str)) {
- contentDisposition.append(bodyDisposition0Str);
- }
-
- final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
- if (!bodyDispositionParams.isEmpty()) {
- /*
- * If there is body disposition information we can pull some more
- * information about the attachment out.
- */
- for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
-
- // TODO We need to convert " into %22. See above.
- contentDisposition.append(String.format(Locale.US, ";\n %s=\"%s\"",
- bodyDispositionParams.getStringOrEmpty(i - 1)
- .getString().toLowerCase(Locale.US),
- bodyDispositionParams.getStringOrEmpty(i).getString()));
- }
- }
- }
-
- if ((size > 0)
- && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size")
- == null)) {
- contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
- }
-
- if (contentDisposition.length() > 0) {
- /*
- * Set the content disposition containing at least the size. Attachment
- * handling code will use this down the road.
- */
- part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
- contentDisposition.toString());
- }
-
- /*
- * Set the Content-Transfer-Encoding header. Attachment code will use this
- * to parse the body.
- */
- if (!encoding.isEmpty()) {
- part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
- encoding.getString());
- }
-
- /*
- * Set the Content-ID header.
- */
- if (!cid.isEmpty()) {
- part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
- }
-
- if (size > 0) {
- if (part instanceof ImapMessage) {
- ((ImapMessage) part).setSize(size);
- } else if (part instanceof MimeBodyPart) {
- ((MimeBodyPart) part).setSize(size);
- } else {
- throw new MessagingException("Unknown part type " + part.toString());
- }
- }
- part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
- }
-
- }
-
- /**
- * Appends the given messages to the selected folder. This implementation also determines
- * the new UID of the given message on the IMAP server and sets the Message's UID to the
- * new server UID.
- * @param message Message
- * @param noTimeout Set to true on manual syncs, disables the timeout after sending the message
- * content to the server
- */
- @Override
- public void appendMessage(final Context context, final Message message, final boolean noTimeout)
- throws MessagingException {
- checkOpen();
- try {
- // Create temp file
- /**
- * We need to know the encoded message size before we upload it, and encoding
- * attachments as Base64, possibly reading from a slow provider, is a non-trivial
- * operation. So we write the contents to a temp file while measuring the size,
- * and then use that temp file and size to do the actual upsync.
- * For context, most classic email clients would store the message in RFC822 format
- * internally, and so would not need to do this on-the-fly.
- */
- final File tempDir = context.getExternalCacheDir();
- final File tempFile = File.createTempFile("IMAPupsync", ".eml", tempDir);
- // Delete here so we don't leave the file lingering. We've got a handle to it so we
- // can still use it.
- final boolean deleteSuccessful = tempFile.delete();
- if (!deleteSuccessful) {
- LogUtils.w(LogUtils.TAG, "Could not delete temp file %s",
- tempFile.getAbsolutePath());
- }
- final OutputStream tempOut = new FileOutputStream(tempFile);
- // Create output count while writing temp file
- final CountingOutputStream out = new CountingOutputStream(tempOut);
- final EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out);
- message.writeTo(eolOut);
- eolOut.flush();
- // Create flag list (most often this will be "\SEEN")
- String flagList = "";
- Flag[] flags = message.getFlags();
- if (flags.length > 0) {
- StringBuilder sb = new StringBuilder();
- for (final Flag flag : flags) {
- if (flag == Flag.SEEN) {
- sb.append(" " + ImapConstants.FLAG_SEEN);
- } else if (flag == Flag.FLAGGED) {
- sb.append(" " + ImapConstants.FLAG_FLAGGED);
- }
- }
- if (sb.length() > 0) {
- flagList = sb.substring(1);
- }
- }
-
- mConnection.sendCommand(
- String.format(Locale.US, ImapConstants.APPEND + " \"%s\" (%s) {%d}",
- ImapStore.encodeFolderName(mName, mStore.mPathPrefix),
- flagList,
- out.getCount()), false);
- ImapResponse response;
- do {
- final int socketTimeout = mConnection.mTransport.getSoTimeout();
- try {
- // Need to set the timeout to unlimited since we might be upsyncing a pretty
- // big attachment so who knows how long it'll take. It would sure be nice
- // if this only timed out after the send buffer drained but welp.
- if (noTimeout) {
- // For now, only unset the timeout if we're doing a manual sync
- mConnection.mTransport.setSoTimeout(0);
- }
- response = mConnection.readResponse();
- if (response.isContinuationRequest()) {
- final OutputStream transportOutputStream =
- mConnection.mTransport.getOutputStream();
- IOUtils.copyLarge(new FileInputStream(tempFile), transportOutputStream);
- transportOutputStream.write('\r');
- transportOutputStream.write('\n');
- transportOutputStream.flush();
- } else if (!response.isTagged()) {
- handleUntaggedResponse(response);
- }
- } finally {
- mConnection.mTransport.setSoTimeout(socketTimeout);
- }
- } while (!response.isTagged());
-
- // TODO Why not check the response?
-
- /*
- * Try to recover the UID of the message from an APPENDUID response.
- * e.g. 11 OK [APPENDUID 2 238268] APPEND completed
- */
- final ImapList appendList = response.getListOrEmpty(1);
- if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) {
- String serverUid = appendList.getStringOrEmpty(2).getString();
- if (!TextUtils.isEmpty(serverUid)) {
- message.setUid(serverUid);
- return;
- }
- }
-
- /*
- * Try to find the UID of the message we just appended using the
- * Message-ID header. If there are more than one response, take the
- * last one, as it's most likely the newest (the one we just uploaded).
- */
- final String messageId = message.getMessageId();
- if (messageId == null || messageId.length() == 0) {
- return;
- }
- // Most servers don't care about parenthesis in the search query [and, some
- // fail to work if they are used]
- String[] uids = searchForUids(
- String.format(Locale.US, "HEADER MESSAGE-ID %s", messageId));
- if (uids.length > 0) {
- message.setUid(uids[0]);
- }
- // However, there's at least one server [AOL] that fails to work unless there
- // are parenthesis, so, try this as a last resort
- uids = searchForUids(String.format(Locale.US, "(HEADER MESSAGE-ID %s)", messageId));
- if (uids.length > 0) {
- message.setUid(uids[0]);
- }
- } catch (IOException ioe) {
- throw ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- }
-
- @Override
- public Message[] expunge() throws MessagingException {
- checkOpen();
- try {
- handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
- } catch (IOException ioe) {
- throw ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- return null;
- }
-
- @Override
- public void setFlags(Message[] messages, Flag[] flags, boolean value)
- throws MessagingException {
- checkOpen();
-
- String allFlags = "";
- if (flags.length > 0) {
- StringBuilder flagList = new StringBuilder();
- for (int i = 0, count = flags.length; i < count; i++) {
- Flag flag = flags[i];
- if (flag == Flag.SEEN) {
- flagList.append(" " + ImapConstants.FLAG_SEEN);
- } else if (flag == Flag.DELETED) {
- flagList.append(" " + ImapConstants.FLAG_DELETED);
- } else if (flag == Flag.FLAGGED) {
- flagList.append(" " + ImapConstants.FLAG_FLAGGED);
- } else if (flag == Flag.ANSWERED) {
- flagList.append(" " + ImapConstants.FLAG_ANSWERED);
- }
- }
- allFlags = flagList.substring(1);
- }
- try {
- mConnection.executeSimpleCommand(String.format(Locale.US,
- ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
- ImapStore.joinMessageUids(messages),
- value ? "+" : "-",
- allFlags));
-
- } catch (IOException ioe) {
- throw ioExceptionHandler(mConnection, ioe);
- } finally {
- destroyResponses();
- }
- }
-
- /**
- * Persists this folder. We will always perform the proper database operation (e.g.
- * 'save' or 'update'). As an optimization, if a folder has not been modified, no
- * database operations are performed.
- */
- void save(Context context) {
- final Mailbox mailbox = mMailbox;
- if (!mailbox.isSaved()) {
- mailbox.save(context);
- mHash = mailbox.getHashes();
- } else {
- Object[] hash = mailbox.getHashes();
- if (!Arrays.equals(mHash, hash)) {
- mailbox.update(context, mailbox.toContentValues());
- mHash = hash; // Save updated hash
- }
- }
- }
-
- /**
- * Selects the folder for use. Before performing any operations on this folder, it
- * must be selected.
- */
- private void doSelect() throws IOException, MessagingException {
- final List<ImapResponse> responses = mConnection.executeSimpleCommand(
- String.format(Locale.US, ImapConstants.SELECT + " \"%s\"",
- ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
-
- // Assume the folder is opened read-write; unless we are notified otherwise
- mMode = OpenMode.READ_WRITE;
- int messageCount = -1;
- for (ImapResponse response : responses) {
- if (response.isDataResponse(1, ImapConstants.EXISTS)) {
- messageCount = response.getStringOrEmpty(0).getNumberOrZero();
- } else if (response.isOk()) {
- final ImapString responseCode = response.getResponseCodeOrEmpty();
- if (responseCode.is(ImapConstants.READ_ONLY)) {
- mMode = OpenMode.READ_ONLY;
- } else if (responseCode.is(ImapConstants.READ_WRITE)) {
- mMode = OpenMode.READ_WRITE;
- }
- } else if (response.isTagged()) { // Not OK
- throw new MessagingException("Can't open mailbox: "
- + response.getStatusResponseTextOrEmpty());
- }
- }
- if (messageCount == -1) {
- throw new MessagingException("Did not find message count during select");
- }
- mMessageCount = messageCount;
- mExists = true;
- }
-
- private void checkOpen() throws MessagingException {
- if (!isOpen()) {
- throw new MessagingException("Folder " + mName + " is not open.");
- }
- }
-
- private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
- }
- connection.close();
- if (connection == mConnection) {
- mConnection = null; // To prevent close() from returning the connection to the pool.
- close(false);
- }
- return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
- }
-
- @Override
- public boolean equals(Object o) {
- if (o instanceof ImapFolder) {
- return ((ImapFolder)o).mName.equals(mName);
- }
- return super.equals(o);
- }
-
- @Override
- public Message createMessage(String uid) {
- return new ImapMessage(uid, this);
- }
-}
diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java
deleted file mode 100644
index d45188837..000000000
--- a/src/com/android/email/mail/store/ImapStore.java
+++ /dev/null
@@ -1,657 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store;
-
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
-import android.util.Base64;
-
-import com.android.email.LegacyConversions;
-import com.android.email.Preferences;
-import com.android.email.mail.Store;
-import com.android.email.mail.store.imap.ImapConstants;
-import com.android.email.mail.store.imap.ImapResponse;
-import com.android.email.mail.store.imap.ImapString;
-import com.android.email.mail.transport.MailTransport;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.VendorPolicyLoader;
-import com.android.emailcommon.internet.MimeMessage;
-import com.android.emailcommon.mail.AuthenticationFailedException;
-import com.android.emailcommon.mail.Flag;
-import com.android.emailcommon.mail.Folder;
-import com.android.emailcommon.mail.Message;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.Credential;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.service.EmailServiceProxy;
-import com.android.emailcommon.utility.Utility;
-import com.android.mail.utils.LogUtils;
-import com.beetstra.jutf7.CharsetProvider;
-import com.google.common.annotations.VisibleForTesting;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.regex.Pattern;
-
-
-/**
- * <pre>
- * TODO Need to start keeping track of UIDVALIDITY
- * TODO Need a default response handler for things like folder updates
- * TODO In fetch(), if we need a ImapMessage and were given
- * something else we can try to do a pre-fetch first.
- * TODO Collect ALERT messages and show them to users.
- *
- * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for
- * certain information in a FETCH command, the server may return the requested
- * information in any order, not necessarily in the order that it was requested.
- * Further, the server may return the information in separate FETCH responses
- * and may also return information that was not explicitly requested (to reflect
- * to the client changes in the state of the subject message).
- * </pre>
- */
-public class ImapStore extends Store {
- /** Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. */
- private static final Charset MODIFIED_UTF_7_CHARSET =
- new CharsetProvider().charsetForName("X-RFC-3501");
-
- @VisibleForTesting static String sImapId = null;
- @VisibleForTesting String mPathPrefix;
- @VisibleForTesting String mPathSeparator;
-
- private boolean mUseOAuth;
-
- private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool =
- new ConcurrentLinkedQueue<ImapConnection>();
-
- /**
- * Static named constructor.
- */
- public static Store newInstance(Account account, Context context) throws MessagingException {
- return new ImapStore(context, account);
- }
-
- /**
- * Creates a new store for the given account. Always use
- * {@link #newInstance(Account, Context)} to create an IMAP store.
- */
- private ImapStore(Context context, Account account) throws MessagingException {
- mContext = context;
- mAccount = account;
-
- HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
- if (recvAuth == null) {
- throw new MessagingException("No HostAuth in ImapStore?");
- }
- mTransport = new MailTransport(context, "IMAP", recvAuth);
-
- String[] userInfo = recvAuth.getLogin();
- mUsername = userInfo[0];
- mPassword = userInfo[1];
- final Credential cred = recvAuth.getCredential(context);
- mUseOAuth = (cred != null);
- mPathPrefix = recvAuth.mDomain;
- }
-
- boolean getUseOAuth() {
- return mUseOAuth;
- }
-
- String getUsername() {
- return mUsername;
- }
-
- String getPassword() {
- return mPassword;
- }
-
- @VisibleForTesting
- Collection<ImapConnection> getConnectionPoolForTest() {
- return mConnectionPool;
- }
-
- /**
- * For testing only. Injects a different root transport (it will be copied using
- * newInstanceWithConfiguration() each time IMAP sets up a new channel). The transport
- * should already be set up and ready to use. Do not use for real code.
- * @param testTransport The Transport to inject and use for all future communication.
- */
- @VisibleForTesting
- void setTransportForTest(MailTransport testTransport) {
- mTransport = testTransport;
- }
-
- /**
- * Return, or create and return, an string suitable for use in an IMAP ID message.
- * This is constructed similarly to the way the browser sets up its user-agent strings.
- * See RFC 2971 for more details. The output of this command will be a series of key-value
- * pairs delimited by spaces (there is no point in returning a structured result because
- * this will be sent as-is to the IMAP server). No tokens, parenthesis or "ID" are included,
- * because some connections may append additional values.
- *
- * The following IMAP ID keys may be included:
- * name Android package name of the program
- * os "android"
- * os-version "version; model; build-id"
- * vendor Vendor of the client/server
- * x-android-device-model Model (only revealed if release build)
- * x-android-net-operator Mobile network operator (if known)
- * AGUID A device+account UID
- *
- * In addition, a vendor policy .apk can append key/value pairs.
- *
- * @param userName the username of the account
- * @param host the host (server) of the account
- * @param capabilities a list of the capabilities from the server
- * @return a String for use in an IMAP ID message.
- */
- public static String getImapId(Context context, String userName, String host,
- String capabilities) {
- // The first section is global to all IMAP connections, and generates the fixed
- // values in any IMAP ID message
- synchronized (ImapStore.class) {
- if (sImapId == null) {
- TelephonyManager tm =
- (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- String networkOperator = tm.getNetworkOperatorName();
- if (networkOperator == null) networkOperator = "";
-
- sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE,
- Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER,
- networkOperator);
- }
- }
-
- // This section is per Store, and adds in a dynamic elements like UID's.
- // We don't cache the result of this work, because the caller does anyway.
- StringBuilder id = new StringBuilder(sImapId);
-
- // Optionally add any vendor-supplied id keys
- String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host,
- capabilities);
- if (vendorId != null) {
- id.append(' ');
- id.append(vendorId);
- }
-
- // Generate a UID that mixes a "stable" device UID with the email address
- try {
- String devUID = Preferences.getPreferences(context).getDeviceUID();
- MessageDigest messageDigest;
- messageDigest = MessageDigest.getInstance("SHA-1");
- messageDigest.update(userName.getBytes());
- messageDigest.update(devUID.getBytes());
- byte[] uid = messageDigest.digest();
- String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP);
- id.append(" \"AGUID\" \"");
- id.append(hexUid);
- id.append('\"');
- } catch (NoSuchAlgorithmException e) {
- LogUtils.d(Logging.LOG_TAG, "couldn't obtain SHA-1 hash for device UID");
- }
- return id.toString();
- }
-
- /**
- * Helper function that actually builds the static part of the IMAP ID string. This is
- * separated from getImapId for testability. There is no escaping or encoding in IMAP ID so
- * any rogue chars must be filtered here.
- *
- * @param packageName context.getPackageName()
- * @param version Build.VERSION.RELEASE
- * @param codeName Build.VERSION.CODENAME
- * @param model Build.MODEL
- * @param id Build.ID
- * @param vendor Build.MANUFACTURER
- * @param networkOperator TelephonyManager.getNetworkOperatorName()
- * @return the static (never changes) portion of the IMAP ID
- */
- @VisibleForTesting
- static String makeCommonImapId(String packageName, String version,
- String codeName, String model, String id, String vendor, String networkOperator) {
-
- // Before building up IMAP ID string, pre-filter the input strings for "legal" chars
- // This is using a fairly arbitrary char set intended to pass through most reasonable
- // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space>
- // The most important thing is *not* to pass parens, quotes, or CRLF, which would break
- // the format of the IMAP ID list.
- Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]");
- packageName = p.matcher(packageName).replaceAll("");
- version = p.matcher(version).replaceAll("");
- codeName = p.matcher(codeName).replaceAll("");
- model = p.matcher(model).replaceAll("");
- id = p.matcher(id).replaceAll("");
- vendor = p.matcher(vendor).replaceAll("");
- networkOperator = p.matcher(networkOperator).replaceAll("");
-
- // "name" "com.android.email"
- StringBuilder sb = new StringBuilder("\"name\" \"");
- sb.append(packageName);
- sb.append("\"");
-
- // "os" "android"
- sb.append(" \"os\" \"android\"");
-
- // "os-version" "version; build-id"
- sb.append(" \"os-version\" \"");
- if (version.length() > 0) {
- sb.append(version);
- } else {
- // default to "1.0"
- sb.append("1.0");
- }
- // add the build ID or build #
- if (id.length() > 0) {
- sb.append("; ");
- sb.append(id);
- }
- sb.append("\"");
-
- // "vendor" "the vendor"
- if (vendor.length() > 0) {
- sb.append(" \"vendor\" \"");
- sb.append(vendor);
- sb.append("\"");
- }
-
- // "x-android-device-model" the device model (on release builds only)
- if ("REL".equals(codeName)) {
- if (model.length() > 0) {
- sb.append(" \"x-android-device-model\" \"");
- sb.append(model);
- sb.append("\"");
- }
- }
-
- // "x-android-mobile-net-operator" "name of network operator"
- if (networkOperator.length() > 0) {
- sb.append(" \"x-android-mobile-net-operator\" \"");
- sb.append(networkOperator);
- sb.append("\"");
- }
-
- return sb.toString();
- }
-
-
- @Override
- public Folder getFolder(String name) {
- return new ImapFolder(this, name);
- }
-
- /**
- * Creates a mailbox hierarchy out of the flat data provided by the server.
- */
- @VisibleForTesting
- static void createHierarchy(HashMap<String, ImapFolder> mailboxes) {
- Set<String> pathnames = mailboxes.keySet();
- for (String path : pathnames) {
- final ImapFolder folder = mailboxes.get(path);
- final Mailbox mailbox = folder.mMailbox;
- int delimiterIdx = mailbox.mServerId.lastIndexOf(mailbox.mDelimiter);
- long parentKey = Mailbox.NO_MAILBOX;
- String parentPath = null;
- if (delimiterIdx != -1) {
- parentPath = path.substring(0, delimiterIdx);
- final ImapFolder parentFolder = mailboxes.get(parentPath);
- final Mailbox parentMailbox = (parentFolder == null) ? null : parentFolder.mMailbox;
- if (parentMailbox != null) {
- parentKey = parentMailbox.mId;
- parentMailbox.mFlags
- |= (Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE);
- }
- }
- mailbox.mParentKey = parentKey;
- mailbox.mParentServerId = parentPath;
- }
- }
-
- /**
- * Creates a {@link Folder} and associated {@link Mailbox}. If the folder does not already
- * exist in the local database, a new row will immediately be created in the mailbox table.
- * Otherwise, the existing row will be used. Any changes to existing rows, will not be stored
- * to the database immediately.
- * @param accountId The ID of the account the mailbox is to be associated with
- * @param mailboxPath The path of the mailbox to add
- * @param delimiter A path delimiter. May be {@code null} if there is no delimiter.
- * @param selectable If {@code true}, the mailbox can be selected and used to store messages.
- * @param mailbox If not null, mailbox is used instead of querying for the Mailbox.
- */
- private ImapFolder addMailbox(Context context, long accountId, String mailboxPath,
- char delimiter, boolean selectable, Mailbox mailbox) {
- // TODO: pass in the mailbox type, or do a proper lookup here
- final int mailboxType;
- if (mailbox == null) {
- mailboxType = LegacyConversions.inferMailboxTypeFromName(context, mailboxPath);
- mailbox = Mailbox.getMailboxForPath(context, accountId, mailboxPath);
- } else {
- mailboxType = mailbox.mType;
- }
- final ImapFolder folder = (ImapFolder) getFolder(mailboxPath);
- if (mailbox.isSaved()) {
- // existing mailbox
- // mailbox retrieved from database; save hash _before_ updating fields
- folder.mHash = mailbox.getHashes();
- }
- updateMailbox(mailbox, accountId, mailboxPath, delimiter, selectable, mailboxType);
- if (folder.mHash == null) {
- // new mailbox
- // save hash after updating. allows tracking changes if the mailbox is saved
- // outside of #saveMailboxList()
- folder.mHash = mailbox.getHashes();
- // We must save this here to make sure we have a valid ID for later
- mailbox.save(mContext);
- }
- folder.mMailbox = mailbox;
- return folder;
- }
-
- /**
- * Persists the folders in the given list.
- */
- private static void saveMailboxList(Context context, HashMap<String, ImapFolder> folderMap) {
- for (ImapFolder imapFolder : folderMap.values()) {
- imapFolder.save(context);
- }
- }
-
- @Override
- public Folder[] updateFolders() throws MessagingException {
- // TODO: There is nothing that ever closes this connection. Trouble is, it's not exactly
- // clear when we should close it, we'd like to keep it open until we're really done
- // using it.
- ImapConnection connection = getConnection();
- try {
- final HashMap<String, ImapFolder> mailboxes = new HashMap<String, ImapFolder>();
- // Establish a connection to the IMAP server; if necessary
- // This ensures a valid prefix if the prefix is automatically set by the server
- connection.executeSimpleCommand(ImapConstants.NOOP);
- String imapCommand = ImapConstants.LIST + " \"\" \"*\"";
- if (mPathPrefix != null) {
- imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\"";
- }
- List<ImapResponse> responses = connection.executeSimpleCommand(imapCommand);
- for (ImapResponse response : responses) {
- // S: * LIST (\Noselect) "/" ~/Mail/foo
- if (response.isDataResponse(0, ImapConstants.LIST)) {
- // Get folder name.
- ImapString encodedFolder = response.getStringOrEmpty(3);
- if (encodedFolder.isEmpty()) continue;
-
- String folderName = decodeFolderName(encodedFolder.getString(), mPathPrefix);
-
- if (ImapConstants.INBOX.equalsIgnoreCase(folderName)) continue;
-
- // Parse attributes.
- boolean selectable =
- !response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT);
- String delimiter = response.getStringOrEmpty(2).getString();
- char delimiterChar = '\0';
- if (!TextUtils.isEmpty(delimiter)) {
- delimiterChar = delimiter.charAt(0);
- }
- ImapFolder folder = addMailbox(
- mContext, mAccount.mId, folderName, delimiterChar, selectable, null);
- mailboxes.put(folderName, folder);
- }
- }
-
- // In order to properly map INBOX -> Inbox, handle it as a special case.
- final Mailbox inbox =
- Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
- final ImapFolder newFolder = addMailbox(
- mContext, mAccount.mId, inbox.mServerId, '\0', true /*selectable*/, inbox);
- mailboxes.put(ImapConstants.INBOX, newFolder);
-
- createHierarchy(mailboxes);
- saveMailboxList(mContext, mailboxes);
- return mailboxes.values().toArray(new Folder[mailboxes.size()]);
- } catch (IOException ioe) {
- connection.close();
- throw new MessagingException("Unable to get folder list", ioe);
- } catch (AuthenticationFailedException afe) {
- // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT
- // commands to the server
- connection.destroyResponses();
- connection = null;
- throw afe;
- } finally {
- if (connection != null) {
- // We keep our connection out of the pool as long as we are using it, then
- // put it back into the pool so it can be reused.
- poolConnection(connection);
- }
- }
- }
-
- @Override
- public Bundle checkSettings() throws MessagingException {
- int result = MessagingException.NO_ERROR;
- Bundle bundle = new Bundle();
- // TODO: why doesn't this use getConnection()? I guess this is only done during setup,
- // so there's need to look for a pooled connection?
- // But then why doesn't it use poolConnection() after it's done?
- ImapConnection connection = new ImapConnection(this);
- try {
- connection.open();
- connection.close();
- } catch (IOException ioe) {
- bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage());
- result = MessagingException.IOERROR;
- } finally {
- connection.destroyResponses();
- }
- bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
- return bundle;
- }
-
- /**
- * Returns whether or not the prefix has been set by the user. This can be determined by
- * the fact that the prefix is set, but, the path separator is not set.
- */
- boolean isUserPrefixSet() {
- return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix);
- }
-
- /** Sets the path separator */
- void setPathSeparator(String pathSeparator) {
- mPathSeparator = pathSeparator;
- }
-
- /** Sets the prefix */
- void setPathPrefix(String pathPrefix) {
- mPathPrefix = pathPrefix;
- }
-
- /** Gets the context for this store */
- Context getContext() {
- return mContext;
- }
-
- /** Returns a clone of the transport associated with this store. */
- MailTransport cloneTransport() {
- return mTransport.clone();
- }
-
- /**
- * Fixes the path prefix, if necessary. The path prefix must always end with the
- * path separator.
- */
- void ensurePrefixIsValid() {
- // Make sure the path prefix ends with the path separator
- if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) {
- if (!mPathPrefix.endsWith(mPathSeparator)) {
- mPathPrefix = mPathPrefix + mPathSeparator;
- }
- }
- }
-
- /**
- * Gets a connection if one is available from the pool, or creates a new one if not.
- */
- ImapConnection getConnection() {
- // TODO Why would we ever have (or need to have) more than one active connection?
- // TODO We set new username/password each time, but we don't actually close the transport
- // when we do this. So if that information has changed, this connection will fail.
- ImapConnection connection;
- while ((connection = mConnectionPool.poll()) != null) {
- try {
- connection.setStore(this);
- connection.executeSimpleCommand(ImapConstants.NOOP);
- break;
- } catch (MessagingException e) {
- // Fall through
- } catch (IOException e) {
- // Fall through
- }
- connection.close();
- }
-
- if (connection == null) {
- connection = new ImapConnection(this);
- }
- return connection;
- }
-
- /**
- * Save a {@link ImapConnection} in the pool for reuse. Any responses associated with the
- * connection are destroyed before adding the connection to the pool.
- */
- void poolConnection(ImapConnection connection) {
- if (connection != null) {
- connection.destroyResponses();
- mConnectionPool.add(connection);
- }
- }
-
- /**
- * Prepends the folder name with the given prefix and UTF-7 encodes it.
- */
- static String encodeFolderName(String name, String prefix) {
- // do NOT add the prefix to the special name "INBOX"
- if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name;
-
- // Prepend prefix
- if (prefix != null) {
- name = prefix + name;
- }
-
- // TODO bypass the conversion if name doesn't have special char.
- ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name);
- byte[] b = new byte[bb.limit()];
- bb.get(b);
-
- return Utility.fromAscii(b);
- }
-
- /**
- * UTF-7 decodes the folder name and removes the given path prefix.
- */
- static String decodeFolderName(String name, String prefix) {
- // TODO bypass the conversion if name doesn't have special char.
- String folder;
- folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString();
- if ((prefix != null) && folder.startsWith(prefix)) {
- folder = folder.substring(prefix.length());
- }
- return folder;
- }
-
- /**
- * Returns UIDs of Messages joined with "," as the separator.
- */
- static String joinMessageUids(Message[] messages) {
- StringBuilder sb = new StringBuilder();
- boolean notFirst = false;
- for (Message m : messages) {
- if (notFirst) {
- sb.append(',');
- }
- sb.append(m.getUid());
- notFirst = true;
- }
- return sb.toString();
- }
-
- static class ImapMessage extends MimeMessage {
- ImapMessage(String uid, ImapFolder folder) {
- mUid = uid;
- mFolder = folder;
- }
-
- public void setSize(int size) {
- mSize = size;
- }
-
- @Override
- public void parse(InputStream in) throws IOException, MessagingException {
- super.parse(in);
- }
-
- public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
- super.setFlag(flag, set);
- }
-
- @Override
- public void setFlag(Flag flag, boolean set) throws MessagingException {
- super.setFlag(flag, set);
- mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
- }
- }
-
- static class ImapException extends MessagingException {
- private static final long serialVersionUID = 1L;
-
- private final String mAlertText;
- private final String mResponseCode;
-
- public ImapException(String message, String alertText, String responseCode) {
- super(message);
- mAlertText = alertText;
- mResponseCode = responseCode;
- }
-
- public String getAlertText() {
- return mAlertText;
- }
-
- public String getResponseCode() {
- return mResponseCode;
- }
- }
-
- public void closeConnections() {
- ImapConnection connection;
- while ((connection = mConnectionPool.poll()) != null) {
- connection.close();
- }
- }
-}
diff --git a/src/com/android/email/mail/store/Pop3Store.java b/src/com/android/email/mail/store/Pop3Store.java
deleted file mode 100644
index 4ea75ccf3..000000000
--- a/src/com/android/email/mail/store/Pop3Store.java
+++ /dev/null
@@ -1,833 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store;
-
-import android.content.Context;
-import android.os.Bundle;
-
-import com.android.email.DebugUtils;
-import com.android.email.mail.Store;
-import com.android.email.mail.transport.MailTransport;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.internet.MimeMessage;
-import com.android.emailcommon.mail.AuthenticationFailedException;
-import com.android.emailcommon.mail.FetchProfile;
-import com.android.emailcommon.mail.Flag;
-import com.android.emailcommon.mail.Folder;
-import com.android.emailcommon.mail.Folder.OpenMode;
-import com.android.emailcommon.mail.Message;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.service.EmailServiceProxy;
-import com.android.emailcommon.service.SearchParams;
-import com.android.emailcommon.utility.LoggingInputStream;
-import com.android.emailcommon.utility.Utility;
-import com.android.mail.utils.LogUtils;
-import com.google.common.annotations.VisibleForTesting;
-
-import org.apache.james.mime4j.EOLConvertingInputStream;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Locale;
-
-public class Pop3Store extends Store {
- // All flags defining debug or development code settings must be FALSE
- // when code is checked in or released.
- private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false;
- private static boolean DEBUG_LOG_RAW_STREAM = false;
-
- private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
- /** The name of the only mailbox available to POP3 accounts */
- private static final String POP3_MAILBOX_NAME = "INBOX";
- private final HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
- private final Message[] mOneMessage = new Message[1];
-
- /**
- * Static named constructor.
- */
- public static Store newInstance(Account account, Context context) throws MessagingException {
- return new Pop3Store(context, account);
- }
-
- /**
- * Creates a new store for the given account.
- */
- private Pop3Store(Context context, Account account) throws MessagingException {
- mContext = context;
- mAccount = account;
-
- HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
- mTransport = new MailTransport(context, "POP3", recvAuth);
- String[] userInfoParts = recvAuth.getLogin();
- mUsername = userInfoParts[0];
- mPassword = userInfoParts[1];
- }
-
- /**
- * For testing only. Injects a different transport. The transport should already be set
- * up and ready to use. Do not use for real code.
- * @param testTransport The Transport to inject and use for all future communication.
- */
- /* package */ void setTransport(MailTransport testTransport) {
- mTransport = testTransport;
- }
-
- @Override
- public Folder getFolder(String name) {
- Folder folder = mFolders.get(name);
- if (folder == null) {
- folder = new Pop3Folder(name);
- mFolders.put(folder.getName(), folder);
- }
- return folder;
- }
-
- @Override
- public Folder[] updateFolders() {
- Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
- if (mailbox == null) {
- mailbox = Mailbox.newSystemMailbox(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
- }
- if (mailbox.isSaved()) {
- mailbox.update(mContext, mailbox.toContentValues());
- } else {
- mailbox.save(mContext);
- }
- return new Folder[] { getFolder(mailbox.mServerId) };
- }
-
- /**
- * Used by account setup to test if an account's settings are appropriate. The definition
- * of "checked" here is simply, can you log into the account and does it meet some minimum set
- * of feature requirements?
- *
- * @throws MessagingException if there was some problem with the account
- */
- @Override
- public Bundle checkSettings() throws MessagingException {
- Pop3Folder folder = new Pop3Folder(POP3_MAILBOX_NAME);
- Bundle bundle = null;
- // Close any open or half-open connections - checkSettings should always be "fresh"
- if (mTransport.isOpen()) {
- folder.close(false);
- }
- try {
- folder.open(OpenMode.READ_WRITE);
- bundle = folder.checkSettings();
- } finally {
- folder.close(false); // false == don't expunge anything
- }
- return bundle;
- }
-
- public class Pop3Folder extends Folder {
- private final HashMap<String, Pop3Message> mUidToMsgMap
- = new HashMap<String, Pop3Message>();
- private final HashMap<Integer, Pop3Message> mMsgNumToMsgMap
- = new HashMap<Integer, Pop3Message>();
- private final HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>();
- private final String mName;
- private int mMessageCount;
- private Pop3Capabilities mCapabilities;
-
- public Pop3Folder(String name) {
- if (name.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
- mName = POP3_MAILBOX_NAME;
- } else {
- mName = name;
- }
- }
-
- /**
- * Used by account setup to test if an account's settings are appropriate. Here, we run
- * an additional test to see if UIDL is supported on the server. If it's not we
- * can't service this account.
- *
- * @return Bundle containing validation data (code and, if appropriate, error message)
- * @throws MessagingException if the account is not going to be useable
- */
- public Bundle checkSettings() throws MessagingException {
- Bundle bundle = new Bundle();
- int result = MessagingException.NO_ERROR;
- try {
- UidlParser parser = new UidlParser();
- executeSimpleCommand("UIDL");
- // drain the entire output, so additional communications don't get confused.
- String response;
- while ((response = mTransport.readLine(false)) != null) {
- parser.parseMultiLine(response);
- if (parser.mEndOfMessage) {
- break;
- }
- }
- } catch (IOException ioe) {
- mTransport.close();
- result = MessagingException.IOERROR;
- bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE,
- ioe.getMessage());
- }
- bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
- return bundle;
- }
-
- @Override
- public synchronized void open(OpenMode mode) throws MessagingException {
- if (mTransport.isOpen()) {
- return;
- }
-
- if (!mName.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
- throw new MessagingException("Folder does not exist");
- }
-
- try {
- mTransport.open();
-
- // Eat the banner
- executeSimpleCommand(null);
-
- mCapabilities = getCapabilities();
-
- if (mTransport.canTryTlsSecurity()) {
- if (mCapabilities.stls) {
- executeSimpleCommand("STLS");
- mTransport.reopenTls();
- } else {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, "TLS not supported but required");
- }
- throw new MessagingException(MessagingException.TLS_REQUIRED);
- }
- }
-
- try {
- executeSensitiveCommand("USER " + mUsername, "USER /redacted/");
- executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/");
- } catch (MessagingException me) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, me.toString());
- }
- throw new AuthenticationFailedException(null, me);
- }
- } catch (IOException ioe) {
- mTransport.close();
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ioe.toString());
- }
- throw new MessagingException(MessagingException.IOERROR, ioe.toString());
- }
-
- Exception statException = null;
- try {
- String response = executeSimpleCommand("STAT");
- String[] parts = response.split(" ");
- if (parts.length < 2) {
- statException = new IOException();
- } else {
- mMessageCount = Integer.parseInt(parts[1]);
- }
- } catch (MessagingException me) {
- statException = me;
- } catch (IOException ioe) {
- statException = ioe;
- } catch (NumberFormatException nfe) {
- statException = nfe;
- }
- if (statException != null) {
- mTransport.close();
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, statException.toString());
- }
- throw new MessagingException("POP3 STAT", statException);
- }
- mUidToMsgMap.clear();
- mMsgNumToMsgMap.clear();
- mUidToMsgNumMap.clear();
- }
-
- @Override
- public OpenMode getMode() {
- return OpenMode.READ_WRITE;
- }
-
- /**
- * Close the folder (and the transport below it).
- *
- * MUST NOT return any exceptions.
- *
- * @param expunge If true all deleted messages will be expunged (TODO - not implemented)
- */
- @Override
- public void close(boolean expunge) {
- try {
- executeSimpleCommand("QUIT");
- }
- catch (Exception e) {
- // ignore any problems here - just continue closing
- }
- mTransport.close();
- }
-
- @Override
- public String getName() {
- return mName;
- }
-
- // POP3 does not folder creation
- @Override
- public boolean canCreate(FolderType type) {
- return false;
- }
-
- @Override
- public boolean create(FolderType type) {
- return false;
- }
-
- @Override
- public boolean exists() {
- return mName.equalsIgnoreCase(POP3_MAILBOX_NAME);
- }
-
- @Override
- public int getMessageCount() {
- return mMessageCount;
- }
-
- @Override
- public int getUnreadMessageCount() {
- return -1;
- }
-
- @Override
- public Message getMessage(String uid) throws MessagingException {
- if (mUidToMsgNumMap.size() == 0) {
- try {
- indexMsgNums(1, mMessageCount);
- } catch (IOException ioe) {
- mTransport.close();
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe);
- }
- throw new MessagingException("getMessages", ioe);
- }
- }
- Pop3Message message = mUidToMsgMap.get(uid);
- return message;
- }
-
- @Override
- public Pop3Message[] getMessages(int start, int end, MessageRetrievalListener listener)
- throws MessagingException {
- return null;
- }
-
- @Override
- public Pop3Message[] getMessages(long startDate, long endDate,
- MessageRetrievalListener listener) throws MessagingException {
- return null;
- }
-
- public Pop3Message[] getMessages(int end, final int limit)
- throws MessagingException {
- try {
- indexMsgNums(1, end);
- } catch (IOException ioe) {
- mTransport.close();
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ioe.toString());
- }
- throw new MessagingException("getMessages", ioe);
- }
- ArrayList<Message> messages = new ArrayList<Message>();
- for (int msgNum = end; msgNum > 0 && (messages.size() < limit); msgNum--) {
- Pop3Message message = mMsgNumToMsgMap.get(msgNum);
- if (message != null) {
- messages.add(message);
- }
- }
- return messages.toArray(new Pop3Message[messages.size()]);
- }
-
- /**
- * Ensures that the given message set (from start to end inclusive)
- * has been queried so that uids are available in the local cache.
- * @param start
- * @param end
- * @throws MessagingException
- * @throws IOException
- */
- private void indexMsgNums(int start, int end)
- throws MessagingException, IOException {
- if (!mMsgNumToMsgMap.isEmpty()) {
- return;
- }
- UidlParser parser = new UidlParser();
- if (DEBUG_FORCE_SINGLE_LINE_UIDL || (mMessageCount > 5000)) {
- /*
- * In extreme cases we'll do a UIDL command per message instead of a bulk
- * download.
- */
- for (int msgNum = start; msgNum <= end; msgNum++) {
- Pop3Message message = mMsgNumToMsgMap.get(msgNum);
- if (message == null) {
- String response = executeSimpleCommand("UIDL " + msgNum);
- if (!parser.parseSingleLine(response)) {
- throw new IOException();
- }
- message = new Pop3Message(parser.mUniqueId, this);
- indexMessage(msgNum, message);
- }
- }
- } else {
- String response = executeSimpleCommand("UIDL");
- while ((response = mTransport.readLine(false)) != null) {
- if (!parser.parseMultiLine(response)) {
- throw new IOException();
- }
- if (parser.mEndOfMessage) {
- break;
- }
- int msgNum = parser.mMessageNumber;
- if (msgNum >= start && msgNum <= end) {
- Pop3Message message = mMsgNumToMsgMap.get(msgNum);
- if (message == null) {
- message = new Pop3Message(parser.mUniqueId, this);
- indexMessage(msgNum, message);
- }
- }
- }
- }
- }
-
- /**
- * Simple parser class for UIDL messages.
- *
- * <p>NOTE: In variance with RFC 1939, we allow multiple whitespace between the
- * message-number and unique-id fields. This provides greater compatibility with some
- * non-compliant POP3 servers, e.g. mail.comcast.net.
- */
- /* package */ class UidlParser {
-
- /**
- * Caller can read back message-number from this field
- */
- public int mMessageNumber;
- /**
- * Caller can read back unique-id from this field
- */
- public String mUniqueId;
- /**
- * True if the response was "end-of-message"
- */
- public boolean mEndOfMessage;
- /**
- * True if an error was reported
- */
- public boolean mErr;
-
- /**
- * Construct & Initialize
- */
- public UidlParser() {
- mErr = true;
- }
-
- /**
- * Parse a single-line response. This is returned from a command of the form
- * "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or
- * "-ERR diagnostic text"
- *
- * @param response The string returned from the server
- * @return true if the string parsed as expected (e.g. no syntax problems)
- */
- public boolean parseSingleLine(String response) {
- mErr = false;
- if (response == null || response.length() == 0) {
- return false;
- }
- char first = response.charAt(0);
- if (first == '+') {
- String[] uidParts = response.split(" +");
- if (uidParts.length >= 3) {
- try {
- mMessageNumber = Integer.parseInt(uidParts[1]);
- } catch (NumberFormatException nfe) {
- return false;
- }
- mUniqueId = uidParts[2];
- mEndOfMessage = true;
- return true;
- }
- } else if (first == '-') {
- mErr = true;
- return true;
- }
- return false;
- }
-
- /**
- * Parse a multi-line response. This is returned from a command of the form
- * "UIDL" and will be formatted as: "." or "msg-num unique-id".
- *
- * @param response The string returned from the server
- * @return true if the string parsed as expected (e.g. no syntax problems)
- */
- public boolean parseMultiLine(String response) {
- mErr = false;
- if (response == null || response.length() == 0) {
- return false;
- }
- char first = response.charAt(0);
- if (first == '.') {
- mEndOfMessage = true;
- return true;
- } else {
- String[] uidParts = response.split(" +");
- if (uidParts.length >= 2) {
- try {
- mMessageNumber = Integer.parseInt(uidParts[0]);
- } catch (NumberFormatException nfe) {
- return false;
- }
- mUniqueId = uidParts[1];
- mEndOfMessage = false;
- return true;
- }
- }
- return false;
- }
- }
-
- private void indexMessage(int msgNum, Pop3Message message) {
- mMsgNumToMsgMap.put(msgNum, message);
- mUidToMsgMap.put(message.getUid(), message);
- mUidToMsgNumMap.put(message.getUid(), msgNum);
- }
-
- @Override
- public Message[] getMessages(String[] uids, MessageRetrievalListener listener) {
- throw new UnsupportedOperationException(
- "Pop3Folder.getMessage(MessageRetrievalListener)");
- }
-
- /**
- * Fetch the items contained in the FetchProfile into the given set of
- * Messages in as efficient a manner as possible.
- * @param messages
- * @param fp
- * @throws MessagingException
- */
- @Override
- public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
- throws MessagingException {
- throw new UnsupportedOperationException(
- "Pop3Folder.fetch(Message[], FetchProfile, MessageRetrievalListener)");
- }
-
- /**
- * Fetches the body of the given message, limiting the stored data
- * to the specified number of lines. If lines is -1 the entire message
- * is fetched. This is implemented with RETR for lines = -1 or TOP
- * for any other value. If the server does not support TOP it is
- * emulated with RETR and extra lines are thrown away.
- *
- * @param message
- * @param lines
- * @param callback optional callback that reports progress of the fetch
- */
- public void fetchBody(Pop3Message message, int lines,
- EOLConvertingInputStream.Callback callback) throws IOException, MessagingException {
- String response = null;
- int messageId = mUidToMsgNumMap.get(message.getUid());
- if (lines == -1) {
- // Fetch entire message
- response = executeSimpleCommand(String.format(Locale.US, "RETR %d", messageId));
- } else {
- // Fetch partial message. Try "TOP", and fall back to slower "RETR" if necessary
- try {
- response = executeSimpleCommand(
- String.format(Locale.US, "TOP %d %d", messageId, lines));
- } catch (MessagingException me) {
- try {
- response = executeSimpleCommand(
- String.format(Locale.US, "RETR %d", messageId));
- } catch (MessagingException e) {
- LogUtils.w(Logging.LOG_TAG, "Can't read message " + messageId);
- }
- }
- }
- if (response != null) {
- try {
- int ok = response.indexOf("OK");
- if (ok > 0) {
- try {
- int start = ok + 3;
- if (start > response.length()) {
- // No length was supplied, this is a protocol error.
- LogUtils.e(Logging.LOG_TAG, "No body length supplied");
- message.setSize(0);
- } else {
- int end = response.indexOf(" ", start);
- final String intString;
- if (end > 0) {
- intString = response.substring(start, end);
- } else {
- intString = response.substring(start);
- }
- message.setSize(Integer.parseInt(intString));
- }
- } catch (NumberFormatException e) {
- // We tried
- }
- }
- InputStream in = mTransport.getInputStream();
- if (DEBUG_LOG_RAW_STREAM && DebugUtils.DEBUG) {
- in = new LoggingInputStream(in);
- }
- message.parse(new Pop3ResponseInputStream(in), callback);
- }
- catch (MessagingException me) {
- /*
- * If we're only downloading headers it's possible
- * we'll get a broken MIME message which we're not
- * real worried about. If we've downloaded the body
- * and can't parse it we need to let the user know.
- */
- if (lines == -1) {
- throw me;
- }
- }
- }
- }
-
- @Override
- public Flag[] getPermanentFlags() {
- return PERMANENT_FLAGS;
- }
-
- @Override
- public void appendMessage(Context context, Message message, boolean noTimeout) {
- }
-
- @Override
- public void delete(boolean recurse) {
- }
-
- @Override
- public Message[] expunge() {
- return null;
- }
-
- public void deleteMessage(Message message) throws MessagingException {
- mOneMessage[0] = message;
- setFlags(mOneMessage, PERMANENT_FLAGS, true);
- }
-
- @Override
- public void setFlags(Message[] messages, Flag[] flags, boolean value)
- throws MessagingException {
- if (!value || !Utility.arrayContains(flags, Flag.DELETED)) {
- /*
- * The only flagging we support is setting the Deleted flag.
- */
- return;
- }
- try {
- for (Message message : messages) {
- try {
- String uid = message.getUid();
- int msgNum = mUidToMsgNumMap.get(uid);
- executeSimpleCommand(String.format(Locale.US, "DELE %s", msgNum));
- // Remove from the maps
- mMsgNumToMsgMap.remove(msgNum);
- mUidToMsgNumMap.remove(uid);
- } catch (MessagingException e) {
- // A failed deletion isn't a problem
- }
- }
- }
- catch (IOException ioe) {
- mTransport.close();
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ioe.toString());
- }
- throw new MessagingException("setFlags()", ioe);
- }
- }
-
- @Override
- public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) {
- throw new UnsupportedOperationException("copyMessages is not supported in POP3");
- }
-
- private Pop3Capabilities getCapabilities() throws IOException {
- Pop3Capabilities capabilities = new Pop3Capabilities();
- try {
- String response = executeSimpleCommand("CAPA");
- while ((response = mTransport.readLine(true)) != null) {
- if (response.equals(".")) {
- break;
- } else if (response.equalsIgnoreCase("STLS")){
- capabilities.stls = true;
- }
- }
- }
- catch (MessagingException me) {
- /*
- * The server may not support the CAPA command, so we just eat this Exception
- * and allow the empty capabilities object to be returned.
- */
- }
- return capabilities;
- }
-
- /**
- * Send a single command and wait for a single line response. Reopens the connection,
- * if it is closed. Leaves the connection open.
- *
- * @param command The command string to send to the server.
- * @return Returns the response string from the server.
- */
- private String executeSimpleCommand(String command) throws IOException, MessagingException {
- return executeSensitiveCommand(command, null);
- }
-
- /**
- * Send a single command and wait for a single line response. Reopens the connection,
- * if it is closed. Leaves the connection open.
- *
- * @param command The command string to send to the server.
- * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
- * please pass a replacement string here (for logging).
- * @return Returns the response string from the server.
- */
- private String executeSensitiveCommand(String command, String sensitiveReplacement)
- throws IOException, MessagingException {
- open(OpenMode.READ_WRITE);
-
- if (command != null) {
- mTransport.writeLine(command, sensitiveReplacement);
- }
-
- String response = mTransport.readLine(true);
-
- if (response.length() > 1 && response.charAt(0) == '-') {
- throw new MessagingException(response);
- }
-
- return response;
- }
-
- @Override
- public boolean equals(Object o) {
- if (o instanceof Pop3Folder) {
- return ((Pop3Folder) o).mName.equals(mName);
- }
- return super.equals(o);
- }
-
- @Override
- @VisibleForTesting
- public boolean isOpen() {
- return mTransport.isOpen();
- }
-
- @Override
- public Message createMessage(String uid) {
- return new Pop3Message(uid, this);
- }
-
- @Override
- public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) {
- return null;
- }
- }
-
- public static class Pop3Message extends MimeMessage {
- public Pop3Message(String uid, Pop3Folder folder) {
- mUid = uid;
- mFolder = folder;
- mSize = -1;
- }
-
- public void setSize(int size) {
- mSize = size;
- }
-
- @Override
- public void parse(InputStream in) throws IOException, MessagingException {
- super.parse(in);
- }
-
- @Override
- public void setFlag(Flag flag, boolean set) throws MessagingException {
- super.setFlag(flag, set);
- mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
- }
- }
-
- /**
- * POP3 Capabilities as defined in RFC 2449. This is not a complete list of CAPA
- * responses - just those that we use in this client.
- */
- class Pop3Capabilities {
- /** The STLS (start TLS) command is supported */
- public boolean stls;
-
- @Override
- public String toString() {
- return String.format("STLS %b", stls);
- }
- }
-
- // TODO figure out what is special about this and merge it into MailTransport
- class Pop3ResponseInputStream extends InputStream {
- private final InputStream mIn;
- private boolean mStartOfLine = true;
- private boolean mFinished;
-
- public Pop3ResponseInputStream(InputStream in) {
- mIn = in;
- }
-
- @Override
- public int read() throws IOException {
- if (mFinished) {
- return -1;
- }
- int d = mIn.read();
- if (mStartOfLine && d == '.') {
- d = mIn.read();
- if (d == '\r') {
- mFinished = true;
- mIn.read();
- return -1;
- }
- }
-
- mStartOfLine = (d == '\n');
-
- return d;
- }
- }
-}
diff --git a/src/com/android/email/mail/store/ServiceStore.java b/src/com/android/email/mail/store/ServiceStore.java
deleted file mode 100644
index ae568f516..000000000
--- a/src/com/android/email/mail/store/ServiceStore.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.os.RemoteException;
-
-import com.android.email.mail.Store;
-import com.android.email.service.EmailServiceUtils;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.emailcommon.service.EmailServiceProxy;
-import com.android.emailcommon.service.HostAuthCompat;
-import com.android.emailcommon.service.IEmailService;
-
-/**
- * Base class for service-based stores
- */
-public class ServiceStore extends Store {
- protected final HostAuth mHostAuth;
-
- /**
- * Creates a new store for the given account.
- */
- public ServiceStore(Account account, Context context) throws MessagingException {
- mContext = context;
- mHostAuth = account.getOrCreateHostAuthRecv(mContext);
- }
-
- /**
- * Static named constructor.
- */
- public static Store newInstance(Account account, Context context) throws MessagingException {
- return new ServiceStore(account, context);
- }
-
- private IEmailService getService() {
- return EmailServiceUtils.getService(mContext, mHostAuth.mProtocol);
- }
-
- @Override
- public Bundle checkSettings() throws MessagingException {
- /**
- * Here's where we check the settings
- * @throws MessagingException if we can't authenticate the account
- */
- try {
- IEmailService svc = getService();
- // Use a longer timeout for the validate command. Note that the instanceof check
- // shouldn't be necessary; we'll do it anyway, just to be safe
- if (svc instanceof EmailServiceProxy) {
- ((EmailServiceProxy)svc).setTimeout(90);
- }
- HostAuthCompat hostAuthCom = new HostAuthCompat(mHostAuth);
- return svc.validate(hostAuthCom);
- } catch (RemoteException e) {
- throw new MessagingException("Call to validate generated an exception", e);
- }
- }
-
- /**
- * We handle AutoDiscover here, wrapping the EmailService call. The service call returns a
- * HostAuth and we return null if there was a service issue
- */
- @Override
- public Bundle autoDiscover(Context context, String username, String password) {
- try {
- return getService().autoDiscover(username, password);
- } catch (RemoteException e) {
- return null;
- }
- }
-}
diff --git a/src/com/android/email/mail/store/imap/ImapConstants.java b/src/com/android/email/mail/store/imap/ImapConstants.java
deleted file mode 100644
index 9f4d59290..000000000
--- a/src/com/android/email/mail/store/imap/ImapConstants.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store.imap;
-
-import com.android.email.mail.Store;
-
-import java.util.Locale;
-
-public final class ImapConstants {
- private ImapConstants() {}
-
- public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK";
- public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]";
- public static final String FETCH_FIELD_BODY_PEEK_SANE
- = String.format(Locale.US, "BODY.PEEK[]<0.%d>", Store.FETCH_BODY_SANE_SUGGESTED_SIZE);
- public static final String FETCH_FIELD_HEADERS =
- "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]";
-
- public static final String ALERT = "ALERT";
- public static final String APPEND = "APPEND";
- public static final String AUTHENTICATE = "AUTHENTICATE";
- public static final String BAD = "BAD";
- public static final String BADCHARSET = "BADCHARSET";
- public static final String BODY = "BODY";
- public static final String BODY_BRACKET_HEADER = "BODY[HEADER";
- public static final String BODYSTRUCTURE = "BODYSTRUCTURE";
- public static final String BYE = "BYE";
- public static final String CAPABILITY = "CAPABILITY";
- public static final String CHECK = "CHECK";
- public static final String CLOSE = "CLOSE";
- public static final String COPY = "COPY";
- public static final String COPYUID = "COPYUID";
- public static final String CREATE = "CREATE";
- public static final String DELETE = "DELETE";
- public static final String EXAMINE = "EXAMINE";
- public static final String EXISTS = "EXISTS";
- public static final String EXPUNGE = "EXPUNGE";
- public static final String FETCH = "FETCH";
- public static final String FLAG_ANSWERED = "\\ANSWERED";
- public static final String FLAG_DELETED = "\\DELETED";
- public static final String FLAG_FLAGGED = "\\FLAGGED";
- public static final String FLAG_NO_SELECT = "\\NOSELECT";
- public static final String FLAG_SEEN = "\\SEEN";
- public static final String FLAGS = "FLAGS";
- public static final String FLAGS_SILENT = "FLAGS.SILENT";
- public static final String ID = "ID";
- public static final String INBOX = "INBOX";
- public static final String INTERNALDATE = "INTERNALDATE";
- public static final String LIST = "LIST";
- public static final String LOGIN = "LOGIN";
- public static final String LOGOUT = "LOGOUT";
- public static final String LSUB = "LSUB";
- public static final String NAMESPACE = "NAMESPACE";
- public static final String NO = "NO";
- public static final String NOOP = "NOOP";
- public static final String OK = "OK";
- public static final String PARSE = "PARSE";
- public static final String PERMANENTFLAGS = "PERMANENTFLAGS";
- public static final String PREAUTH = "PREAUTH";
- public static final String READ_ONLY = "READ-ONLY";
- public static final String READ_WRITE = "READ-WRITE";
- public static final String RENAME = "RENAME";
- public static final String RFC822_SIZE = "RFC822.SIZE";
- public static final String SEARCH = "SEARCH";
- public static final String SELECT = "SELECT";
- public static final String STARTTLS = "STARTTLS";
- public static final String STATUS = "STATUS";
- public static final String STORE = "STORE";
- public static final String SUBSCRIBE = "SUBSCRIBE";
- public static final String TEXT = "TEXT";
- public static final String TRYCREATE = "TRYCREATE";
- public static final String UID = "UID";
- public static final String UID_COPY = "UID COPY";
- public static final String UID_FETCH = "UID FETCH";
- public static final String UID_SEARCH = "UID SEARCH";
- public static final String UID_STORE = "UID STORE";
- public static final String UIDNEXT = "UIDNEXT";
- public static final String UIDPLUS = "UIDPLUS";
- public static final String UIDVALIDITY = "UIDVALIDITY";
- public static final String UNSEEN = "UNSEEN";
- public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
- public static final String XOAUTH2 = "XOAUTH2";
- public static final String APPENDUID = "APPENDUID";
- public static final String NIL = "NIL";
-
- /** response codes within IMAP responses */
- public static final String EXPIRED = "EXPIRED";
- public static final String AUTHENTICATIONFAILED = "AUTHENTICATIONFAILED";
- public static final String UNAVAILABLE = "UNAVAILABLE";
-}
diff --git a/src/com/android/email/mail/store/imap/ImapElement.java b/src/com/android/email/mail/store/imap/ImapElement.java
deleted file mode 100644
index 80bb6cd99..000000000
--- a/src/com/android/email/mail/store/imap/ImapElement.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store.imap;
-
-/**
- * Class representing "element"s in IMAP responses.
- *
- * <p>Class hierarchy:
- * <pre>
- * ImapElement
- * |
- * |-- ImapElement.NONE (for 'index out of range')
- * |
- * |-- ImapList (isList() == true)
- * | |
- * | |-- ImapList.EMPTY
- * | |
- * | --- ImapResponse
- * |
- * --- ImapString (isString() == true)
- * |
- * |-- ImapString.EMPTY
- * |
- * |-- ImapSimpleString
- * |
- * |-- ImapMemoryLiteral
- * |
- * --- ImapTempFileLiteral
- * </pre>
- */
-public abstract class ImapElement {
- /**
- * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index
- * is out of range.
- */
- public static final ImapElement NONE = new ImapElement() {
- @Override public void destroy() {
- // Don't call super.destroy().
- // It's a shared object. We don't want the mDestroyed to be set on this.
- }
-
- @Override public boolean isList() {
- return false;
- }
-
- @Override public boolean isString() {
- return false;
- }
-
- @Override public String toString() {
- return "[NO ELEMENT]";
- }
-
- @Override
- public boolean equalsForTest(ImapElement that) {
- return super.equalsForTest(that);
- }
- };
-
- private boolean mDestroyed = false;
-
- public abstract boolean isList();
-
- public abstract boolean isString();
-
- protected boolean isDestroyed() {
- return mDestroyed;
- }
-
- /**
- * Clean up the resources used by the instance.
- * It's for removing a temp file used by {@link ImapTempFileLiteral}.
- */
- public void destroy() {
- mDestroyed = true;
- }
-
- /**
- * Throws {@link RuntimeException} if it's already destroyed.
- */
- protected final void checkNotDestroyed() {
- if (mDestroyed) {
- throw new RuntimeException("Already destroyed");
- }
- }
-
- /**
- * Return a string that represents this object; it's purely for the debug purpose. Don't
- * mistake it for {@link ImapString#getString}.
- *
- * Abstract to force subclasses to implement it.
- */
- @Override
- public abstract String toString();
-
- /**
- * The equals implementation that is intended to be used only for unit testing.
- * (Because it may be heavy and has a special sense of "equal" for testing.)
- */
- public boolean equalsForTest(ImapElement that) {
- if (that == null) {
- return false;
- }
- return this.getClass() == that.getClass(); // Has to be the same class.
- }
-}
diff --git a/src/com/android/email/mail/store/imap/ImapList.java b/src/com/android/email/mail/store/imap/ImapList.java
deleted file mode 100644
index e28355989..000000000
--- a/src/com/android/email/mail/store/imap/ImapList.java
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store.imap;
-
-import java.util.ArrayList;
-
-/**
- * Class represents an IMAP list.
- */
-public class ImapList extends ImapElement {
- /**
- * {@link ImapList} representing an empty list.
- */
- public static final ImapList EMPTY = new ImapList() {
- @Override public void destroy() {
- // Don't call super.destroy().
- // It's a shared object. We don't want the mDestroyed to be set on this.
- }
-
- @Override void add(ImapElement e) {
- throw new RuntimeException();
- }
- };
-
- private ArrayList<ImapElement> mList = new ArrayList<ImapElement>();
-
- /* package */ void add(ImapElement e) {
- if (e == null) {
- throw new RuntimeException("Can't add null");
- }
- mList.add(e);
- }
-
- @Override
- public final boolean isString() {
- return false;
- }
-
- @Override
- public final boolean isList() {
- return true;
- }
-
- public final int size() {
- return mList.size();
- }
-
- public final boolean isEmpty() {
- return size() == 0;
- }
-
- /**
- * Return true if the element at {@code index} exists, is string, and equals to {@code s}.
- * (case insensitive)
- */
- public final boolean is(int index, String s) {
- return is(index, s, false);
- }
-
- /**
- * Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}.
- */
- public final boolean is(int index, String s, boolean prefixMatch) {
- if (!prefixMatch) {
- return getStringOrEmpty(index).is(s);
- } else {
- return getStringOrEmpty(index).startsWith(s);
- }
- }
-
- /**
- * Return the element at {@code index}.
- * If {@code index} is out of range, returns {@link ImapElement#NONE}.
- */
- public final ImapElement getElementOrNone(int index) {
- return (index >= mList.size()) ? ImapElement.NONE : mList.get(index);
- }
-
- /**
- * Return the element at {@code index} if it's a list.
- * If {@code index} is out of range or not a list, returns {@link ImapList#EMPTY}.
- */
- public final ImapList getListOrEmpty(int index) {
- ImapElement el = getElementOrNone(index);
- return el.isList() ? (ImapList) el : EMPTY;
- }
-
- /**
- * Return the element at {@code index} if it's a string.
- * If {@code index} is out of range or not a string, returns {@link ImapString#EMPTY}.
- */
- public final ImapString getStringOrEmpty(int index) {
- ImapElement el = getElementOrNone(index);
- return el.isString() ? (ImapString) el : ImapString.EMPTY;
- }
-
- /**
- * Return an element keyed by {@code key}. Return null if not found. {@code key} has to be
- * at an even index.
- */
- /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) {
- for (int i = 1; i < size(); i += 2) {
- if (is(i-1, key, prefixMatch)) {
- return mList.get(i);
- }
- }
- return null;
- }
-
- /**
- * Return an {@link ImapList} keyed by {@code key}.
- * Return {@link ImapList#EMPTY} if not found.
- */
- public final ImapList getKeyedListOrEmpty(String key) {
- return getKeyedListOrEmpty(key, false);
- }
-
- /**
- * Return an {@link ImapList} keyed by {@code key}.
- * Return {@link ImapList#EMPTY} if not found.
- */
- public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) {
- ImapElement e = getKeyedElementOrNull(key, prefixMatch);
- return (e != null) ? ((ImapList) e) : ImapList.EMPTY;
- }
-
- /**
- * Return an {@link ImapString} keyed by {@code key}.
- * Return {@link ImapString#EMPTY} if not found.
- */
- public final ImapString getKeyedStringOrEmpty(String key) {
- return getKeyedStringOrEmpty(key, false);
- }
-
- /**
- * Return an {@link ImapString} keyed by {@code key}.
- * Return {@link ImapString#EMPTY} if not found.
- */
- public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) {
- ImapElement e = getKeyedElementOrNull(key, prefixMatch);
- return (e != null) ? ((ImapString) e) : ImapString.EMPTY;
- }
-
- /**
- * Return true if it contains {@code s}.
- */
- public final boolean contains(String s) {
- for (int i = 0; i < size(); i++) {
- if (getStringOrEmpty(i).is(s)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public void destroy() {
- if (mList != null) {
- for (ImapElement e : mList) {
- e.destroy();
- }
- mList = null;
- }
- super.destroy();
- }
-
- @Override
- public String toString() {
- return mList.toString();
- }
-
- /**
- * Return the text representations of the contents concatenated with ",".
- */
- public final String flatten() {
- return flatten(new StringBuilder()).toString();
- }
-
- /**
- * Returns text representations (i.e. getString()) of contents joined together with
- * "," as the separator.
- *
- * Only used for building the capability string passed to vendor policies.
- *
- * We can't use toString(), because it's for debugging (meaning the format may change any time),
- * and it won't expand literals.
- */
- private final StringBuilder flatten(StringBuilder sb) {
- sb.append('[');
- for (int i = 0; i < mList.size(); i++) {
- if (i > 0) {
- sb.append(',');
- }
- final ImapElement e = getElementOrNone(i);
- if (e.isList()) {
- getListOrEmpty(i).flatten(sb);
- } else if (e.isString()) {
- sb.append(getStringOrEmpty(i).getString());
- }
- }
- sb.append(']');
- return sb;
- }
-
- @Override
- public boolean equalsForTest(ImapElement that) {
- if (!super.equalsForTest(that)) {
- return false;
- }
- ImapList thatList = (ImapList) that;
- if (size() != thatList.size()) {
- return false;
- }
- for (int i = 0; i < size(); i++) {
- if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) {
- return false;
- }
- }
- return true;
- }
-}
diff --git a/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java b/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java
deleted file mode 100644
index 26f5e6c9c..000000000
--- a/src/com/android/email/mail/store/imap/ImapMemoryLiteral.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store.imap;
-
-import com.android.email.FixedLengthInputStream;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.utility.Utility;
-import com.android.mail.utils.LogUtils;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Subclass of {@link ImapString} used for literals backed by an in-memory byte array.
- */
-public class ImapMemoryLiteral extends ImapString {
- private byte[] mData;
-
- /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException {
- // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary
- // copy....
- mData = new byte[in.getLength()];
- int pos = 0;
- while (pos < mData.length) {
- int read = in.read(mData, pos, mData.length - pos);
- if (read < 0) {
- break;
- }
- pos += read;
- }
- if (pos != mData.length) {
- LogUtils.w(Logging.LOG_TAG, "");
- }
- }
-
- @Override
- public void destroy() {
- mData = null;
- super.destroy();
- }
-
- @Override
- public String getString() {
- return Utility.fromAscii(mData);
- }
-
- @Override
- public InputStream getAsStream() {
- return new ByteArrayInputStream(mData);
- }
-
- @Override
- public String toString() {
- return String.format("{%d byte literal(memory)}", mData.length);
- }
-}
diff --git a/src/com/android/email/mail/store/imap/ImapResponse.java b/src/com/android/email/mail/store/imap/ImapResponse.java
deleted file mode 100644
index 05bf594e6..000000000
--- a/src/com/android/email/mail/store/imap/ImapResponse.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store.imap;
-
-
-/**
- * Class represents an IMAP response.
- */
-public class ImapResponse extends ImapList {
- private final String mTag;
- private final boolean mIsContinuationRequest;
-
- /* package */ ImapResponse(String tag, boolean isContinuationRequest) {
- mTag = tag;
- mIsContinuationRequest = isContinuationRequest;
- }
-
- /* package */ static boolean isStatusResponse(String symbol) {
- return ImapConstants.OK.equalsIgnoreCase(symbol)
- || ImapConstants.NO.equalsIgnoreCase(symbol)
- || ImapConstants.BAD.equalsIgnoreCase(symbol)
- || ImapConstants.PREAUTH.equalsIgnoreCase(symbol)
- || ImapConstants.BYE.equalsIgnoreCase(symbol);
- }
-
- /**
- * @return whether it's a tagged response.
- */
- public boolean isTagged() {
- return mTag != null;
- }
-
- /**
- * @return whether it's a continuation request.
- */
- public boolean isContinuationRequest() {
- return mIsContinuationRequest;
- }
-
- public boolean isStatusResponse() {
- return isStatusResponse(getStringOrEmpty(0).getString());
- }
-
- /**
- * @return whether it's an OK response.
- */
- public boolean isOk() {
- return is(0, ImapConstants.OK);
- }
-
- /**
- * @return whether it's an BAD response.
- */
- public boolean isBad() {
- return is(0, ImapConstants.BAD);
- }
-
- /**
- * @return whether it's an NO response.
- */
- public boolean isNo() {
- return is(0, ImapConstants.NO);
- }
-
- /**
- * @return whether it's an {@code responseType} data response. (i.e. not tagged).
- * @param index where {@code responseType} should appear. e.g. 1 for "FETCH"
- * @param responseType e.g. "FETCH"
- */
- public final boolean isDataResponse(int index, String responseType) {
- return !isTagged() && getStringOrEmpty(index).is(responseType);
- }
-
- /**
- * @return Response code (RFC 3501 7.1) if it's a status response.
- *
- * e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes"
- */
- public ImapString getResponseCodeOrEmpty() {
- if (!isStatusResponse()) {
- return ImapString.EMPTY; // Not a status response.
- }
- return getListOrEmpty(1).getStringOrEmpty(0);
- }
-
- /**
- * @return Alert message it it has ALERT response code.
- *
- * e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes"
- */
- public ImapString getAlertTextOrEmpty() {
- if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) {
- return ImapString.EMPTY; // Not an ALERT
- }
- // The 3rd element contains all the rest of line.
- return getStringOrEmpty(2);
- }
-
- /**
- * @return Response text in a status response.
- */
- public ImapString getStatusResponseTextOrEmpty() {
- if (!isStatusResponse()) {
- return ImapString.EMPTY;
- }
- return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1);
- }
-
- @Override
- public String toString() {
- String tag = mTag;
- if (isContinuationRequest()) {
- tag = "+";
- }
- return "#" + tag + "# " + super.toString();
- }
-
- @Override
- public boolean equalsForTest(ImapElement that) {
- if (!super.equalsForTest(that)) {
- return false;
- }
- final ImapResponse thatResponse = (ImapResponse) that;
- if (mTag == null) {
- if (thatResponse.mTag != null) {
- return false;
- }
- } else {
- if (!mTag.equals(thatResponse.mTag)) {
- return false;
- }
- }
- if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) {
- return false;
- }
- return true;
- }
-}
diff --git a/src/com/android/email/mail/store/imap/ImapResponseParser.java b/src/com/android/email/mail/store/imap/ImapResponseParser.java
deleted file mode 100644
index 8dd1cf610..000000000
--- a/src/com/android/email/mail/store/imap/ImapResponseParser.java
+++ /dev/null
@@ -1,453 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store.imap;
-
-import android.text.TextUtils;
-
-import com.android.email.DebugUtils;
-import com.android.email.FixedLengthInputStream;
-import com.android.email.PeekableInputStream;
-import com.android.email.mail.transport.DiscourseLogger;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.utility.LoggingInputStream;
-import com.android.mail.utils.LogUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-/**
- * IMAP response parser.
- */
-public class ImapResponseParser {
- private static final boolean DEBUG_LOG_RAW_STREAM = false; // DO NOT RELEASE AS 'TRUE'
-
- /**
- * Literal larger than this will be stored in temp file.
- */
- public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024;
-
- /** Input stream */
- private final PeekableInputStream mIn;
-
- /**
- * To log network activities when the parser crashes.
- *
- * <p>We log all bytes received from the server, except for the part sent as literals.
- */
- private final DiscourseLogger mDiscourseLogger;
-
- private final int mLiteralKeepInMemoryThreshold;
-
- /** StringBuilder used by readUntil() */
- private final StringBuilder mBufferReadUntil = new StringBuilder();
-
- /** StringBuilder used by parseBareString() */
- private final StringBuilder mParseBareString = new StringBuilder();
-
- /**
- * We store all {@link ImapResponse} in it. {@link #destroyResponses()} must be called from
- * time to time to destroy them and clear it.
- */
- private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
-
- /**
- * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated
- * in the same way EOF does.
- */
- public static class ByeException extends IOException {
- public static final String MESSAGE = "Received BYE";
- public ByeException() {
- super(MESSAGE);
- }
- }
-
- /**
- * Public constructor for normal use.
- */
- public ImapResponseParser(InputStream in, DiscourseLogger discourseLogger) {
- this(in, discourseLogger, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
- }
-
- /**
- * Constructor for testing to override the literal size threshold.
- */
- /* package for test */ ImapResponseParser(InputStream in, DiscourseLogger discourseLogger,
- int literalKeepInMemoryThreshold) {
- if (DEBUG_LOG_RAW_STREAM && DebugUtils.DEBUG) {
- in = new LoggingInputStream(in);
- }
- mIn = new PeekableInputStream(in);
- mDiscourseLogger = discourseLogger;
- mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
- }
-
- private static IOException newEOSException() {
- final String message = "End of stream reached";
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, message);
- }
- return new IOException(message);
- }
-
- /**
- * Peek next one byte.
- *
- * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n,
- * we shouldn't see EOF during parsing.
- */
- private int peek() throws IOException {
- final int next = mIn.peek();
- if (next == -1) {
- throw newEOSException();
- }
- return next;
- }
-
- /**
- * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
- *
- * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n,
- * we shouldn't see EOF during parsing.
- */
- private int readByte() throws IOException {
- int next = mIn.read();
- if (next == -1) {
- throw newEOSException();
- }
- mDiscourseLogger.addReceivedByte(next);
- return next;
- }
-
- /**
- * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
- *
- * @see #readResponse()
- */
- public void destroyResponses() {
- for (ImapResponse r : mResponsesToDestroy) {
- r.destroy();
- }
- mResponsesToDestroy.clear();
- }
-
- /**
- * Reads the next response available on the stream and returns an
- * {@link ImapResponse} object that represents it.
- *
- * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse}
- * is stored in the internal storage. When the {@link ImapResponse} is no longer used
- * {@link #destroyResponses} should be called to destroy all the responses in the array.
- *
- * @return the parsed {@link ImapResponse} object.
- * @exception ByeException when detects BYE.
- */
- public ImapResponse readResponse() throws IOException, MessagingException {
- ImapResponse response = null;
- try {
- response = parseResponse();
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, "<<< " + response.toString());
- }
-
- } catch (RuntimeException e) {
- // Parser crash -- log network activities.
- onParseError(e);
- throw e;
- } catch (IOException e) {
- // Network error, or received an unexpected char.
- onParseError(e);
- throw e;
- }
-
- // Handle this outside of try-catch. We don't have to dump protocol log when getting BYE.
- if (response.is(0, ImapConstants.BYE)) {
- LogUtils.w(Logging.LOG_TAG, ByeException.MESSAGE);
- response.destroy();
- throw new ByeException();
- }
- mResponsesToDestroy.add(response);
- return response;
- }
-
- private void onParseError(Exception e) {
- // Read a few more bytes, so that the log will contain some more context, even if the parser
- // crashes in the middle of a response.
- // This also makes sure the byte in question will be logged, no matter where it crashes.
- // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
- // before actually reading it.
- // However, we don't want to read too much, because then it may get into an email message.
- try {
- for (int i = 0; i < 4; i++) {
- int b = readByte();
- if (b == -1 || b == '\n') {
- break;
- }
- }
- } catch (IOException ignore) {
- }
- LogUtils.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage());
- mDiscourseLogger.logLastDiscourse();
- }
-
- /**
- * Read next byte from stream and throw it away. If the byte is different from {@code expected}
- * throw {@link MessagingException}.
- */
- /* package for test */ void expect(char expected) throws IOException {
- final int next = readByte();
- if (expected != next) {
- throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)",
- (int) expected, expected, next, (char) next));
- }
- }
-
- /**
- * Read bytes until we find {@code end}, and return all as string.
- * The {@code end} will be read (rather than peeked) and won't be included in the result.
- */
- /* package for test */ String readUntil(char end) throws IOException {
- mBufferReadUntil.setLength(0);
- for (;;) {
- final int ch = readByte();
- if (ch != end) {
- mBufferReadUntil.append((char) ch);
- } else {
- return mBufferReadUntil.toString();
- }
- }
- }
-
- /**
- * Read all bytes until \r\n.
- */
- /* package */ String readUntilEol() throws IOException {
- String ret = readUntil('\r');
- expect('\n'); // TODO Should this really be error?
- return ret;
- }
-
- /**
- * Parse and return the response line.
- */
- private ImapResponse parseResponse() throws IOException, MessagingException {
- // We need to destroy the response if we get an exception.
- // So, we first store the response that's being built in responseToDestroy, until it's
- // completely built, at which point we copy it into responseToReturn and null out
- // responseToDestroyt.
- // If responseToDestroy is not null in finally, we destroy it because that means
- // we got an exception somewhere.
- ImapResponse responseToDestroy = null;
- final ImapResponse responseToReturn;
-
- try {
- final int ch = peek();
- if (ch == '+') { // Continuation request
- readByte(); // skip +
- expect(' ');
- responseToDestroy = new ImapResponse(null, true);
-
- // If it's continuation request, we don't really care what's in it.
- responseToDestroy.add(new ImapSimpleString(readUntilEol()));
-
- // Response has successfully been built. Let's return it.
- responseToReturn = responseToDestroy;
- responseToDestroy = null;
- } else {
- // Status response or response data
- final String tag;
- if (ch == '*') {
- tag = null;
- readByte(); // skip *
- expect(' ');
- } else {
- tag = readUntil(' ');
- }
- responseToDestroy = new ImapResponse(tag, false);
-
- final ImapString firstString = parseBareString();
- responseToDestroy.add(firstString);
-
- // parseBareString won't eat a space after the string, so we need to skip it,
- // if exists.
- // If the next char is not ' ', it should be EOL.
- if (peek() == ' ') {
- readByte(); // skip ' '
-
- if (responseToDestroy.isStatusResponse()) { // It's a status response
-
- // Is there a response code?
- final int next = peek();
- if (next == '[') {
- responseToDestroy.add(parseList('[', ']'));
- if (peek() == ' ') { // Skip following space
- readByte();
- }
- }
-
- String rest = readUntilEol();
- if (!TextUtils.isEmpty(rest)) {
- // The rest is free-form text.
- responseToDestroy.add(new ImapSimpleString(rest));
- }
- } else { // It's a response data.
- parseElements(responseToDestroy, '\0');
- }
- } else {
- expect('\r');
- expect('\n');
- }
-
- // Response has successfully been built. Let's return it.
- responseToReturn = responseToDestroy;
- responseToDestroy = null;
- }
- } finally {
- if (responseToDestroy != null) {
- // We get an exception.
- responseToDestroy.destroy();
- }
- }
-
- return responseToReturn;
- }
-
- private ImapElement parseElement() throws IOException, MessagingException {
- final int next = peek();
- switch (next) {
- case '(':
- return parseList('(', ')');
- case '[':
- return parseList('[', ']');
- case '"':
- readByte(); // Skip "
- return new ImapSimpleString(readUntil('"'));
- case '{':
- return parseLiteral();
- case '\r': // CR
- readByte(); // Consume \r
- expect('\n'); // Should be followed by LF.
- return null;
- case '\n': // LF // There shouldn't be a bare LF, but just in case.
- readByte(); // Consume \n
- return null;
- default:
- return parseBareString();
- }
- }
-
- /**
- * Parses an atom.
- *
- * Special case: If an atom contains '[', everything until the next ']' will be considered
- * a part of the atom.
- * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
- *
- * If the value is "NIL", returns an empty string.
- */
- private ImapString parseBareString() throws IOException, MessagingException {
- mParseBareString.setLength(0);
- for (;;) {
- final int ch = peek();
-
- // TODO Can we clean this up? (This condition is from the old parser.)
- if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
- // ']' is not part of atom (it's in resp-specials)
- ch == ']' ||
- // docs claim that flags are \ atom but atom isn't supposed to
- // contain
- // * and some flags contain *
- // ch == '%' || ch == '*' ||
- ch == '%' ||
- // TODO probably should not allow \ and should recognize
- // it as a flag instead
- // ch == '"' || ch == '\' ||
- ch == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) {
- if (mParseBareString.length() == 0) {
- throw new MessagingException("Expected string, none found.");
- }
- String s = mParseBareString.toString();
-
- // NIL will be always converted into the empty string.
- if (ImapConstants.NIL.equalsIgnoreCase(s)) {
- return ImapString.EMPTY;
- }
- return new ImapSimpleString(s);
- } else if (ch == '[') {
- // Eat all until next ']'
- mParseBareString.append((char) readByte());
- mParseBareString.append(readUntil(']'));
- mParseBareString.append(']'); // readUntil won't include the end char.
- } else {
- mParseBareString.append((char) readByte());
- }
- }
- }
-
- private void parseElements(ImapList list, char end)
- throws IOException, MessagingException {
- for (;;) {
- for (;;) {
- final int next = peek();
- if (next == end) {
- return;
- }
- if (next != ' ') {
- break;
- }
- // Skip space
- readByte();
- }
- final ImapElement el = parseElement();
- if (el == null) { // EOL
- return;
- }
- list.add(el);
- }
- }
-
- private ImapList parseList(char opening, char closing)
- throws IOException, MessagingException {
- expect(opening);
- final ImapList list = new ImapList();
- parseElements(list, closing);
- expect(closing);
- return list;
- }
-
- private ImapString parseLiteral() throws IOException, MessagingException {
- expect('{');
- final int size;
- try {
- size = Integer.parseInt(readUntil('}'));
- } catch (NumberFormatException nfe) {
- throw new MessagingException("Invalid length in literal");
- }
- if (size < 0) {
- throw new MessagingException("Invalid negative length in literal");
- }
- expect('\r');
- expect('\n');
- FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
- if (size > mLiteralKeepInMemoryThreshold) {
- return new ImapTempFileLiteral(in);
- } else {
- return new ImapMemoryLiteral(in);
- }
- }
-}
diff --git a/src/com/android/email/mail/store/imap/ImapSimpleString.java b/src/com/android/email/mail/store/imap/ImapSimpleString.java
deleted file mode 100644
index 190c5237f..000000000
--- a/src/com/android/email/mail/store/imap/ImapSimpleString.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store.imap;
-
-import com.android.emailcommon.utility.Utility;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-
-/**
- * Subclass of {@link ImapString} used for non literals.
- */
-public class ImapSimpleString extends ImapString {
- private String mString;
-
- /* package */ ImapSimpleString(String string) {
- mString = (string != null) ? string : "";
- }
-
- @Override
- public void destroy() {
- mString = null;
- super.destroy();
- }
-
- @Override
- public String getString() {
- return mString;
- }
-
- @Override
- public InputStream getAsStream() {
- return new ByteArrayInputStream(Utility.toAscii(mString));
- }
-
- @Override
- public String toString() {
- // Purposefully not return just mString, in order to prevent using it instead of getString.
- return "\"" + mString + "\"";
- }
-}
diff --git a/src/com/android/email/mail/store/imap/ImapString.java b/src/com/android/email/mail/store/imap/ImapString.java
deleted file mode 100644
index d74a7cf0e..000000000
--- a/src/com/android/email/mail/store/imap/ImapString.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store.imap;
-
-import com.android.emailcommon.Logging;
-import com.android.mail.utils.LogUtils;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-
-/**
- * Class represents an IMAP "element" that is not a list.
- *
- * An atom, quoted string, literal, are all represented by this. Values like OK, STATUS are too.
- * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]".
- * See {@link ImapResponseParser}.
- */
-public abstract class ImapString extends ImapElement {
- private static final byte[] EMPTY_BYTES = new byte[0];
-
- public static final ImapString EMPTY = new ImapString() {
- @Override public void destroy() {
- // Don't call super.destroy().
- // It's a shared object. We don't want the mDestroyed to be set on this.
- }
-
- @Override public String getString() {
- return "";
- }
-
- @Override public InputStream getAsStream() {
- return new ByteArrayInputStream(EMPTY_BYTES);
- }
-
- @Override public String toString() {
- return "";
- }
- };
-
- // This is used only for parsing IMAP's FETCH ENVELOPE command, in which
- // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be
- // handled by Locale.US
- private final static SimpleDateFormat DATE_TIME_FORMAT =
- new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US);
-
- private boolean mIsInteger;
- private int mParsedInteger;
- private Date mParsedDate;
-
- @Override
- public final boolean isList() {
- return false;
- }
-
- @Override
- public final boolean isString() {
- return true;
- }
-
- /**
- * @return true if and only if the length of the string is larger than 0.
- *
- * Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser
- * #parseBareString}.
- * On the other hand, a quoted/literal string with value NIL (i.e. "NIL" and {3}\r\nNIL) is
- * treated literally.
- */
- public final boolean isEmpty() {
- return getString().length() == 0;
- }
-
- public abstract String getString();
-
- public abstract InputStream getAsStream();
-
- /**
- * @return whether it can be parsed as a number.
- */
- public final boolean isNumber() {
- if (mIsInteger) {
- return true;
- }
- try {
- mParsedInteger = Integer.parseInt(getString());
- mIsInteger = true;
- return true;
- } catch (NumberFormatException e) {
- return false;
- }
- }
-
- /**
- * @return value parsed as a number.
- */
- public final int getNumberOrZero() {
- if (!isNumber()) {
- return 0;
- }
- return mParsedInteger;
- }
-
- /**
- * @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}.
- */
- public final boolean isDate() {
- if (mParsedDate != null) {
- return true;
- }
- if (isEmpty()) {
- return false;
- }
- try {
- mParsedDate = DATE_TIME_FORMAT.parse(getString());
- return true;
- } catch (ParseException e) {
- LogUtils.w(Logging.LOG_TAG, getString() + " can't be parsed as a date.");
- return false;
- }
- }
-
- /**
- * @return value it can be parsed as a {@link Date}, or null otherwise.
- */
- public final Date getDateOrNull() {
- if (!isDate()) {
- return null;
- }
- return mParsedDate;
- }
-
- /**
- * @return whether the value case-insensitively equals to {@code s}.
- */
- public final boolean is(String s) {
- if (s == null) {
- return false;
- }
- return getString().equalsIgnoreCase(s);
- }
-
-
- /**
- * @return whether the value case-insensitively starts with {@code s}.
- */
- public final boolean startsWith(String prefix) {
- if (prefix == null) {
- return false;
- }
- final String me = this.getString();
- if (me.length() < prefix.length()) {
- return false;
- }
- return me.substring(0, prefix.length()).equalsIgnoreCase(prefix);
- }
-
- // To force subclasses to implement it.
- @Override
- public abstract String toString();
-
- @Override
- public final boolean equalsForTest(ImapElement that) {
- if (!super.equalsForTest(that)) {
- return false;
- }
- ImapString thatString = (ImapString) that;
- return getString().equals(thatString.getString());
- }
-}
diff --git a/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java b/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java
deleted file mode 100644
index 4feccc760..000000000
--- a/src/com/android/email/mail/store/imap/ImapTempFileLiteral.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store.imap;
-
-import com.android.email.FixedLengthInputStream;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.TempDirectory;
-import com.android.emailcommon.utility.Utility;
-import com.android.mail.utils.LogUtils;
-
-import org.apache.commons.io.IOUtils;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Subclass of {@link ImapString} used for literals backed by a temp file.
- */
-public class ImapTempFileLiteral extends ImapString {
- /* package for test */ final File mFile;
-
- /** Size is purely for toString() */
- private final int mSize;
-
- /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
- mSize = stream.getLength();
- mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
-
- // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
- // so it'd simply cause a memory leak.
- // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
- // mFile.deleteOnExit();
- OutputStream out = new FileOutputStream(mFile);
- IOUtils.copy(stream, out);
- out.close();
- }
-
- /**
- * Make sure we delete the temp file.
- *
- * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
- */
- @Override
- protected void finalize() throws Throwable {
- try {
- destroy();
- } finally {
- super.finalize();
- }
- }
-
- @Override
- public InputStream getAsStream() {
- checkNotDestroyed();
- try {
- return new FileInputStream(mFile);
- } catch (FileNotFoundException e) {
- // It's probably possible if we're low on storage and the system clears the cache dir.
- LogUtils.w(Logging.LOG_TAG, "ImapTempFileLiteral: Temp file not found");
-
- // Return 0 byte stream as a dummy...
- return new ByteArrayInputStream(new byte[0]);
- }
- }
-
- @Override
- public String getString() {
- checkNotDestroyed();
- try {
- byte[] bytes = IOUtils.toByteArray(getAsStream());
- // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
- if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
- throw new IOException();
- }
- return Utility.fromAscii(bytes);
- } catch (IOException e) {
- LogUtils.w(Logging.LOG_TAG, "ImapTempFileLiteral: Error while reading temp file", e);
- return "";
- }
- }
-
- @Override
- public void destroy() {
- try {
- if (!isDestroyed() && mFile.exists()) {
- mFile.delete();
- }
- } catch (RuntimeException re) {
- // Just log and ignore.
- LogUtils.w(Logging.LOG_TAG, "Failed to remove temp file: " + re.getMessage());
- }
- super.destroy();
- }
-
- @Override
- public String toString() {
- return String.format("{%d byte literal(file)}", mSize);
- }
-
- public boolean tempFileExistsForTest() {
- return mFile.exists();
- }
-}
diff --git a/src/com/android/email/mail/store/imap/ImapUtility.java b/src/com/android/email/mail/store/imap/ImapUtility.java
deleted file mode 100644
index dc024cce7..000000000
--- a/src/com/android/email/mail/store/imap/ImapUtility.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.store.imap;
-
-import com.android.emailcommon.Logging;
-import com.android.mail.utils.LogUtils;
-
-import java.util.ArrayList;
-
-/**
- * Utility methods for use with IMAP.
- */
-public class ImapUtility {
- /**
- * Apply quoting rules per IMAP RFC,
- * quoted = DQUOTE *QUOTED-CHAR DQUOTE
- * QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
- * quoted-specials = DQUOTE / "\"
- *
- * This is used primarily for IMAP login, but might be useful elsewhere.
- *
- * NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check
- * for trouble chars before calling the replace functions.
- *
- * @param s The string to be quoted.
- * @return A copy of the string, having undergone quoting as described above
- */
- public static String imapQuoted(String s) {
-
- // First, quote any backslashes by replacing \ with \\
- // regex Pattern: \\ (Java string const = \\\\)
- // Substitute: \\\\ (Java string const = \\\\\\\\)
- String result = s.replaceAll("\\\\", "\\\\\\\\");
-
- // Then, quote any double-quotes by replacing " with \"
- // regex Pattern: " (Java string const = \")
- // Substitute: \\" (Java string const = \\\\\")
- result = result.replaceAll("\"", "\\\\\"");
-
- // return string with quotes around it
- return "\"" + result + "\"";
- }
-
- /**
- * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a
- * list of individual numbers. If the set is invalid, an empty array is returned.
- * <pre>
- * sequence-number = nz-number / "*"
- * sequence-range = sequence-number ":" sequence-number
- * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
- * </pre>
- */
- public static String[] getImapSequenceValues(String set) {
- ArrayList<String> list = new ArrayList<String>();
- if (set != null) {
- String[] setItems = set.split(",");
- for (String item : setItems) {
- if (item.indexOf(':') == -1) {
- // simple item
- try {
- Integer.parseInt(item); // Don't need the value; just ensure it's valid
- list.add(item);
- } catch (NumberFormatException e) {
- LogUtils.d(Logging.LOG_TAG, "Invalid UID value", e);
- }
- } else {
- // range
- for (String rangeItem : getImapRangeValues(item)) {
- list.add(rangeItem);
- }
- }
- }
- }
- String[] stringList = new String[list.size()];
- return list.toArray(stringList);
- }
-
- /**
- * Expand the given number range into a list of individual numbers. If the range is not valid,
- * an empty array is returned.
- * <pre>
- * sequence-number = nz-number / "*"
- * sequence-range = sequence-number ":" sequence-number
- * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
- * </pre>
- */
- public static String[] getImapRangeValues(String range) {
- ArrayList<String> list = new ArrayList<String>();
- try {
- if (range != null) {
- int colonPos = range.indexOf(':');
- if (colonPos > 0) {
- int first = Integer.parseInt(range.substring(0, colonPos));
- int second = Integer.parseInt(range.substring(colonPos + 1));
- if (first < second) {
- for (int i = first; i <= second; i++) {
- list.add(Integer.toString(i));
- }
- } else {
- for (int i = first; i >= second; i--) {
- list.add(Integer.toString(i));
- }
- }
- }
- }
- } catch (NumberFormatException e) {
- LogUtils.d(Logging.LOG_TAG, "Invalid range value", e);
- }
- String[] stringList = new String[list.size()];
- return list.toArray(stringList);
- }
-}
diff --git a/src/com/android/email/mail/transport/DiscourseLogger.java b/src/com/android/email/mail/transport/DiscourseLogger.java
deleted file mode 100644
index 67f4e115b..000000000
--- a/src/com/android/email/mail/transport/DiscourseLogger.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.transport;
-
-import com.android.emailcommon.Logging;
-import com.android.mail.utils.LogUtils;
-
-import java.util.ArrayList;
-
-/**
- * A class to keep last N of lines sent to the server and responses received from the server.
- * They are sent to logcat when {@link #logLastDiscourse} is called.
- *
- * <p>This class is used to log the recent network activities when a response parser crashes.
- */
-public class DiscourseLogger {
- private final int mBufferSize;
- private String[] mBuffer;
- private int mPos;
- private final StringBuilder mReceivingLine = new StringBuilder(100);
-
- public DiscourseLogger(int bufferSize) {
- mBufferSize = bufferSize;
- initBuffer();
- }
-
- private void initBuffer() {
- mBuffer = new String[mBufferSize];
- }
-
- /** Add a single line to {@link #mBuffer}. */
- private void addLine(String s) {
- mBuffer[mPos] = s;
- mPos++;
- if (mPos >= mBufferSize) {
- mPos = 0;
- }
- }
-
- private void addReceivingLineToBuffer() {
- if (mReceivingLine.length() > 0) {
- addLine(mReceivingLine.toString());
- mReceivingLine.delete(0, Integer.MAX_VALUE);
- }
- }
-
- /**
- * Store a single byte received from the server in {@link #mReceivingLine}. When LF is
- * received, the content of {@link #mReceivingLine} is added to {@link #mBuffer}.
- */
- public void addReceivedByte(int b) {
- if (0x20 <= b && b <= 0x7e) { // Append only printable ASCII chars.
- mReceivingLine.append((char) b);
- } else if (b == '\n') { // LF
- addReceivingLineToBuffer();
- } else if (b == '\r') { // CR
- } else {
- final String hex = "00" + Integer.toHexString(b);
- mReceivingLine.append("\\x" + hex.substring(hex.length() - 2, hex.length()));
- }
- }
-
- /** Add a line sent to the server to {@link #mBuffer}. */
- public void addSentCommand(String command) {
- addLine(command);
- }
-
- /** @return the contents of {@link #mBuffer} as a String array. */
- /* package for testing */ String[] getLines() {
- addReceivingLineToBuffer();
-
- ArrayList<String> list = new ArrayList<String>();
-
- final int start = mPos;
- int pos = mPos;
- do {
- String s = mBuffer[pos];
- if (s != null) {
- list.add(s);
- }
- pos = (pos + 1) % mBufferSize;
- } while (pos != start);
-
- String[] ret = new String[list.size()];
- list.toArray(ret);
- return ret;
- }
-
- /**
- * Log the contents of the {@link mBuffer}, and clears it out. (So it's okay to call this
- * method successively more than once. There will be no duplicate log.)
- */
- public void logLastDiscourse() {
- String[] lines = getLines();
- if (lines.length == 0) {
- return;
- }
-
- LogUtils.w(Logging.LOG_TAG, "Last network activities:");
- for (String r : getLines()) {
- LogUtils.w(Logging.LOG_TAG, "%s", r);
- }
- initBuffer();
- }
-}
diff --git a/src/com/android/email/mail/transport/MailTransport.java b/src/com/android/email/mail/transport/MailTransport.java
deleted file mode 100644
index 213fbfc99..000000000
--- a/src/com/android/email/mail/transport/MailTransport.java
+++ /dev/null
@@ -1,320 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.email.mail.transport;
-
-import android.content.Context;
-
-import com.android.email.DebugUtils;
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.mail.CertificateValidationException;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.emailcommon.utility.SSLUtils;
-import com.android.mail.utils.LogUtils;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.net.SocketAddress;
-import java.net.SocketException;
-
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.SSLSocket;
-
-public class MailTransport {
-
- // TODO protected eventually
- /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
- /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
-
- private static final HostnameVerifier HOSTNAME_VERIFIER =
- HttpsURLConnection.getDefaultHostnameVerifier();
-
- private final String mDebugLabel;
- private final Context mContext;
- protected final HostAuth mHostAuth;
-
- private Socket mSocket;
- private InputStream mIn;
- private OutputStream mOut;
-
- public MailTransport(Context context, String debugLabel, HostAuth hostAuth) {
- super();
- mContext = context;
- mDebugLabel = debugLabel;
- mHostAuth = hostAuth;
- }
-
- /**
- * Returns a new transport, using the current transport as a model. The new transport is
- * configured identically (as if {@link #setSecurity(int, boolean)}, {@link #setPort(int)}
- * and {@link #setHost(String)} were invoked), but not opened or connected in any way.
- */
- @Override
- public MailTransport clone() {
- return new MailTransport(mContext, mDebugLabel, mHostAuth);
- }
-
- public String getHost() {
- return mHostAuth.mAddress;
- }
-
- public int getPort() {
- return mHostAuth.mPort;
- }
-
- public boolean canTrySslSecurity() {
- return (mHostAuth.mFlags & HostAuth.FLAG_SSL) != 0;
- }
-
- public boolean canTryTlsSecurity() {
- return (mHostAuth.mFlags & HostAuth.FLAG_TLS) != 0;
- }
-
- public boolean canTrustAllCertificates() {
- return (mHostAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0;
- }
-
- /**
- * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt
- * an SSL connection if indicated.
- */
- public void open() throws MessagingException, CertificateValidationException {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, "*** " + mDebugLabel + " open " +
- getHost() + ":" + String.valueOf(getPort()));
- }
-
- try {
- SocketAddress socketAddress = new InetSocketAddress(getHost(), getPort());
- if (canTrySslSecurity()) {
- mSocket = SSLUtils.getSSLSocketFactory(
- mContext, mHostAuth, canTrustAllCertificates()).createSocket();
- } else {
- mSocket = new Socket();
- }
- mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
- // After the socket connects to an SSL server, confirm that the hostname is as expected
- if (canTrySslSecurity() && !canTrustAllCertificates()) {
- verifyHostname(mSocket, getHost());
- }
- mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
- mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
- mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
- } catch (SSLException e) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, e.toString());
- }
- throw new CertificateValidationException(e.getMessage(), e);
- } catch (IOException ioe) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ioe.toString());
- }
- throw new MessagingException(MessagingException.IOERROR, ioe.toString());
- } catch (IllegalArgumentException iae) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, iae.toString());
- }
- throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION, iae.toString());
- }
- }
-
- /**
- * Attempts to reopen a TLS connection using the Uri supplied for connection parameters.
- *
- * NOTE: No explicit hostname verification is required here, because it's handled automatically
- * by the call to createSocket().
- *
- * TODO should we explicitly close the old socket? This seems funky to abandon it.
- */
- public void reopenTls() throws MessagingException {
- try {
- mSocket = SSLUtils.getSSLSocketFactory(mContext, mHostAuth, canTrustAllCertificates())
- .createSocket(mSocket, getHost(), getPort(), true);
- mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
- mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
- mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
-
- } catch (SSLException e) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, e.toString());
- }
- throw new CertificateValidationException(e.getMessage(), e);
- } catch (IOException ioe) {
- if (DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, ioe.toString());
- }
- throw new MessagingException(MessagingException.IOERROR, ioe.toString());
- }
- }
-
- /**
- * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
- * service but is not in the public API.
- *
- * Verify the hostname of the certificate used by the other end of a
- * connected socket. You MUST call this if you did not supply a hostname
- * to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method
- * redundantly if the hostname has already been verified.
- *
- * <p>Wildcard certificates are allowed to verify any matching hostname,
- * so "foo.bar.example.com" is verified if the peer has a certificate
- * for "*.example.com".
- *
- * @param socket An SSL socket which has been connected to a server
- * @param hostname The expected hostname of the remote server
- * @throws IOException if something goes wrong handshaking with the server
- * @throws SSLPeerUnverifiedException if the server cannot prove its identity
- */
- private static void verifyHostname(Socket socket, String hostname) throws IOException {
- // The code at the start of OpenSSLSocketImpl.startHandshake()
- // ensures that the call is idempotent, so we can safely call it.
- SSLSocket ssl = (SSLSocket) socket;
- ssl.startHandshake();
-
- SSLSession session = ssl.getSession();
- if (session == null) {
- throw new SSLException("Cannot verify SSL socket without session");
- }
- // TODO: Instead of reporting the name of the server we think we're connecting to,
- // we should be reporting the bad name in the certificate. Unfortunately this is buried
- // in the verifier code and is not available in the verifier API, and extracting the
- // CN & alts is beyond the scope of this patch.
- if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
- throw new SSLPeerUnverifiedException(
- "Certificate hostname not useable for server: " + hostname);
- }
- }
-
- /**
- * Get the socket timeout.
- * @return the read timeout value in milliseconds
- * @throws SocketException
- */
- public int getSoTimeout() throws SocketException {
- return mSocket.getSoTimeout();
- }
-
- /**
- * Set the socket timeout.
- * @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or
- * {@code 0} for an infinite timeout.
- */
- public void setSoTimeout(int timeoutMilliseconds) throws SocketException {
- mSocket.setSoTimeout(timeoutMilliseconds);
- }
-
- public boolean isOpen() {
- return (mIn != null && mOut != null &&
- mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
- }
-
- /**
- * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe.
- */
- public void close() {
- try {
- mIn.close();
- } catch (Exception e) {
- // May fail if the connection is already closed.
- }
- try {
- mOut.close();
- } catch (Exception e) {
- // May fail if the connection is already closed.
- }
- try {
- mSocket.close();
- } catch (Exception e) {
- // May fail if the connection is already closed.
- }
- mIn = null;
- mOut = null;
- mSocket = null;
- }
-
- public InputStream getInputStream() {
- return mIn;
- }
-
- public OutputStream getOutputStream() {
- return mOut;
- }
-
- /**
- * Writes a single line to the server using \r\n termination.
- */
- public void writeLine(String s, String sensitiveReplacement) throws IOException {
- if (DebugUtils.DEBUG) {
- if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) {
- LogUtils.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement);
- } else {
- LogUtils.d(Logging.LOG_TAG, ">>> " + s);
- }
- }
-
- OutputStream out = getOutputStream();
- out.write(s.getBytes());
- out.write('\r');
- out.write('\n');
- out.flush();
- }
-
- /**
- * Reads a single line from the server, using either \r\n or \n as the delimiter. The
- * delimiter char(s) are not included in the result.
- */
- public String readLine(boolean loggable) throws IOException {
- StringBuffer sb = new StringBuffer();
- InputStream in = getInputStream();
- int d;
- while ((d = in.read()) != -1) {
- if (((char)d) == '\r') {
- continue;
- } else if (((char)d) == '\n') {
- break;
- } else {
- sb.append((char)d);
- }
- }
- if (d == -1 && DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, "End of stream reached while trying to read line.");
- }
- String ret = sb.toString();
- if (loggable && DebugUtils.DEBUG) {
- LogUtils.d(Logging.LOG_TAG, "<<< " + ret);
- }
- return ret;
- }
-
- public InetAddress getLocalAddress() {
- if (isOpen()) {
- return mSocket.getLocalAddress();
- } else {
- return null;
- }
- }
-}