Restore Imap1

* Restore Imap1 code
* Legacy users will use Imap1
* Existing Imap2 users will continue to use Imap2
* New accounts will be created in Imap1
* More to follow

Bug: 7203993

Change-Id: I8b86fcada59a854fd464d5269c94d00ebae85459
This commit is contained in:
Marc Blank 2012-09-20 13:34:13 -07:00
parent ec0af7822c
commit 5c52385838
39 changed files with 5638 additions and 263 deletions

View File

@ -468,6 +468,17 @@
android:resource="@xml/syncadapter_pop3" />
</service>
<service
android:name="com.android.email.service.LegacyImapSyncAdapterService"
android:exported="true">
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter_legacy_imap" />
</service>
<!-- Require provider permission to use our Policy and Account services -->
<service
android:name=".service.PolicyService"
@ -576,15 +587,19 @@
/>
</service>
<service
android:name=".imap2.EmailSyncAdapterService"
android:exported="true">
<service
android:name=".service.LegacyImapAuthenticatorService"
android:exported="false"
android:enabled="true"
>
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter_imap" />
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator_legacy_imap"
/>
</service>
<service
@ -646,21 +661,6 @@
/>
</service>
<service
android:name=".service.LegacyImap2AuthenticatorService"
android:exported="false"
android:enabled="true"
>
<intent-filter>
<action
android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator_legacy_imap2"
/>
</service>
</application>
<!-- Legacy permissions, etc. can go here -->

View File

@ -17,12 +17,15 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- DO NOT TRANSLATE THESE STRINGS -->
<string name="account_manager_type_exchange" translatable="false">com.android.exchange</string>
<string name="account_manager_type_pop3" translatable="false">com.android.pop3</string>
<string name="account_manager_type_imap" translatable="false">com.android.imap</string>
<string name="account_manager_type_pop3" translatable="false">com.android.email</string>
<string name="account_manager_type_imap" translatable="false">com.android.email</string>
<string name="account_manager_type_legacy_imap" translatable="false">com.android.email</string>
<string name="intent_exchange" translatable="false">com.android.email.EXCHANGE_INTENT</string>
<string name="intent_account_manager_entry" translatable="false">com.android.email.ACCOUNT_MANAGER_ENTRY_INTENT</string>
<string name="authority_email_provider" translatable="false">com.android.email.provider</string>
<string name="protocol_legacy_imap" translatable="false">imap</string>
<string name="protocol_imap" translatable="false">imap</string>
<string name="protocol_pop3" translatable="false">pop3</string>
<string name="protocol_eas" translatable="false">eas</string>
<string name="application_mime_type" translatable="false">application/email-ls</string>
</resources>

View File

@ -50,8 +50,8 @@
<emailservices xmlns:email="http://schemas.android.com/apk/res/com.android.email">
<emailservice
email:protocol="pop3"
email:name="POP3"
email:accountType="com.android.email"
email:name="@string/pop3_name"
email:accountType="@string/account_manager_type_pop3"
email:serviceClass="com.android.email.service.Pop3Service"
email:port="110"
email:portSsl="995"
@ -66,17 +66,16 @@
email:offerLoadMore="true"
/>
<emailservice
email:protocol="imap2"
email:name="IMAP"
email:accountType="com.android.imap2"
email:serviceClass="com.android.email.imap2.Imap2SyncManager"
email:protocol="imap"
email:name="@string/imap_name"
email:accountType="@string/account_manager_type_imap"
email:serviceClass="com.android.email.service.ImapService"
email:port="143"
email:portSsl="993"
email:syncIntervalStrings="@array/account_settings_check_frequency_entries_push"
email:syncIntervals="@array/account_settings_check_frequency_values_push"
email:defaultSyncInterval="push"
email:syncIntervalStrings="@array/account_settings_check_frequency_entries"
email:syncIntervals="@array/account_settings_check_frequency_values"
email:defaultSyncInterval="mins15"
email:offerLookback="true"
email:offerTls="true"
email:usesSmtp="true"
email:offerAttachmentPreload="true"
@ -84,12 +83,11 @@
email:syncChanges="true"
email:inferPrefix="imap"
email:offerLoadMore="true"
email:requiresSetup="true"
/>
<emailservice
email:protocol="eas"
email:protocol="@string/protocol_eas"
email:name="Exchange"
email:accountType="com.android.exchange"
email:accountType="@string/account_manager_type_exchange"
email:intent="com.android.email.EXCHANGE_INTENT"
email:port="80"
email:portSsl="443"
@ -107,9 +105,4 @@
email:syncContacts="true"
email:syncCalendar="true"
/>
<emailservice
email:protocol="imap"
email:accountType="com.android.email"
email:replaceWith="imap2"
/>
</emailservices>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 617 B

