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" />
|
android:resource="@xml/syncadapter_pop3" />
|
||||||
</service>
|
</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 -->
|
<!-- Require provider permission to use our Policy and Account services -->
|
||||||
<service
|
<service
|
||||||
android:name=".service.PolicyService"
|
android:name=".service.PolicyService"
|
||||||
|
@ -576,15 +587,19 @@
|
||||||
/>
|
/>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".imap2.EmailSyncAdapterService"
|
android:name=".service.LegacyImapAuthenticatorService"
|
||||||
android:exported="true">
|
android:exported="false"
|
||||||
|
android:enabled="true"
|
||||||
|
>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action
|
<action
|
||||||
android:name="android.content.SyncAdapter" />
|
android:name="android.accounts.AccountAuthenticator" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data android:name="android.content.SyncAdapter"
|
<meta-data
|
||||||
android:resource="@xml/syncadapter_imap" />
|
android:name="android.accounts.AccountAuthenticator"
|
||||||
|
android:resource="@xml/authenticator_legacy_imap"
|
||||||
|
/>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
@ -646,21 +661,6 @@
|
||||||
/>
|
/>
|
||||||
</service>
|
</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>
|
</application>
|
||||||
|
|
||||||
<!-- Legacy permissions, etc. can go here -->
|
<!-- Legacy permissions, etc. can go here -->
|
||||||
|
|
|
@ -17,12 +17,15 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<!-- DO NOT TRANSLATE THESE STRINGS -->
|
<!-- DO NOT TRANSLATE THESE STRINGS -->
|
||||||
<string name="account_manager_type_exchange" translatable="false">com.android.exchange</string>
|
<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_pop3" translatable="false">com.android.email</string>
|
||||||
<string name="account_manager_type_imap" translatable="false">com.android.imap</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_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="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="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_imap" translatable="false">imap</string>
|
||||||
<string name="protocol_pop3" translatable="false">pop3</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>
|
<string name="application_mime_type" translatable="false">application/email-ls</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -50,8 +50,8 @@
|
||||||
<emailservices xmlns:email="http://schemas.android.com/apk/res/com.android.email">
|
<emailservices xmlns:email="http://schemas.android.com/apk/res/com.android.email">
|
||||||
<emailservice
|
<emailservice
|
||||||
email:protocol="pop3"
|
email:protocol="pop3"
|
||||||
email:name="POP3"
|
email:name="@string/pop3_name"
|
||||||
email:accountType="com.android.email"
|
email:accountType="@string/account_manager_type_pop3"
|
||||||
email:serviceClass="com.android.email.service.Pop3Service"
|
email:serviceClass="com.android.email.service.Pop3Service"
|
||||||
email:port="110"
|
email:port="110"
|
||||||
email:portSsl="995"
|
email:portSsl="995"
|
||||||
|
@ -66,17 +66,16 @@
|
||||||
email:offerLoadMore="true"
|
email:offerLoadMore="true"
|
||||||
/>
|
/>
|
||||||
<emailservice
|
<emailservice
|
||||||
email:protocol="imap2"
|
email:protocol="imap"
|
||||||
email:name="IMAP"
|
email:name="@string/imap_name"
|
||||||
email:accountType="com.android.imap2"
|
email:accountType="@string/account_manager_type_imap"
|
||||||
email:serviceClass="com.android.email.imap2.Imap2SyncManager"
|
email:serviceClass="com.android.email.service.ImapService"
|
||||||
email:port="143"
|
email:port="143"
|
||||||
email:portSsl="993"
|
email:portSsl="993"
|
||||||
email:syncIntervalStrings="@array/account_settings_check_frequency_entries_push"
|
email:syncIntervalStrings="@array/account_settings_check_frequency_entries"
|
||||||
email:syncIntervals="@array/account_settings_check_frequency_values_push"
|
email:syncIntervals="@array/account_settings_check_frequency_values"
|
||||||
email:defaultSyncInterval="push"
|
email:defaultSyncInterval="mins15"
|
||||||
|
|
||||||
email:offerLookback="true"
|
|
||||||
email:offerTls="true"
|
email:offerTls="true"
|
||||||
email:usesSmtp="true"
|
email:usesSmtp="true"
|
||||||
email:offerAttachmentPreload="true"
|
email:offerAttachmentPreload="true"
|
||||||
|
@ -84,12 +83,11 @@
|
||||||
email:syncChanges="true"
|
email:syncChanges="true"
|
||||||
email:inferPrefix="imap"
|
email:inferPrefix="imap"
|
||||||
email:offerLoadMore="true"
|
email:offerLoadMore="true"
|
||||||
email:requiresSetup="true"
|
|
||||||
/>
|
/>
|
||||||
<emailservice
|
<emailservice
|
||||||
email:protocol="eas"
|
email:protocol="@string/protocol_eas"
|
||||||
email:name="Exchange"
|
email:name="Exchange"
|
||||||
email:accountType="com.android.exchange"
|
email:accountType="@string/account_manager_type_exchange"
|
||||||
email:intent="com.android.email.EXCHANGE_INTENT"
|
email:intent="com.android.email.EXCHANGE_INTENT"
|
||||||
email:port="80"
|
email:port="80"
|
||||||
email:portSsl="443"
|
email:portSsl="443"
|
||||||
|
@ -107,9 +105,4 @@
|
||||||
email:syncContacts="true"
|
email:syncContacts="true"
|
||||||
email:syncCalendar="true"
|
email:syncCalendar="true"
|
||||||
/>
|
/>
|
||||||
<emailservice
|
|
||||||
email:protocol="imap"
|
|
||||||
email:accountType="com.android.email"
|
|
||||||
email:replaceWith="imap2"
|
|
||||||
/>
|
|
||||||
</emailservices>
|
</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">
|
<declare-styleable name="EmailServiceInfo">
|
||||||
<attr name="protocol" format="string"/>
|
<attr name="protocol" format="string"/>
|
||||||
<attr name="name" format="string"/>
|
<attr name="name" format="string"/>
|
||||||
|
<attr name="hide" format="boolean"/>
|
||||||
<attr name="accountType" format="string"/>
|
<attr name="accountType" format="string"/>
|
||||||
<attr name="replaceWith" format="string"/>
|
<attr name="replaceWith" format="string"/>
|
||||||
<attr name="serviceClass" 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>
|
<string name="no_conversations">No messages.</string>
|
||||||
|
|
||||||
<!-- Used by AccountManager -->
|
<!-- Used by AccountManager -->
|
||||||
<string name="imap2_name" translatable="false">IMAP</string>
|
<string name="imap_name">IMAP</string>
|
||||||
<string name="pop3_name" translatable="false">POP3</string>
|
<string name="pop3_name">POP3</string>
|
||||||
|
|
||||||
<string name="folder_picker_title">Folder picker</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]-->
|
<!-- 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:accountType="@string/account_manager_type_imap"
|
||||||
android:icon="@mipmap/ic_launcher_mail"
|
android:icon="@mipmap/ic_launcher_mail"
|
||||||
android:smallIcon="@drawable/stat_notify_email_generic"
|
android:smallIcon="@drawable/stat_notify_email_generic"
|
||||||
android:label="@string/imap2_name"
|
android:label="@string/imap_name"
|
||||||
android:accountPreferences="@xml/account_preferences"
|
android:accountPreferences="@xml/account_preferences"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,9 +21,9 @@
|
||||||
<!-- for the Account Manager. -->
|
<!-- for the Account Manager. -->
|
||||||
|
|
||||||
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
|
<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:icon="@mipmap/ic_launcher_mail"
|
||||||
android:smallIcon="@drawable/stat_notify_email_generic"
|
android:smallIcon="@drawable/stat_notify_email_generic"
|
||||||
android:label="@string/exchange_name"
|
android:label="@string/imap_name"
|
||||||
android:accountPreferences="@xml/account_preferences"
|
android:accountPreferences="@xml/account_preferences"
|
||||||
/>
|
/>
|
|
@ -22,6 +22,6 @@
|
||||||
|
|
||||||
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:contentAuthority="@string/authority_email_provider"
|
android:contentAuthority="@string/authority_email_provider"
|
||||||
android:accountType="@string/account_manager_type_imap"
|
android:accountType="@string/account_manager_type_legacy_imap"
|
||||||
android:supportsUploading="true"
|
android:supportsUploading="true"
|
||||||
/>
|
/>
|
|
@ -80,7 +80,8 @@ public class AccountSetupType extends AccountSetupActivity implements OnClickLis
|
||||||
for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(this)) {
|
for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(this)) {
|
||||||
if (EmailServiceUtils.isServiceAvailable(this, info.protocol)) {
|
if (EmailServiceUtils.isServiceAvailable(this, info.protocol)) {
|
||||||
// If we're looking for a specific account type, reject others
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
LayoutInflater.from(this).inflate(R.layout.account_type, parent);
|
LayoutInflater.from(this).inflate(R.layout.account_type, parent);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.android.email.R;
|
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.Pop3Store;
|
||||||
import com.android.email.mail.store.ServiceStore;
|
import com.android.email.mail.store.ServiceStore;
|
||||||
import com.android.email.mail.transport.MailTransport;
|
import com.android.email.mail.transport.MailTransport;
|
||||||
|
@ -84,6 +85,7 @@ public abstract class Store {
|
||||||
throws MessagingException {
|
throws MessagingException {
|
||||||
if (sStores.isEmpty()) {
|
if (sStores.isEmpty()) {
|
||||||
sStoreClasses.put(context.getString(R.string.protocol_pop3), Pop3Store.class);
|
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);
|
HostAuth hostAuth = account.getOrCreateHostAuthRecv(context);
|
||||||
// An existing account might have been deleted
|
// 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.NotificationController;
|
||||||
import com.android.email.Preferences;
|
import com.android.email.Preferences;
|
||||||
|
import com.android.email.R;
|
||||||
import com.android.email.SecurityPolicy;
|
import com.android.email.SecurityPolicy;
|
||||||
import com.android.email.activity.setup.AccountSettings;
|
import com.android.email.activity.setup.AccountSettings;
|
||||||
import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
|
|
||||||
import com.android.emailcommon.Logging;
|
import com.android.emailcommon.Logging;
|
||||||
import com.android.emailcommon.VendorPolicyLoader;
|
import com.android.emailcommon.VendorPolicyLoader;
|
||||||
import com.android.emailcommon.provider.Account;
|
import com.android.emailcommon.provider.Account;
|
||||||
import com.android.emailcommon.provider.EmailContent.AccountColumns;
|
import com.android.emailcommon.provider.EmailContent.AccountColumns;
|
||||||
import com.android.emailcommon.provider.HostAuth;
|
import com.android.emailcommon.provider.HostAuth;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service that really handles broadcast intents on a worker thread.
|
* The service that really handles broadcast intents on a worker thread.
|
||||||
*
|
*
|
||||||
|
@ -185,7 +183,8 @@ public class EmailBroadcastProcessorService extends IntentService {
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
long recvAuthKey = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
|
long recvAuthKey = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
|
||||||
HostAuth recvAuth = HostAuth.restoreHostAuthWithId(context, recvAuthKey);
|
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);
|
int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN);
|
||||||
flags &= ~Account.FLAGS_DELETE_POLICY_MASK;
|
flags &= ~Account.FLAGS_DELETE_POLICY_MASK;
|
||||||
flags |= Account.DELETE_POLICY_ON_DELETE << Account.FLAGS_DELETE_POLICY_SHIFT;
|
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.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Debug;
|
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.CalendarContract;
|
import android.provider.CalendarContract;
|
||||||
|
@ -178,7 +177,9 @@ public class EmailServiceUtils {
|
||||||
public boolean requiresAccountUpdate;
|
public boolean requiresAccountUpdate;
|
||||||
public boolean offerLoadMore;
|
public boolean offerLoadMore;
|
||||||
public boolean requiresSetup;
|
public boolean requiresSetup;
|
||||||
|
public boolean hide;
|
||||||
|
|
||||||
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder sb = new StringBuilder("Protocol: ");
|
StringBuilder sb = new StringBuilder("Protocol: ");
|
||||||
sb.append(protocol);
|
sb.append(protocol);
|
||||||
|
@ -288,7 +289,6 @@ public class EmailServiceUtils {
|
||||||
protected Void doInBackground(Void... params) {
|
protected Void doInBackground(Void... params) {
|
||||||
disableComponent(mContext, LegacyEmailAuthenticatorService.class);
|
disableComponent(mContext, LegacyEmailAuthenticatorService.class);
|
||||||
disableComponent(mContext, LegacyEasAuthenticatorService.class);
|
disableComponent(mContext, LegacyEasAuthenticatorService.class);
|
||||||
disableComponent(mContext, LegacyImap2AuthenticatorService.class);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -498,6 +498,7 @@ public class EmailServiceUtils {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
info.name = ta.getString(R.styleable.EmailServiceInfo_name);
|
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);
|
String klass = ta.getString(R.styleable.EmailServiceInfo_serviceClass);
|
||||||
info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent);
|
info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent);
|
||||||
info.defaultSsl = ta.getBoolean(R.styleable.EmailServiceInfo_defaultSsl, false);
|
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
|
* 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;
|
package com.android.email.service;
|
||||||
|
|
||||||
import android.accounts.OperationCanceledException;
|
public class Pop3SyncAdapterService extends PopImapSyncAdapterService {
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|