diff options
Diffstat (limited to 'src/com/android/email/mail')
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; - } - } -} |