View File

@ -24,6 +24,7 @@
<declare-styleable name="EmailServiceInfo">
<attr name="protocol" format="string"/>
<attr name="name" format="string"/>
<attr name="hide" format="boolean"/>
<attr name="accountType" format="string"/>
<attr name="replaceWith" format="string"/>
<attr name="serviceClass" format="string"/>

View File

@ -1322,8 +1322,8 @@ as <xliff:g id="filename">%s</xliff:g>.</string>
<string name="no_conversations">No messages.</string>
<!-- Used by AccountManager -->
<string name="imap2_name" translatable="false">IMAP</string>
<string name="pop3_name" translatable="false">POP3</string>
<string name="imap_name">IMAP</string>
<string name="pop3_name">POP3</string>
<string name="folder_picker_title">Folder picker</string>
<!-- Displayed when the user must pick his server's trash folder from a list [CHAR LIMIT 30]-->

View File

@ -24,6 +24,6 @@
android:accountType="@string/account_manager_type_imap"
android:icon="@mipmap/ic_launcher_mail"
android:smallIcon="@drawable/stat_notify_email_generic"
android:label="@string/imap2_name"
android:label="@string/imap_name"
android:accountPreferences="@xml/account_preferences"
/>

View File

@ -21,9 +21,9 @@
<!-- for the Account Manager. -->
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.android.imap2"
android:accountType="@string/account_manager_type_legacy_imap"
android:icon="@mipmap/ic_launcher_mail"
android:smallIcon="@drawable/stat_notify_email_generic"
android:label="@string/exchange_name"
android:label="@string/imap_name"
android:accountPreferences="@xml/account_preferences"
/>

View File

@ -22,6 +22,6 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="@string/authority_email_provider"
android:accountType="@string/account_manager_type_imap"
android:accountType="@string/account_manager_type_legacy_imap"
android:supportsUploading="true"
/>

View File

