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
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Before Width: | Height: | Size: 933 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 734 B |
Before Width: | Height: | Size: 604 B |
Before Width: | Height: | Size: 540 B |
Before Width: | Height: | Size: 629 B |
Before Width: | Height: | Size: 214 B |
Before Width: | Height: | Size: 617 B |
|
@ -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"/>
|
||||
|
|
|
@ -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]-->
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
/>
|
|
@ -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"
|
||||
/>
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 + "\"";
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|