@ -80,7 +80,8 @@ public class AccountSetupType extends AccountSetupActivity implements OnClickLis
for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(this)) {
if (EmailServiceUtils.isServiceAvailable(this, info.protocol)) {
// If we're looking for a specific account type, reject others
if (accountType != null && !accountType.equals(info.accountType)) {
// Don't show types with "hide" set
if (info.hide || (accountType != null && !accountType.equals(info.accountType))) {
continue;
}
LayoutInflater.from(this).inflate(R.layout.account_type, parent);

View File

@ -21,6 +21,7 @@ import android.os.Bundle;
import android.util.Log;
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;
@ -84,6 +85,7 @@ public abstract class Store {
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

View File

@ -0,0 +1,519 @@
/*
* 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.Log;
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.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.MessagingException;
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;
private static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
MailTransport mTransport;
private ImapResponseParser mParser;
private ImapStore mImapStore;
private String mUsername;
private String mLoginPhrase;
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, String username, String password) {
setStore(store, username, password);
}
void setStore(ImapStore store, String username, String password) {
if (username != null && password != null) {
mUsername = username;
// build the LOGIN string once (instead of over-and-over again.)
// apply the quoting here around the built-up password
mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " "
+ ImapUtility.imapQuoted(password);
}
mImapStore = store;
}
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();
mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
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 (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, e.toString());
}
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 (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw ioe;
} finally {
destroyResponses();
}
}
/**
* Closes the connection and releases all resources. This connection can not be used again
* until {@link #setStore(ImapStore, String, String)} 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() : false;
}
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 {
open();
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 {
ArrayList<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();
destroyResponses();
throw new ImapException(toString, alert);
}
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 {
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(), mUsername, 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 (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} 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 (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} 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 {
// TODO eventually we need to add additional authentication
// options such as SASL
executeSimpleCommand(mLoginPhrase, true);
} catch (ImapException ie) {
if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
throw new AuthenticationFailedException(ie.getAlertText(), ie);
} catch (MessagingException me) {
throw new AuthenticationFailedException(null, me);
}
}
/**
* 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 (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} 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();
mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
createParser();
// Per RFC requirement (3501-6.2.1) gather new capabilities
return(queryCapabilities());
} else {
if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "TLS not supported but required");
}
throw new MessagingException(MessagingException.TLS_REQUIRED);
}
}
return null;
}
/** @see DiscourseLogger#logLastDiscourse() */
void logLastDiscourse() {
mDiscourse.logLastDiscourse();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,617 @@
/*
* 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 android.util.Log;
import com.android.email.LegacyConversions;
import com.android.email.Preferences;
import com.android.email.R;
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.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.utility.Utility;
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 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();
if (userInfo != null) {
mUsername = userInfo[0];
mPassword = userInfo[1];
} else {
mUsername = null;
mPassword = null;
}
mPathPrefix = recvAuth.mDomain;
}
@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) {
Log.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"
StringBuffer sb = new StringBuffer("\"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;
if (delimiterIdx != -1) {
String 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;
}
}
/**
* 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.
*/
private ImapFolder addMailbox(Context context, long accountId, String mailboxPath,
char delimiter, boolean selectable) {
ImapFolder folder = (ImapFolder) getFolder(mailboxPath);
Mailbox mailbox = Mailbox.getMailboxForPath(context, accountId, 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,
LegacyConversions.inferMailboxTypeFromName(context, mailboxPath));
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 {
ImapConnection connection = getConnection();
try {
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);
mailboxes.put(folderName, folder);
}
}
String inboxName = mContext.getString(R.string.mailbox_name_display_inbox);
Folder newFolder =
addMailbox(mContext, mAccount.mId, inboxName, '\0', true /*selectable*/);
mailboxes.put(ImapConstants.INBOX, (ImapFolder)newFolder);
createHierarchy(mailboxes);
saveMailboxList(mContext, mailboxes);
return mailboxes.values().toArray(new Folder[] {});
} 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) {
poolConnection(connection);
}
}
}
@Override
public Bundle checkSettings() throws MessagingException {
int result = MessagingException.NO_ERROR;
Bundle bundle = new Bundle();
ImapConnection connection = new ImapConnection(this, mUsername, mPassword);
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() {
ImapConnection connection = null;
while ((connection = mConnectionPool.poll()) != null) {
try {
connection.setStore(this, mUsername, mPassword);
connection.executeSimpleCommand(ImapConstants.NOOP);
break;
} catch (MessagingException e) {
// Fall through
} catch (IOException e) {
// Fall through
}
connection.close();
connection = null;
}
if (connection == null) {
connection = new ImapConnection(this, mUsername, mPassword);
}
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;
String mAlertText;
public ImapException(String message, String alertText, Throwable throwable) {
super(message, throwable);
mAlertText = alertText;
}
public ImapException(String message, String alertText) {
super(message);
mAlertText = alertText;
}
public String getAlertText() {
return mAlertText;
}
public void setAlertText(String alertText) {
mAlertText = alertText;
}
}
}

View File

@ -0,0 +1,95 @@
/*
* 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;
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("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 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 APPENDUID = "APPENDUID";
public static final String NIL = "NIL";
}

View File

@ -0,0 +1,120 @@
/*
* 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.
}
}

View File

@ -0,0 +1,235 @@
/*
* 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;
}
}

View File

@ -0,0 +1,72 @@
/*
* 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 android.util.Log;
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) {
Log.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);
}
}

View File

@ -0,0 +1,152 @@
/*
* 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;
}
}

View File

@ -0,0 +1,450 @@
/*
* 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 android.util.Log;
import com.android.email.FixedLengthInputStream;
import com.android.email.PeekableInputStream;
import com.android.email.mail.transport.DiscourseLogger;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.utility.LoggingInputStream;
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 && MailActivityEmail.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 (MailActivityEmail.DEBUG) {
Log.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 (MailActivityEmail.DEBUG) {
Log.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)) {
Log.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) {
}
Log.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");
}
expect('\r');
expect('\n');
FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
if (size > mLiteralKeepInMemoryThreshold) {
return new ImapTempFileLiteral(in);
} else {
return new ImapMemoryLiteral(in);
}
}
}

View File

@ -0,0 +1,55 @@
/*
* 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 + "\"";
}
}

View File

@ -0,0 +1,187 @@
/*
* 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 android.util.Log;
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) {
Log.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());
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.util.Log;
import com.android.email.FixedLengthInputStream;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TempDirectory;
import com.android.emailcommon.utility.Utility;
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.
Log.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) {
Log.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.
Log.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();
}
}

View File

@ -0,0 +1,127 @@
/*
* 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 android.util.Log;
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) {
Log.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) {
Log.d(Logging.LOG_TAG, "Invalid range value", e);
}
String[] stringList = new String[list.size()];
return list.toArray(stringList);
}
}

View File

@ -31,17 +31,15 @@ import android.util.Log;
import com.android.email.NotificationController;
import com.android.email.Preferences;
import com.android.email.R;
import com.android.email.SecurityPolicy;
import com.android.email.activity.setup.AccountSettings;
import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
import com.android.emailcommon.Logging;
import com.android.emailcommon.VendorPolicyLoader;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.HostAuth;
import java.util.List;
/**
* The service that really handles broadcast intents on a worker thread.
*
@ -185,7 +183,8 @@ public class EmailBroadcastProcessorService extends IntentService {
while (c.moveToNext()) {
long recvAuthKey = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
HostAuth recvAuth = HostAuth.restoreHostAuthWithId(context, recvAuthKey);
if (HostAuth.LEGACY_SCHEME_IMAP.equals(recvAuth.mProtocol)) {
String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap);
if (legacyImapProtocol.equals(recvAuth.mProtocol)) {
int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN);
flags &= ~Account.FLAGS_DELETE_POLICY_MASK;
flags |= Account.DELETE_POLICY_ON_DELETE << Account.FLAGS_DELETE_POLICY_SHIFT;

View File

@ -36,7 +36,6 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Debug;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.CalendarContract;
@ -178,7 +177,9 @@ public class EmailServiceUtils {
public boolean requiresAccountUpdate;
public boolean offerLoadMore;
public boolean requiresSetup;
public boolean hide;
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Protocol: ");
sb.append(protocol);
@ -288,7 +289,6 @@ public class EmailServiceUtils {
protected Void doInBackground(Void... params) {
disableComponent(mContext, LegacyEmailAuthenticatorService.class);
disableComponent(mContext, LegacyEasAuthenticatorService.class);
disableComponent(mContext, LegacyImap2AuthenticatorService.class);
return null;
}
}
@ -498,6 +498,7 @@ public class EmailServiceUtils {
continue;
}
info.name = ta.getString(R.styleable.EmailServiceInfo_name);
info.hide = ta.getBoolean(R.styleable.EmailServiceInfo_hide, false);
String klass = ta.getString(R.styleable.EmailServiceInfo_serviceClass);
info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent);
info.defaultSsl = ta.getBoolean(R.styleable.EmailServiceInfo_defaultSsl, false);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,127 @@
/*
* 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.service;
import android.util.Log;
import com.android.email.FixedLengthInputStream;
import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapResponseParser;
import com.android.email.mail.store.imap.ImapString;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TempDirectory;
import com.android.emailcommon.utility.Utility;
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.
Log.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) {
Log.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.
Log.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();
}
}

View File

@ -19,5 +19,5 @@ package com.android.email.service;
/**
* This service needs to be declared separately from the base service
*/
public class LegacyImap2AuthenticatorService extends AuthenticatorService {
public class LegacyImapAuthenticatorService extends AuthenticatorService {
}

View File

@ -0,0 +1,20 @@
/*
* 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.service;
public class LegacyImapSyncAdapterService extends PopImapSyncAdapterService {
}

View File

@ -16,212 +16,5 @@
package com.android.email.service;
import android.accounts.OperationCanceledException;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import com.android.emailcommon.TempDirectory;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceProxy;
import java.util.ArrayList;
public class Pop3SyncAdapterService extends Service {
private static final String TAG = "Pop3SyncAdapterService";
private static SyncAdapterImpl sSyncAdapter = null;
private static final Object sSyncAdapterLock = new Object();
public Pop3SyncAdapterService() {
super();
}
private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
private Context mContext;
public SyncAdapterImpl(Context context) {
super(context, true /* autoInitialize */);
mContext = context;
}
@Override
public void onPerformSync(android.accounts.Account account, Bundle extras,
String authority, ContentProviderClient provider, SyncResult syncResult) {
try {
Pop3SyncAdapterService.performSync(mContext, account, extras,
authority, provider, syncResult);
} catch (OperationCanceledException e) {
}
}
}
@Override
public void onCreate() {
super.onCreate();
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new SyncAdapterImpl(getApplicationContext());
}
}
}
@Override
public IBinder onBind(Intent intent) {
return sSyncAdapter.getSyncAdapterBinder();
}
private static void sync(Context context, long mailboxId, SyncResult syncResult,
boolean uiRefresh) {
TempDirectory.setTempDirectory(context);
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
if (mailbox == null) return;
Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
if (account == null) return;
ContentResolver resolver = context.getContentResolver();
if ((mailbox.mType != Mailbox.TYPE_OUTBOX) && (mailbox.mType != Mailbox.TYPE_INBOX)) {
// This is an update to a message in a non-syncing mailbox; delete this from the
// updates table and return
resolver.delete(Message.UPDATED_CONTENT_URI, Message.MAILBOX_KEY + "=?",
new String[] {Long.toString(mailbox.mId)});
return;
}
Log.d(TAG, "Mailbox: " + mailbox.mDisplayName);
Uri mailboxUri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
ContentValues values = new ContentValues();
// Set mailbox sync state
values.put(Mailbox.UI_SYNC_STATUS,
uiRefresh ? EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND);
resolver.update(mailboxUri, values, null, null);
try {
try {
if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
EmailServiceStub.sendMailImpl(context, account.mId);
} else {
Pop3Service.synchronizeMailboxSynchronous(context, account, mailbox);
}
} catch (MessagingException e) {
int cause = e.getExceptionType();
switch(cause) {
case MessagingException.IOERROR:
syncResult.stats.numIoExceptions++;
break;
case MessagingException.AUTHENTICATION_FAILED:
syncResult.stats.numAuthExceptions++;
break;
}
}
} finally {
// Always clear our sync state
values.put(Mailbox.UI_SYNC_STATUS, EmailContent.SYNC_STATUS_NONE);
resolver.update(mailboxUri, values, null, null);
}
}
/**
* Partial integration with system SyncManager; we initiate manual syncs upon request
*/
private static void performSync(Context context, android.accounts.Account account,
Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult)
throws OperationCanceledException {
// Find an EmailProvider account with the Account's email address
Cursor c = null;
try {
c = provider.query(com.android.emailcommon.provider.Account.CONTENT_URI,
Account.CONTENT_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
new String[] {account.name}, null);
if (c != null && c.moveToNext()) {
Account acct = new Account();
acct.restore(c);
if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
Log.d(TAG, "Upload sync request for " + acct.mDisplayName);
// See if any boxes have mail...
ArrayList<Long> mailboxesToUpdate;
Cursor updatesCursor = provider.query(Message.UPDATED_CONTENT_URI,
new String[] {Message.MAILBOX_KEY},
Message.ACCOUNT_KEY + "=?",
new String[] {Long.toString(acct.mId)},
null);
try {
if ((updatesCursor == null) || (updatesCursor.getCount() == 0)) return;
mailboxesToUpdate = new ArrayList<Long>();
while (updatesCursor.moveToNext()) {
Long mailboxId = updatesCursor.getLong(0);
if (!mailboxesToUpdate.contains(mailboxId)) {
mailboxesToUpdate.add(mailboxId);
}
}
} finally {
if (updatesCursor != null) {
updatesCursor.close();
}
}
for (long mailboxId: mailboxesToUpdate) {
sync(context, mailboxId, syncResult, false);
}
} else {
Log.d(TAG, "Sync request for " + acct.mDisplayName);
Log.d(TAG, extras.toString());
long mailboxId = extras.getLong(EmailServiceStub.SYNC_EXTRA_MAILBOX_ID,
Mailbox.NO_MAILBOX);
boolean isInbox = false;
if (mailboxId == Mailbox.NO_MAILBOX) {
mailboxId = Mailbox.findMailboxOfType(context, acct.mId,
Mailbox.TYPE_INBOX);
if (mailboxId == Mailbox.NO_MAILBOX) {
// Update folders?
EmailServiceProxy service =
EmailServiceUtils.getServiceForAccount(context, null, acct.mId);
service.updateFolderList(acct.mId);
}
isInbox = true;
}
if (mailboxId == Mailbox.NO_MAILBOX) return;
boolean uiRefresh =
extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false);
sync(context, mailboxId, syncResult, uiRefresh);
// Outbox is a special case here
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
return;
}
// Convert from minutes to seconds
int syncFrequency = acct.mSyncInterval * 60;
// Values < 0 are for "never" or "push"; 0 is undefined
if (syncFrequency <= 0) return;
Bundle ex = new Bundle();
if (!isInbox) {
ex.putLong(EmailServiceStub.SYNC_EXTRA_MAILBOX_ID, mailboxId);
}
Log.d(TAG, "Setting periodic sync for " + acct.mDisplayName + ": " +
syncFrequency + " seconds");
ContentResolver.addPeriodicSync(account, authority, ex, syncFrequency);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (c != null) {
c.close();
}
}
}
public class Pop3SyncAdapterService extends PopImapSyncAdapterService {
}

View File

@ -0,0 +1,252 @@
/*
* 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.service;
import android.accounts.OperationCanceledException;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import com.android.email.R;
import com.android.emailcommon.TempDirectory;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceProxy;
import java.util.ArrayList;
public class PopImapSyncAdapterService extends Service {
private static final String TAG = "PopImapSyncAdapterService";
private SyncAdapterImpl mSyncAdapter = null;
private static final Object sSyncAdapterLock = new Object();
public PopImapSyncAdapterService() {
super();
}
private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
private Context mContext;
public SyncAdapterImpl(Context context) {
super(context, true /* autoInitialize */);
mContext = context;
}
@Override
public void onPerformSync(android.accounts.Account account, Bundle extras,
String authority, ContentProviderClient provider, SyncResult syncResult) {
try {
PopImapSyncAdapterService.performSync(mContext, account, extras,
authority, provider, syncResult);
} catch (OperationCanceledException e) {
}
}
}
@Override
public void onCreate() {
super.onCreate();
synchronized (sSyncAdapterLock) {
mSyncAdapter = new SyncAdapterImpl(getApplicationContext());
}
}
@Override
public IBinder onBind(Intent intent) {
return mSyncAdapter.getSyncAdapterBinder();
}
/**
* @return whether or not this mailbox retrieves its data from the server (as opposed to just
* a local mailbox that is never synced).
*/
private static boolean loadsFromServer(Context context, Mailbox m, String protocol) {
String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap);
if (legacyImapProtocol.equals(protocol)) {
// TODO: actually use a sync flag when creating the mailboxes. Right now we use an
// approximation for IMAP.
return m.mType != Mailbox.TYPE_DRAFTS
&& m.mType != Mailbox.TYPE_OUTBOX
&& m.mType != Mailbox.TYPE_SEARCH;
} else if (HostAuth.LEGACY_SCHEME_POP3.equals(protocol)) {
return Mailbox.TYPE_INBOX == m.mType;
}
return false;
}
private static void sync(Context context, long mailboxId, SyncResult syncResult,
boolean uiRefresh) {
TempDirectory.setTempDirectory(context);
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
if (mailbox == null) return;
Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
if (account == null) return;
ContentResolver resolver = context.getContentResolver();
String protocol = account.getProtocol(context);
if ((mailbox.mType != Mailbox.TYPE_OUTBOX) &&
!loadsFromServer(context, mailbox, protocol)) {
// This is an update to a message in a non-syncing mailbox; delete this from the
// updates table and return
resolver.delete(Message.UPDATED_CONTENT_URI, Message.MAILBOX_KEY + "=?",
new String[] {Long.toString(mailbox.mId)});
return;
}
Log.d(TAG, "Mailbox: " + mailbox.mDisplayName);
Uri mailboxUri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
ContentValues values = new ContentValues();
// Set mailbox sync state
values.put(Mailbox.UI_SYNC_STATUS,
uiRefresh ? EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND);
resolver.update(mailboxUri, values, null, null);
try {
try {
String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap);
if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
EmailServiceStub.sendMailImpl(context, account.mId);
} else if (protocol.equals(legacyImapProtocol)) {
ImapService.synchronizeMailboxSynchronous(context, account, mailbox);
} else {
Pop3Service.synchronizeMailboxSynchronous(context, account, mailbox);
}
} catch (MessagingException e) {
int cause = e.getExceptionType();
switch(cause) {
case MessagingException.IOERROR:
syncResult.stats.numIoExceptions++;
break;
case MessagingException.AUTHENTICATION_FAILED:
syncResult.stats.numAuthExceptions++;
break;
}
}
} finally {
// Always clear our sync state
values.put(Mailbox.UI_SYNC_STATUS, EmailContent.SYNC_STATUS_NONE);
resolver.update(mailboxUri, values, null, null);
}
}
/**
* Partial integration with system SyncManager; we initiate manual syncs upon request
*/
private static void performSync(Context context, android.accounts.Account account,
Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult)
throws OperationCanceledException {
// Find an EmailProvider account with the Account's email address
Cursor c = null;
try {
c = provider.query(com.android.emailcommon.provider.Account.CONTENT_URI,
Account.CONTENT_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
new String[] {account.name}, null);
if (c != null && c.moveToNext()) {
Account acct = new Account();
acct.restore(c);
if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
Log.d(TAG, "Upload sync request for " + acct.mDisplayName);
// See if any boxes have mail...
ArrayList<Long> mailboxesToUpdate;
Cursor updatesCursor = provider.query(Message.UPDATED_CONTENT_URI,
new String[] {Message.MAILBOX_KEY},
Message.ACCOUNT_KEY + "=?",
new String[] {Long.toString(acct.mId)},
null);
try {
if ((updatesCursor == null) || (updatesCursor.getCount() == 0)) return;
mailboxesToUpdate = new ArrayList<Long>();
while (updatesCursor.moveToNext()) {
Long mailboxId = updatesCursor.getLong(0);
if (!mailboxesToUpdate.contains(mailboxId)) {
mailboxesToUpdate.add(mailboxId);
}
}
} finally {
if (updatesCursor != null) {
updatesCursor.close();
}
}
for (long mailboxId: mailboxesToUpdate) {
sync(context, mailboxId, syncResult, false);
}
} else {
Log.d(TAG, "Sync request for " + acct.mDisplayName);
Log.d(TAG, extras.toString());
long mailboxId = extras.getLong(EmailServiceStub.SYNC_EXTRA_MAILBOX_ID,
Mailbox.NO_MAILBOX);
boolean isInbox = false;
if (mailboxId == Mailbox.NO_MAILBOX) {
mailboxId = Mailbox.findMailboxOfType(context, acct.mId,
Mailbox.TYPE_INBOX);
if (mailboxId == Mailbox.NO_MAILBOX) {
// Update folders?
EmailServiceProxy service =
EmailServiceUtils.getServiceForAccount(context, null, acct.mId);
service.updateFolderList(acct.mId);
}
isInbox = true;
}
if (mailboxId == Mailbox.NO_MAILBOX) return;
boolean uiRefresh =
extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
sync(context, mailboxId, syncResult, uiRefresh);
// Outbox is a special case here
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
return;
}
// Convert from minutes to seconds
int syncFrequency = acct.mSyncInterval * 60;
// Values < 0 are for "never" or "push"; 0 is undefined
if (syncFrequency <= 0) return;
Bundle ex = new Bundle();
if (!isInbox) {
ex.putLong(EmailServiceStub.SYNC_EXTRA_MAILBOX_ID, mailboxId);
}
Log.d(TAG, "Setting periodic sync for " + acct.mDisplayName + ": " +
syncFrequency + " seconds");
ContentResolver.addPeriodicSync(account, authority, ex, syncFrequency);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (c != null) {
c.close();
}
}
}
}