From c6089bc01f2ae49fb11904a4b4f222811358254f Mon Sep 17 00:00:00 2001 From: Marc Blank Date: Fri, 29 Jun 2012 09:42:05 -0700 Subject: [PATCH] Initial Imap2 implementation This CL includes the following: * New Imap2.apk generation (not included in builds) * "Push IMAP" option for accounts when Imap2.apk present * Account creation/setup * 2-way sync of messages, deletions, flag updates * Push (messages, flags) * Folder list hierarchy handling * Message text (one plain or html part) * Picker UI for trash folder (placeholder) * Capabilities handling/UI command Major Imap2 new features: * Push * Multiple folder sync * Sync window (like EAS) TODO: * Picker UI for sent folder * Upload of sent messages to server * Search * Multiple viewable parts * Probably lots more, incl. unit tests Change-Id: Ia5d74073d9c307e0bdae72a7f76b27140dde7d14 --- AndroidManifest.xml | 21 +- .../emailcommon}/VendorPolicyLoader.java | 50 +- .../emailcommon/provider/EmailContent.java | 53 +- .../provider/MailboxUtilities.java | 274 +++ .../service/EmailServiceCallback.java | 117 + .../emailcommon/service/SyncWindow.java | 21 + .../emailsync/EmailSyncAlarmReceiver.java | 4 +- .../emailsync/MailboxAlarmReceiver.java | 8 +- .../android/emailsync/MessageMoveRequest.java | 42 + .../com/android/emailsync/PartRequest.java | 2 +- ...ncServiceManager.java => SyncManager.java} | 79 +- imap2/Android.mk | 36 + imap2/AndroidManifest.xml | 113 + imap2/res/mipmap-hdpi/icon.png | Bin 0 -> 7010 bytes imap2/res/mipmap-mdpi/icon.png | Bin 0 -> 3846 bytes imap2/res/mipmap-xhdpi/icon.png | Bin 0 -> 10781 bytes imap2/res/values/strings.xml | 37 + imap2/res/xml/syncadapter_email.xml | 27 + .../com/android/imap2/AttachmentLoader.java | 197 ++ .../imap2/BroadcastProcessorService.java | 82 + .../imap2/EmailSyncAdapterService.java | 122 + imap2/src/com/android/imap2/Imap2.java | 23 + .../android/imap2/Imap2BroadcastReceiver.java | 31 + .../com/android/imap2/Imap2SyncManager.java | 325 +++ .../com/android/imap2/Imap2SyncService.java | 2056 +++++++++++++++++ imap2/src/com/android/imap2/ImapId.java | 189 ++ .../com/android/imap2/ImapInputStream.java | 48 + imap2/src/com/android/imap2/Parser.java | 202 ++ .../com/android/imap2/QuotedPrintable.java | 121 + res/values/strings.xml | 8 + res/xml/imap2_authenticator.xml | 29 + res/xml/providers.xml | 5 + res/xml/services.xml | 17 + .../activity/setup/AccountSettingsUtils.java | 47 +- .../activity/setup/AccountSetupBasics.java | 2 +- .../activity/setup/AccountSetupOptions.java | 2 +- .../android/email/mail/store/ImapStore.java | 6 +- src/com/android/email/provider/DBHelper.java | 30 +- .../android/email/provider/EmailProvider.java | 88 +- .../email/provider/FolderPickerActivity.java | 75 + .../email/provider/FolderPickerCallback.java | 25 + .../email/provider/FolderSelectionDialog.java | 145 ++ .../android/email/service/AccountService.java | 2 +- .../EmailBroadcastProcessorService.java | 2 +- .../service/Imap2AuthenticatorService.java | 23 + .../android/email/service/ImapService.java | 104 +- src/com/beetstra/ThirdPartyProject.prop | 9 - 47 files changed, 4651 insertions(+), 248 deletions(-) rename {src/com/android/email => emailcommon/src/com/android/emailcommon}/VendorPolicyLoader.java (82%) create mode 100644 emailcommon/src/com/android/emailcommon/provider/MailboxUtilities.java create mode 100644 emailcommon/src/com/android/emailcommon/service/EmailServiceCallback.java create mode 100644 emailsync/src/com/android/emailsync/MessageMoveRequest.java rename emailsync/src/com/android/emailsync/{SyncServiceManager.java => SyncManager.java} (97%) create mode 100644 imap2/Android.mk create mode 100644 imap2/AndroidManifest.xml create mode 100644 imap2/res/mipmap-hdpi/icon.png create mode 100644 imap2/res/mipmap-mdpi/icon.png create mode 100644 imap2/res/mipmap-xhdpi/icon.png create mode 100644 imap2/res/values/strings.xml create mode 100644 imap2/res/xml/syncadapter_email.xml create mode 100644 imap2/src/com/android/imap2/AttachmentLoader.java create mode 100644 imap2/src/com/android/imap2/BroadcastProcessorService.java create mode 100644 imap2/src/com/android/imap2/EmailSyncAdapterService.java create mode 100644 imap2/src/com/android/imap2/Imap2.java create mode 100644 imap2/src/com/android/imap2/Imap2BroadcastReceiver.java create mode 100644 imap2/src/com/android/imap2/Imap2SyncManager.java create mode 100644 imap2/src/com/android/imap2/Imap2SyncService.java create mode 100644 imap2/src/com/android/imap2/ImapId.java create mode 100644 imap2/src/com/android/imap2/ImapInputStream.java create mode 100644 imap2/src/com/android/imap2/Parser.java create mode 100644 imap2/src/com/android/imap2/QuotedPrintable.java create mode 100644 res/xml/imap2_authenticator.xml create mode 100644 src/com/android/email/provider/FolderPickerActivity.java create mode 100644 src/com/android/email/provider/FolderPickerCallback.java create mode 100644 src/com/android/email/provider/FolderSelectionDialog.java create mode 100644 src/com/android/email/service/Imap2AuthenticatorService.java delete mode 100644 src/com/beetstra/ThirdPartyProject.prop diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e3643ec75..04e625af5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -198,6 +198,12 @@ + + + - + + + + + + mCallbackList; + + public EmailServiceCallback(RemoteCallbackList callbackList) { + mCallbackList = callbackList; + } + /** + * Broadcast a callback to the everyone that's registered + * + * @param wrapper the ServiceCallbackWrapper used in the broadcast + */ + private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { + RemoteCallbackList callbackList = mCallbackList; + if (callbackList != null) { + // Call everyone on our callback list + int count = callbackList.beginBroadcast(); + try { + for (int i = 0; i < count; i++) { + try { + wrapper.call(callbackList.getBroadcastItem(i)); + } catch (RemoteException e) { + // Safe to ignore + } catch (RuntimeException e) { + // We don't want an exception in one call to prevent other calls, so + // we'll just log this and continue + Log.e("EmailServiceCallback", "Caught RuntimeException in broadcast", e); + } + } + } finally { + // No matter what, we need to finish the broadcast + callbackList.finishBroadcast(); + } + } + } + + @Override + public void loadAttachmentStatus(final long messageId, final long attachmentId, + final int status, final int progress) { + broadcastCallback(new ServiceCallbackWrapper() { + @Override + public void call(IEmailServiceCallback cb) throws RemoteException { + cb.loadAttachmentStatus(messageId, attachmentId, status, progress); + } + }); + } + + @Override + public void loadMessageStatus(final long messageId, final int status, final int progress) { + broadcastCallback(new ServiceCallbackWrapper() { + @Override + public void call(IEmailServiceCallback cb) throws RemoteException { + cb.loadMessageStatus(messageId, status, progress); + } + }); + } + + @Override + public void sendMessageStatus(final long messageId, final String subject, final int status, + final int progress) { + broadcastCallback(new ServiceCallbackWrapper() { + @Override + public void call(IEmailServiceCallback cb) throws RemoteException { + cb.sendMessageStatus(messageId, subject, status, progress); + } + }); + } + + @Override + public void syncMailboxListStatus(final long accountId, final int status, + final int progress) { + broadcastCallback(new ServiceCallbackWrapper() { + @Override + public void call(IEmailServiceCallback cb) throws RemoteException { + cb.syncMailboxListStatus(accountId, status, progress); + } + }); + } + + @Override + public void syncMailboxStatus(final long mailboxId, final int status, + final int progress) { + broadcastCallback(new ServiceCallbackWrapper() { + @Override + public void call(IEmailServiceCallback cb) throws RemoteException { + cb.syncMailboxStatus(mailboxId, status, progress); + } + }); + } + + private interface ServiceCallbackWrapper { + public void call(IEmailServiceCallback cb) throws RemoteException; + } +} diff --git a/emailcommon/src/com/android/emailcommon/service/SyncWindow.java b/emailcommon/src/com/android/emailcommon/service/SyncWindow.java index 52839b204..3863e4f3f 100644 --- a/emailcommon/src/com/android/emailcommon/service/SyncWindow.java +++ b/emailcommon/src/com/android/emailcommon/service/SyncWindow.java @@ -26,4 +26,25 @@ public class SyncWindow { public static final int SYNC_WINDOW_2_WEEKS = 4; public static final int SYNC_WINDOW_1_MONTH = 5; public static final int SYNC_WINDOW_ALL = 6; + + public static int toDays(int window) { + switch(window) { + case SYNC_WINDOW_1_DAY: + return 1; + case SYNC_WINDOW_3_DAYS: + return 3; + case SYNC_WINDOW_1_WEEK: + return 7; + case SYNC_WINDOW_2_WEEKS: + return 14; + case SYNC_WINDOW_1_MONTH: + return 30; + case SYNC_WINDOW_ALL: + return 365*10; + case SYNC_WINDOW_UNKNOWN: + case SYNC_WINDOW_AUTO: + default: + return 14; + } + } } diff --git a/emailsync/src/com/android/emailsync/EmailSyncAlarmReceiver.java b/emailsync/src/com/android/emailsync/EmailSyncAlarmReceiver.java index 639c9cfc2..8ac0f0633 100644 --- a/emailsync/src/com/android/emailsync/EmailSyncAlarmReceiver.java +++ b/emailsync/src/com/android/emailsync/EmailSyncAlarmReceiver.java @@ -65,7 +65,7 @@ public class EmailSyncAlarmReceiver extends BroadcastReceiver { ContentResolver cr = context.getContentResolver(); // Get a selector for EAS accounts (we don't want to sync on changes to POP/IMAP messages) - String selector = SyncServiceManager.getAccountSelector(); + String selector = SyncManager.getAccountSelector(); try { // Find all of the deletions @@ -102,7 +102,7 @@ public class EmailSyncAlarmReceiver extends BroadcastReceiver { // Request service from the mailbox for (Long mailboxId: mailboxesToNotify) { - SyncServiceManager.serviceRequest(mailboxId, SyncServiceManager.SYNC_UPSYNC); + SyncManager.serviceRequest(mailboxId, SyncManager.SYNC_UPSYNC); } } catch (ProviderUnavailableException e) { Log.e("EmailSyncAlarmReceiver", "EmailProvider unavailable; aborting alarm receiver"); diff --git a/emailsync/src/com/android/emailsync/MailboxAlarmReceiver.java b/emailsync/src/com/android/emailsync/MailboxAlarmReceiver.java index 17a2590eb..810579924 100644 --- a/emailsync/src/com/android/emailsync/MailboxAlarmReceiver.java +++ b/emailsync/src/com/android/emailsync/MailboxAlarmReceiver.java @@ -30,12 +30,12 @@ import android.content.Intent; public class MailboxAlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - long mailboxId = intent.getLongExtra("mailbox", SyncServiceManager.EXTRA_MAILBOX_ID); + long mailboxId = intent.getLongExtra("mailbox", SyncManager.EXTRA_MAILBOX_ID); // EXCHANGE_SERVICE_MAILBOX_ID tells us that the service is asking to be started - if (mailboxId == SyncServiceManager.SYNC_SERVICE_MAILBOX_ID) { - context.startService(new Intent(context, SyncServiceManager.class)); + if (mailboxId == SyncManager.SYNC_SERVICE_MAILBOX_ID) { + context.startService(new Intent(context, SyncManager.class)); } else { - SyncServiceManager.alert(context, mailboxId); + SyncManager.alert(context, mailboxId); } } } diff --git a/emailsync/src/com/android/emailsync/MessageMoveRequest.java b/emailsync/src/com/android/emailsync/MessageMoveRequest.java new file mode 100644 index 000000000..3f783a40d --- /dev/null +++ b/emailsync/src/com/android/emailsync/MessageMoveRequest.java @@ -0,0 +1,42 @@ +/* + * 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.emailsync; + +import com.android.emailsync.Request; + +/** + * MessageMoveRequest is the EAS wrapper for requesting a "move to folder" + */ +public class MessageMoveRequest extends Request { + public final long mMailboxId; + + public MessageMoveRequest(long messageId, long mailboxId) { + super(messageId); + mMailboxId = mailboxId; + } + + // MessageMoveRequests are unique by their message id (i.e. it's meaningless to have two + // separate message moves queued at the same time) + public boolean equals(Object o) { + if (!(o instanceof MessageMoveRequest)) return false; + return ((MessageMoveRequest)o).mMessageId == mMessageId; + } + + public int hashCode() { + return (int)mMessageId; + } +} diff --git a/emailsync/src/com/android/emailsync/PartRequest.java b/emailsync/src/com/android/emailsync/PartRequest.java index 955f62d87..ce0070f18 100644 --- a/emailsync/src/com/android/emailsync/PartRequest.java +++ b/emailsync/src/com/android/emailsync/PartRequest.java @@ -20,7 +20,7 @@ package com.android.emailsync; import com.android.emailcommon.provider.EmailContent.Attachment; /** - * PartRequest is the EAS wrapper for attachment loading requests. In addition to information about + * PartRequest is the wrapper for attachment loading requests. In addition to information about * the attachment to be loaded, it also contains the callback to be used for status/progress * updates to the UI. */ diff --git a/emailsync/src/com/android/emailsync/SyncServiceManager.java b/emailsync/src/com/android/emailsync/SyncManager.java similarity index 97% rename from emailsync/src/com/android/emailsync/SyncServiceManager.java rename to emailsync/src/com/android/emailsync/SyncManager.java index 4a0efcdce..080dc984d 100644 --- a/emailsync/src/com/android/emailsync/SyncServiceManager.java +++ b/emailsync/src/com/android/emailsync/SyncManager.java @@ -61,7 +61,6 @@ import com.android.emailcommon.provider.ProviderUnavailableException; import com.android.emailcommon.service.AccountServiceProxy; import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.EmailServiceStatus; -import com.android.emailcommon.service.IEmailServiceCallback; import com.android.emailcommon.service.IEmailServiceCallback.Stub; import com.android.emailcommon.service.PolicyServiceProxy; import com.android.emailcommon.utility.EmailAsyncTask; @@ -88,7 +87,7 @@ import java.util.concurrent.ConcurrentHashMap; * order to maintain proper 2-way syncing of data. (More documentation to follow) * */ -public abstract class SyncServiceManager extends Service implements Runnable { +public abstract class SyncManager extends Service implements Runnable { private static final String TAG = "SyncServiceManager"; @@ -201,7 +200,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { public ContentResolver mResolver; // The singleton SyncServiceManager object, with its thread and stop flag - protected static SyncServiceManager INSTANCE; + protected static SyncManager INSTANCE; protected static Thread sServiceThread = null; // Cached unique device id protected static String sDeviceId = null; @@ -338,7 +337,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } public static String getAccountSelector() { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm == null) return ""; return ssm.getAccountsSelector(); } @@ -380,8 +379,8 @@ public abstract class SyncServiceManager extends Service implements Runnable { if (onSecurityHold(account)) { // If we're in a security hold, and our policies are active, release // the hold - if (PolicyServiceProxy.isActive(SyncServiceManager.this, null)) { - PolicyServiceProxy.setAccountHoldFlag(SyncServiceManager.this, + if (PolicyServiceProxy.isActive(SyncManager.this, null)) { + PolicyServiceProxy.setAccountHoldFlag(SyncManager.this, account, false); log("isActive true; release hold for " + account.mDisplayName); } @@ -440,7 +439,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { // The implication is that the account has been deleted; let's find out alwaysLog("Observer found deleted account: " + account.mDisplayName); // Run the reconciler (the reconciliation itself runs in the Email app) - runAccountReconcilerSync(SyncServiceManager.this); + runAccountReconcilerSync(SyncManager.this); // See if the account is still around Account deletedAccount = Account.restoreAccountWithId(context, account.mId); @@ -477,7 +476,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { // See if this account is no longer on security hold if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) { - releaseSyncHolds(SyncServiceManager.this, + releaseSyncHolds(SyncManager.this, AbstractSyncService.EXIT_SECURITY_FAILURE, account); } @@ -548,7 +547,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { * Unregister all CalendarObserver's */ static public void unregisterCalendarObservers() { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm == null) return; ContentResolver resolver = ssm.mResolver; for (CalendarObserver observer: ssm.mCalendarObservers.values()) { @@ -718,7 +717,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } static public Account getAccountById(long accountId) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { AccountList accountList = ssm.mAccountList; synchronized (accountList) { @@ -729,7 +728,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } static public Account getAccountByName(String accountName) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { AccountList accountList = ssm.mAccountList; synchronized (accountList) { @@ -795,7 +794,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { * @param account the account whose Mailboxes should be released from security hold */ static public void releaseSecurityHold(Account account) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { ssm.releaseSyncHolds(INSTANCE, AbstractSyncService.EXIT_SECURITY_FAILURE, account); @@ -910,7 +909,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } public static void stopAccountSyncs(long acctId) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { ssm.stopAccountSyncs(acctId, true); } @@ -957,7 +956,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { * @param acctId */ static public void stopNonAccountMailboxSyncsForAccount(long acctId) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { ssm.stopAccountSyncs(acctId, false); kick("reload folder list"); @@ -1051,7 +1050,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } static public void runAwake(long id) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { ssm.acquireWakeLock(id); ssm.clearAlarm(id); @@ -1059,7 +1058,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } static public void runAsleep(long id, long millis) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { ssm.setAlarm(id, millis); ssm.releaseWakeLock(id); @@ -1067,27 +1066,27 @@ public abstract class SyncServiceManager extends Service implements Runnable { } static public void clearWatchdogAlarm(long id) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { ssm.clearAlarm(id); } } static public void setWatchdogAlarm(long id, long millis) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { ssm.setAlarm(id, millis); } } static public void alert(Context context, final long id) { - final SyncServiceManager ssm = INSTANCE; + final SyncManager ssm = INSTANCE; checkSyncServiceManagerServiceRunning(); if (id < 0) { log("SyncServiceManager alert"); kick("ping SyncServiceManager"); } else if (ssm == null) { - context.startService(new Intent(context, SyncServiceManager.class)); + context.startService(new Intent(context, SyncManager.class)); } else { final AbstractSyncService service = ssm.mServiceMap.get(id); if (service != null) { @@ -1129,7 +1128,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } // Shutdown the connection manager; this should close all of our // sockets and generate IOExceptions all around. - SyncServiceManager.shutdownConnectionManager(); + SyncManager.shutdownConnectionManager(); } } }}, threadName).start(); @@ -1177,7 +1176,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { public void run() { synchronized (mAccountList) { for (Account account : mAccountList) - SyncServiceManager.stopAccountSyncs(account.mId); + SyncManager.stopAccountSyncs(account.mId); } }}); } @@ -1400,13 +1399,13 @@ public abstract class SyncServiceManager extends Service implements Runnable { try { synchronized (sSyncLock) { // SyncServiceManager cannot start unless we connect to AccountService - if (!new AccountServiceProxy(SyncServiceManager.this).test()) { + if (!new AccountServiceProxy(SyncManager.this).test()) { alwaysLog("!!! Email application not found; stopping self"); stopSelf(); } if (sDeviceId == null) { try { - String deviceId = getDeviceId(SyncServiceManager.this); + String deviceId = getDeviceId(SyncManager.this); if (deviceId != null) { sDeviceId = deviceId; } @@ -1430,7 +1429,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } // Run the reconciler and clean up mismatched accounts - if we weren't // running when accounts were deleted, it won't have been called. - runAccountReconcilerSync(SyncServiceManager.this); + runAccountReconcilerSync(SyncManager.this); // Update other services depending on final account configuration maybeStartSyncServiceManagerThread(); if (sServiceThread == null) { @@ -1450,7 +1449,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } public static void reconcileAccounts(Context context) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { ssm.runAccountReconcilerSync(context); } @@ -1504,11 +1503,11 @@ public abstract class SyncServiceManager extends Service implements Runnable { * com.android.email) and hasn't been restarted. See the comment for onCreate for details */ static void checkSyncServiceManagerServiceRunning() { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm == null) return; if (sServiceThread == null) { log("!!! checkSyncServiceManagerServiceRunning; starting service..."); - ssm.startService(new Intent(ssm, SyncServiceManager.class)); + ssm.startService(new Intent(ssm, SyncManager.class)); } } @@ -1598,7 +1597,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { // process starts running again, remote processes will be started again in due course Log.e(TAG, "EmailProvider unavailable; shutting down"); // Ask for our service to be restarted; this should kick-start the Email process as well - startService(new Intent(this, SyncServiceManager.class)); + startService(new Intent(this, SyncManager.class)); } catch (RuntimeException e) { // Crash; this is a completely unexpected runtime error Log.e(TAG, "RuntimeException in SyncServiceManager", e); @@ -1718,7 +1717,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { * @return whether or not the account can sync automatically */ /*package*/ public static boolean canAutoSync(Account account) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm == null) { return false; } @@ -1971,7 +1970,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } static public void serviceRequest(long mailboxId, long ms, int reason) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm == null) return; Mailbox m = Mailbox.restoreMailboxWithId(ssm, mailboxId); if (m == null || !isSyncable(m)) return; @@ -1989,7 +1988,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } static public void serviceRequestImmediate(long mailboxId) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm == null) return; AbstractSyncService service = ssm.mServiceMap.get(mailboxId); if (service != null) { @@ -2004,7 +2003,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } static public void sendMessageRequest(Request req) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; Message msg = Message.restoreMessageWithId(ssm, req.mMessageId); if (msg == null) return; long mailboxId = msg.mMailboxKey; @@ -2044,7 +2043,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { * @return whether or not the Mailbox is available for syncing (i.e. is a valid push target) */ static public int pingStatus(long mailboxId) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm == null) return PING_STATUS_OK; // Already syncing... if (ssm.mServiceMap.get(mailboxId) != null) { @@ -2063,7 +2062,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } static public void startManualSync(long mailboxId, int reason, Request req) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm == null) return; synchronized (sSyncLock) { AbstractSyncService svc = ssm.mServiceMap.get(mailboxId); @@ -2085,7 +2084,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP static public void stopManualSync(long mailboxId) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm == null) return; synchronized (sSyncLock) { AbstractSyncService svc = ssm.mServiceMap.get(mailboxId); @@ -2102,7 +2101,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { * Wake up SyncServiceManager to check for mailboxes needing service */ static public void kick(String reason) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { synchronized (ssm) { //INSTANCE.log("Kick: " + reason); @@ -2122,7 +2121,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { * @param mailboxId the id of the mailbox */ static public void removeFromSyncErrorMap(long mailboxId) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm != null) { ssm.mSyncErrorMap.remove(mailboxId); } @@ -2142,7 +2141,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { * @param svc the service that is finished */ static public void done(AbstractSyncService svc) { - SyncServiceManager ssm = INSTANCE; + SyncManager ssm = INSTANCE; if (ssm == null) return; synchronized(sSyncLock) { long mailboxId = svc.mMailboxId; @@ -2181,7 +2180,7 @@ public abstract class SyncServiceManager extends Service implements Runnable { } errorMap.remove(mailboxId); // If we've had a successful sync, clear the shutdown count - synchronized (SyncServiceManager.class) { + synchronized (SyncManager.class) { sClientConnectionManagerShutdownCount = 0; } // Leave now; other statuses are errors diff --git a/imap2/Android.mk b/imap2/Android.mk new file mode 100644 index 000000000..da642b4ba --- /dev/null +++ b/imap2/Android.mk @@ -0,0 +1,36 @@ +# Copyright 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. + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +# +# Imap2 +# +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_SRC_FILES += $(call all-java-files-under, ../src/com/beetstra) + +LOCAL_STATIC_JAVA_LIBRARIES := android-common com.android.emailcommon2 com.android.emailsync + +LOCAL_PACKAGE_NAME := Imap2 + +#LOCAL_PROGUARD_FLAG_FILES := proguard.flags +LOCAL_SDK_VERSION := 15 + +include $(BUILD_PACKAGE) + +# additionally, build unit tests in a separate .apk +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/imap2/AndroidManifest.xml b/imap2/AndroidManifest.xml new file mode 100644 index 000000000..b4ee3d0d6 --- /dev/null +++ b/imap2/AndroidManifest.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/imap2/res/mipmap-hdpi/icon.png b/imap2/res/mipmap-hdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6fd0a9851d6d3ea9120f1fa6b5893e602e5ecb3f GIT binary patch literal 7010 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 bGd3+SH##sd + + + + + + Imap2 + + + + Inbox + + Outbox + + Drafts + + Trash + + Sent + + Junk + + + diff --git a/imap2/res/xml/syncadapter_email.xml b/imap2/res/xml/syncadapter_email.xml new file mode 100644 index 000000000..278f4ad4e --- /dev/null +++ b/imap2/res/xml/syncadapter_email.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/imap2/src/com/android/imap2/AttachmentLoader.java b/imap2/src/com/android/imap2/AttachmentLoader.java new file mode 100644 index 000000000..3e24a1489 --- /dev/null +++ b/imap2/src/com/android/imap2/AttachmentLoader.java @@ -0,0 +1,197 @@ +/* Copyright (C) 2012 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.imap2; + +import android.content.Context; +import android.os.RemoteException; + +import com.android.emailcommon.provider.EmailContent.Attachment; +import com.android.emailcommon.provider.EmailContent.Message; +import com.android.emailcommon.service.EmailServiceStatus; +import com.android.emailsync.PartRequest; +import com.android.emailcommon.utility.AttachmentUtilities; +import com.android.imap2.Imap2SyncService.Connection; +import com.android.mail.providers.UIProvider; + +import java.io.Closeable; +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; + +/** + * Handle IMAP2 attachment loading + */ +public class AttachmentLoader { + static private final int CHUNK_SIZE = 16*1024; + + private final Context mContext; + private final Attachment mAttachment; + private final long mAttachmentId; + private final long mMessageId; + private final Message mMessage; + private final Imap2SyncService mService; + + public AttachmentLoader(Imap2SyncService service, PartRequest req) { + mService = service; + mContext = service.mContext; + mAttachment = req.mAttachment; + mAttachmentId = mAttachment.mId; + mMessageId = mAttachment.mMessageKey; + mMessage = Message.restoreMessageWithId(mContext, mMessageId); + } + + private void doStatusCallback(int status) { + try { + Imap2SyncManager.callback().loadAttachmentStatus(mMessageId, mAttachmentId, status, 0); + } catch (RemoteException e) { + // No danger if the client is no longer around + } + } + + private void doProgressCallback(int progress) { + try { + Imap2SyncManager.callback().loadAttachmentStatus(mMessageId, mAttachmentId, + EmailServiceStatus.IN_PROGRESS, progress); + } catch (RemoteException e) { + // No danger if the client is no longer around + } + } + + /** + * Close, ignoring errors (as during cleanup) + * @param c a Closeable + */ + private void close(Closeable c) { + try { + c.close(); + } catch (IOException e) { + } + } + + /** + * Save away the contentUri for this Attachment and notify listeners + * @throws IOException + */ + private void finishLoadAttachment(File file, OutputStream os) throws IOException { + InputStream in = null; + try { + in = new FileInputStream(file); + AttachmentUtilities.saveAttachment(mContext, in, mAttachment); + doStatusCallback(EmailServiceStatus.SUCCESS); + } catch (FileNotFoundException e) { + // Not bloody likely, as we just created it successfully + throw new IOException("Attachment file not found?"); + } finally { + close(in); + } + } + + private void readPart (ImapInputStream in, String tag, OutputStream out) throws IOException { + String res = in.readLine(); + int bstart = res.indexOf("body["); + if (bstart < 0) + bstart = res.indexOf("BODY["); + if (bstart < 0) + return; + int bend = res.indexOf(']', bstart); + if (bend < 0) + return; + int br = res.indexOf('{'); + if (br > 0) { + Parser p = new Parser(res, br + 1); + int expectedLength = p.parseInteger(); + int remainingLength = expectedLength; + int totalRead = 0; + byte[] buf = new byte[CHUNK_SIZE]; + int lastCallbackPct = -1; + int lastCallbackTotalRead = 0; + while (remainingLength > 0) { + int rdlen = (remainingLength > CHUNK_SIZE ? CHUNK_SIZE : remainingLength); + int bytesRead = in.read(buf, 0, rdlen); + totalRead += bytesRead; + out.write(buf, 0, bytesRead); + remainingLength -= bytesRead; + int pct = (totalRead * 100) / expectedLength; + // Callback only if we've read at least 1% more and have read more than CHUNK_SIZE + // We don't want to spam the Email app + if ((pct > lastCallbackPct) && (totalRead > (lastCallbackTotalRead + CHUNK_SIZE))) { + // Report progress back to the UI + doProgressCallback(pct); + lastCallbackTotalRead = totalRead; + lastCallbackPct = pct; + } + } + out.close(); + String line = in.readLine(); + if (!line.endsWith(")")) { + mService.errorLog("Bad part?"); + throw new IOException(); + } + line = in.readLine(); + if (!line.startsWith(tag)) { + mService.userLog("Bad part?"); + throw new IOException(); + } + } + } + + /** + * Loads an attachment, based on the PartRequest passed in the constructor + * @throws IOException + */ + public void loadAttachment(Connection conn) throws IOException { + if (mMessage == null) { + doStatusCallback(EmailServiceStatus.MESSAGE_NOT_FOUND); + return; + } + if (mAttachment.mUiState == UIProvider.AttachmentState.SAVED) { + return; + } + // Say we've started loading the attachment + doProgressCallback(0); + + try { + OutputStream os = null; + File tmpFile = null; + try { + tmpFile = File.createTempFile("imap2_", "tmp", mContext.getCacheDir()); + os = new FileOutputStream(tmpFile); + String tag = mService.writeCommand(conn.writer, "uid fetch " + mMessage.mServerId + + " body[" + mAttachment.mLocation + ']'); + readPart(conn.reader, tag, os); + finishLoadAttachment(tmpFile, os); + return; + } catch (FileNotFoundException e) { + mService.errorLog("Can't get attachment; write file not found?"); + doStatusCallback(EmailServiceStatus.ATTACHMENT_NOT_FOUND); + } finally { + close(os); + if (tmpFile != null) { + tmpFile.delete(); + } + } + } catch (IOException e) { + // Report the error, but also report back to the service + doStatusCallback(EmailServiceStatus.CONNECTION_ERROR); + throw e; + } finally { + } + } +} diff --git a/imap2/src/com/android/imap2/BroadcastProcessorService.java b/imap2/src/com/android/imap2/BroadcastProcessorService.java new file mode 100644 index 000000000..b86183dc8 --- /dev/null +++ b/imap2/src/com/android/imap2/BroadcastProcessorService.java @@ -0,0 +1,82 @@ +/* + * 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.imap2; + +import android.accounts.AccountManager; +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.android.emailcommon.Logging; +import com.android.emailsync.SyncManager; + +/** + * The service that really handles broadcast intents on a worker thread. + * + * We make it a service, because: + *
    + *
  • So that it's less likely for the process to get killed. + *
  • Even if it does, the Intent that have started it will be re-delivered by the system, + * and we can start the process again. (Using {@link #setIntentRedelivery}). + *
+ */ +public class BroadcastProcessorService extends IntentService { + // Action used for BroadcastReceiver entry point + private static final String ACTION_BROADCAST = "broadcast_receiver"; + + public BroadcastProcessorService() { + // Class name will be the thread name. + super(BroadcastProcessorService.class.getName()); + // Intent should be redelivered if the process gets killed before completing the job. + setIntentRedelivery(true); + } + + /** + * Entry point for {@link Imap2BroadcastReceiver}. + */ + public static void processBroadcastIntent(Context context, Intent broadcastIntent) { + Intent i = new Intent(context, BroadcastProcessorService.class); + i.setAction(ACTION_BROADCAST); + i.putExtra(Intent.EXTRA_INTENT, broadcastIntent); + context.startService(i); + } + + @Override + protected void onHandleIntent(Intent intent) { + // Dispatch from entry point + final String action = intent.getAction(); + if (ACTION_BROADCAST.equals(action)) { + final Intent broadcastIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT); + final String broadcastAction = broadcastIntent.getAction(); + + if (Intent.ACTION_BOOT_COMPLETED.equals(broadcastAction)) { + onBootCompleted(); + } else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) { + Log.d(Logging.LOG_TAG, "Login accounts changed; reconciling..."); + SyncManager.reconcileAccounts(this); + } + } + } + + /** + * Handles {@link Intent#ACTION_BOOT_COMPLETED}. Called on a worker thread. + */ + private void onBootCompleted() { + startService(new Intent(this, Imap2SyncManager.class)); + } +} diff --git a/imap2/src/com/android/imap2/EmailSyncAdapterService.java b/imap2/src/com/android/imap2/EmailSyncAdapterService.java new file mode 100644 index 000000000..d3a530d14 --- /dev/null +++ b/imap2/src/com/android/imap2/EmailSyncAdapterService.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2012 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.imap2; + +import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.provider.EmailContent.AccountColumns; +import com.android.emailcommon.provider.EmailContent.MailboxColumns; +import com.android.emailcommon.provider.Mailbox; + +import android.accounts.Account; +import android.accounts.OperationCanceledException; +import android.app.Service; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SyncResult; +import android.database.Cursor; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; + +public class EmailSyncAdapterService extends Service { + private static final String TAG = "Imap2 EmailSyncAdapterService"; + private static SyncAdapterImpl sSyncAdapter = null; + private static final Object sSyncAdapterLock = new Object(); + + private static final String[] ID_PROJECTION = new String[] {EmailContent.RECORD_ID}; + private static final String ACCOUNT_AND_TYPE_INBOX = + MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_INBOX; + + public EmailSyncAdapterService() { + super(); + } + + private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter { + private Context mContext; + + public SyncAdapterImpl(Context context) { + super(context, true /* autoInitialize */); + mContext = context; + } + + @Override + public void onPerformSync(Account account, Bundle extras, + String authority, ContentProviderClient provider, SyncResult syncResult) { + try { + EmailSyncAdapterService.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(); + } + + /** + * Partial integration with system SyncManager; we tell our EAS ExchangeService to start an + * inbox sync when we get the signal from the system SyncManager. + */ + private static void performSync(Context context, Account account, Bundle extras, + String authority, ContentProviderClient provider, SyncResult syncResult) + throws OperationCanceledException { + ContentResolver cr = context.getContentResolver(); + Log.i(TAG, "performSync"); + + // Find the (EmailProvider) account associated with this email address + Cursor accountCursor = + cr.query(com.android.emailcommon.provider.Account.CONTENT_URI, + ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", new String[] {account.name}, + null); + try { + if (accountCursor.moveToFirst()) { + long accountId = accountCursor.getLong(0); + // Now, find the inbox associated with the account + Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION, + ACCOUNT_AND_TYPE_INBOX, new String[] {Long.toString(accountId)}, null); + try { + if (mailboxCursor.moveToFirst()) { + Log.i(TAG, "Mail sync requested for " + account.name); + // Ask for a sync from our sync manager + //*** + //SyncServiceManager.serviceRequest(mailboxCursor.getLong(0), + // SyncServiceManager.SYNC_KICK); + } + } finally { + mailboxCursor.close(); + } + } + } finally { + accountCursor.close(); + } + } +} \ No newline at end of file diff --git a/imap2/src/com/android/imap2/Imap2.java b/imap2/src/com/android/imap2/Imap2.java new file mode 100644 index 000000000..2a2995f6b --- /dev/null +++ b/imap2/src/com/android/imap2/Imap2.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2012 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.imap2; + +import android.app.Application; + +public class Imap2 extends Application { + // TODO Investigate whether this class is needed +} diff --git a/imap2/src/com/android/imap2/Imap2BroadcastReceiver.java b/imap2/src/com/android/imap2/Imap2BroadcastReceiver.java new file mode 100644 index 000000000..8ea3c0029 --- /dev/null +++ b/imap2/src/com/android/imap2/Imap2BroadcastReceiver.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2012 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.imap2; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * The broadcast receiver. The actual job is done in EmailBroadcastProcessor on a worker thread. + */ +public class Imap2BroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + BroadcastProcessorService.processBroadcastIntent(context, intent); + } +} diff --git a/imap2/src/com/android/imap2/Imap2SyncManager.java b/imap2/src/com/android/imap2/Imap2SyncManager.java new file mode 100644 index 000000000..258775ca6 --- /dev/null +++ b/imap2/src/com/android/imap2/Imap2SyncManager.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2012 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.imap2; + +import android.os.Bundle; +import android.os.Debug; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; + +import com.android.emailcommon.Api; +import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.provider.EmailContent.Attachment; +import com.android.emailcommon.provider.EmailContent.MailboxColumns; +import com.android.emailcommon.provider.Account; +import com.android.emailcommon.provider.HostAuth; +import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.provider.ProviderUnavailableException; +import com.android.emailcommon.service.AccountServiceProxy; +import com.android.emailcommon.service.EmailServiceCallback; +import com.android.emailcommon.service.IEmailService; +import com.android.emailcommon.service.IEmailServiceCallback; +import com.android.emailcommon.service.IEmailServiceCallback.Stub; +import com.android.emailcommon.service.SearchParams; +import com.android.emailsync.AbstractSyncService; +import com.android.emailsync.PartRequest; +import com.android.emailsync.SyncManager; +import com.android.mail.providers.UIProvider.AccountCapabilities; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +public class Imap2SyncManager extends SyncManager { + + // Callbacks as set up via setCallback + private static final RemoteCallbackList mCallbackList = + new RemoteCallbackList(); + + private static final EmailServiceCallback sCallbackProxy = + new EmailServiceCallback(mCallbackList); + + /** + * Create our EmailService implementation here. + */ + private final IEmailService.Stub mBinder = new IEmailService.Stub() { + + @Override + public int getApiLevel() { + return Api.LEVEL; + } + + @Override + public Bundle validate(HostAuth hostAuth) throws RemoteException { + return new Imap2SyncService(Imap2SyncManager.this, + new Mailbox()).validateAccount(hostAuth, Imap2SyncManager.this); + } + + @Override + public Bundle autoDiscover(String userName, String password) throws RemoteException { + return null; + } + + @Override + public void startSync(long mailboxId, boolean userRequest) throws RemoteException { + SyncManager imapService = INSTANCE; + if (imapService == null) return; + Imap2SyncService svc = (Imap2SyncService) imapService.mServiceMap.get(mailboxId); + if (svc == null) { + startManualSync(mailboxId, userRequest ? SYNC_UI_REQUEST : SYNC_SERVICE_START_SYNC, + null); + } else { + svc.ping(); + } + } + + @Override + public void stopSync(long mailboxId) throws RemoteException { + stopManualSync(mailboxId); + } + + @Override + public void loadAttachment(long attachmentId, boolean background) throws RemoteException { + Attachment att = Attachment.restoreAttachmentWithId(Imap2SyncManager.this, attachmentId); + log("loadAttachment " + attachmentId + ": " + att.mFileName); + sendMessageRequest(new PartRequest(att, null, null)); + } + + @Override + public void updateFolderList(long accountId) throws RemoteException { + //*** + //reloadFolderList(ImapService.this, accountId, false); + } + + @Override + public void hostChanged(long accountId) throws RemoteException { + SyncManager exchangeService = INSTANCE; + if (exchangeService == null) return; + ConcurrentHashMap syncErrorMap = exchangeService.mSyncErrorMap; + // Go through the various error mailboxes + for (long mailboxId: syncErrorMap.keySet()) { + SyncError error = syncErrorMap.get(mailboxId); + // If it's a login failure, look a little harder + Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId); + // If it's for the account whose host has changed, clear the error + // If the mailbox is no longer around, remove the entry in the map + if (m == null) { + syncErrorMap.remove(mailboxId); + } else if (error != null && m.mAccountKey == accountId) { + error.fatal = false; + error.holdEndTime = 0; + } + } + // Stop any running syncs + exchangeService.stopAccountSyncs(accountId, true); + // Kick ExchangeService + kick("host changed"); + } + + @Override + public void setLogging(int flags) throws RemoteException { + // Protocol logging + //Eas.setUserDebug(flags); + // Sync logging + setUserDebug(flags); + } + + @Override + public void sendMeetingResponse(long messageId, int response) throws RemoteException { + // Not used in IMAP + } + + @Override + public void loadMore(long messageId) throws RemoteException { + } + + // The following three methods are not implemented in this version + @Override + public boolean createFolder(long accountId, String name) throws RemoteException { + return false; + } + + @Override + public boolean deleteFolder(long accountId, String name) throws RemoteException { + return false; + } + + @Override + public boolean renameFolder(long accountId, String oldName, String newName) + throws RemoteException { + return false; + } + + @Override + public void setCallback(IEmailServiceCallback cb) throws RemoteException { + mCallbackList.register(cb); + } + + @Override + public void deleteAccountPIMData(long accountId) throws RemoteException { + // Not required for IMAP + } + + @Override + public int searchMessages(long accountId, SearchParams params, long destMailboxId) + throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void sendMail(long accountId) throws RemoteException { + // Not required for IMAP + } + + @Override + public int getCapabilities(long accountId) throws RemoteException { + return AccountCapabilities.SYNCABLE_FOLDERS | + AccountCapabilities.FOLDER_SERVER_SEARCH | + AccountCapabilities.UNDO; + } + }; + + static public IEmailServiceCallback callback() { + return sCallbackProxy; + } + + @Override + public AccountObserver getAccountObserver(Handler handler) { + return new AccountObserver(handler) { + @Override + public void newAccount(long acctId) { + // Create the Inbox for the account + Account acct = Account.restoreAccountWithId(getContext(), acctId); + Mailbox inbox = new Mailbox(); + inbox.mDisplayName = "Inbox"; // Localize + inbox.mServerId = "Inbox"; + inbox.mAccountKey = acct.mId; + inbox.mType = Mailbox.TYPE_INBOX; + inbox.mSyncInterval = acct.mSyncInterval; + inbox.save(getContext()); + log("Creating inbox for account: " + acct.mDisplayName); + Imap2SyncManager.kick("New account"); + // Need to sync folder list first; sigh + Imap2SyncService svc = new Imap2SyncService(Imap2SyncManager.this, acct); + try { + svc.loadFolderList(); + mResolver.update( + ContentUris.withAppendedId(EmailContent.PICK_TRASH_FOLDER_URI, acctId), + new ContentValues(), null, null); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + }; + } + + @Override + public void onStartup() { + // No special behavior + } + + private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in ("; + private String mAccountSelector; + @Override + public String getAccountsSelector() { + if (mAccountSelector == null) { + StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN); + boolean first = true; + synchronized (mAccountList) { + for (Account account : mAccountList) { + if (!first) { + sb.append(','); + } else { + first = false; + } + sb.append(account.mId); + } + } + sb.append(')'); + mAccountSelector = sb.toString(); + } + return mAccountSelector; + } + + @Override + public AbstractSyncService getServiceForMailbox(Context context, Mailbox mailbox) { + return new Imap2SyncService(context, mailbox); + } + + @Override + public AccountList collectAccounts(Context context, AccountList accounts) { + ContentResolver resolver = context.getContentResolver(); + Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, + null); + // We must throw here; callers might use the information we provide for reconciliation, etc. + if (c == null) throw new ProviderUnavailableException(); + try { + while (c.moveToNext()) { + long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN); + if (hostAuthId > 0) { + HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId); + if (ha != null && ha.mProtocol.equals("imap2")) { + Account account = new Account(); + account.restore(c); + account.mHostAuthRecv = ha; + accounts.add(account); + } + } + } + } finally { + c.close(); + } + return accounts; + } + + @Override + public String getAccountManagerType() { + return "com.android.imap2"; + } + + @Override + public String getServiceIntentAction() { + return "com.android.email.IMAP2_INTENT"; + } + + @Override + public Stub getCallbackProxy() { + return sCallbackProxy; + } + + @Override + protected void runAccountReconcilerSync(Context context) { + alwaysLog("Reconciling accounts..."); + new AccountServiceProxy(context).reconcileAccounts("imap2", getAccountManagerType()); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } +} diff --git a/imap2/src/com/android/imap2/Imap2SyncService.java b/imap2/src/com/android/imap2/Imap2SyncService.java new file mode 100644 index 000000000..ac08aedac --- /dev/null +++ b/imap2/src/com/android/imap2/Imap2SyncService.java @@ -0,0 +1,2056 @@ +/* Copyright (C) 2012 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.imap2; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderOperation.Builder; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.net.TrafficStats; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import com.android.emailcommon.TrafficFlags; +import com.android.emailcommon.internet.MimeUtility; +import com.android.emailcommon.mail.Address; +import com.android.emailcommon.mail.CertificateValidationException; +import com.android.emailcommon.mail.MessagingException; +import com.android.emailcommon.provider.Account; +import com.android.emailcommon.provider.EmailContent.Attachment; +import com.android.emailcommon.provider.EmailContent.Body; +import com.android.emailcommon.provider.EmailContent.MailboxColumns; +import com.android.emailcommon.provider.EmailContent.MessageColumns; +import com.android.emailcommon.provider.EmailContent.SyncColumns; +import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.provider.HostAuth; +import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.provider.MailboxUtilities; +import com.android.emailcommon.provider.ProviderUnavailableException; +import com.android.emailcommon.provider.EmailContent.Message; +import com.android.emailcommon.service.EmailServiceProxy; +import com.android.emailcommon.service.EmailServiceStatus; +import com.android.emailcommon.service.SyncWindow; +import com.android.emailcommon.utility.SSLUtils; +import com.android.emailcommon.utility.TextUtilities; +import com.android.emailcommon.utility.Utility; +import com.android.emailsync.AbstractSyncService; +import com.android.emailsync.PartRequest; +import com.android.emailsync.Request; +import com.android.emailsync.SyncManager; +import com.android.mail.providers.UIProvider; +import com.beetstra.jutf7.CharsetProvider; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + +public class Imap2SyncService extends AbstractSyncService { + + private static final String IMAP_OK = "OK"; + private static final SimpleDateFormat GMAIL_INTERNALDATE_FORMAT = + new SimpleDateFormat("EEE, dd MMM yy HH:mm:ss z"); + private static final String IMAP_ERR = "ERR"; + + private static final SimpleDateFormat IMAP_DATE_FORMAT = + new SimpleDateFormat("dd-MMM-yyyy"); + private static final SimpleDateFormat INTERNALDATE_FORMAT = + new SimpleDateFormat("dd-MMM-yy HH:mm:ss z"); + private static final Charset MODIFIED_UTF_7_CHARSET = + new CharsetProvider().charsetForName("X-RFC-3501"); + + public static final String IMAP_DELETED_MESSAGES_FOLDER_NAME = "AndroidMail Trash"; + public static final String GMAIL_TRASH_FOLDER = "[Gmail]/Trash"; + + private static Pattern IMAP_RESPONSE_PATTERN = Pattern.compile("\\*(\\s(\\d+))?\\s(\\w+).*"); + + private static final int HEADER_BATCH_COUNT = 10; + + // private static final int IDLE_TIMEOUT_MILLIS = 12*MINS; + private static final int SECONDS = 1000; + private static final int MINS = 60*SECONDS; + private static final int IDLE_ASLEEP_MILLIS = 11*MINS; + // private static final int COMMAND_TIMEOUT_MILLIS = 24*SECS; + + private static final int SOCKET_CONNECT_TIMEOUT = 10*SECONDS; + private static final int SOCKET_TIMEOUT = 20*SECONDS; + + private ContentResolver mResolver; + private int mWriterTag = 1; + private boolean mIsGmail = false; + private boolean mIsIdle = false; + private int mLastExists = -1; + + private ArrayList mImapResponse = null; + private String mImapResult; + private String mImapErrorLine = null; + + private Socket mSocket = null; + private boolean mStop = false; + + public int mServiceResult = 0; + private boolean mIsServiceRequestPending = false; + + private final String[] MAILBOX_SERVER_ID_ARGS = new String[2]; + public Imap2SyncService() { + this("Imap2 Validation"); + } + + private final ArrayList SERVER_DELETES = new ArrayList(); + + private static final String INBOX_SERVER_NAME = "Inbox"; // Per RFC3501 + + private BufferedWriter mWriter; + private ImapInputStream mReader; + + private HostAuth mHostAuth; + private String mPrefix; + + public Imap2SyncService(Context _context, Mailbox _mailbox) { + super(_context, _mailbox); + mResolver = _context.getContentResolver(); + MAILBOX_SERVER_ID_ARGS[0] = Long.toString(mMailboxId); + } + + private Imap2SyncService(String prefix) { + super(prefix); + } + + public Imap2SyncService(Context _context, Account _account) { + this("Imap2 Account"); + mContext = _context; + mResolver = _context.getContentResolver(); + mAccount = _account; + mHostAuth = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv); + mPrefix = mHostAuth.mDomain; + } + + @Override + public boolean alarm() { + // See if we've got anything to do... + Cursor updates = getUpdatesCursor(); + Cursor deletes = getDeletesCursor(); + try { + if (mRequestQueue.isEmpty() && updates == null && deletes == null) { + userLog("Ping: nothing to do"); + } else { + int cnt = mRequestQueue.size(); + if (updates != null) { + cnt += updates.getCount(); + } + if (deletes != null) { + cnt += deletes.getCount(); + } + userLog("Ping: " + cnt + " tasks"); + ping(); + } + } finally { + if (updates != null) { + updates.close(); + } + if (deletes != null) { + deletes.close(); + } + } + return true; + } + + @Override + public void reset() { + // TODO Auto-generated method stub + } + + public void addRequest(Request req) { + super.addRequest(req); + if (req instanceof PartRequest) { + userLog("Request for attachment " + ((PartRequest)req).mAttachment.mId); + } + ping(); + } + + @Override + public Bundle validateAccount(HostAuth hostAuth, Context context) { + Bundle bundle = new Bundle(); + int resultCode = MessagingException.IOERROR; + + Connection conn = connectAndLogin(hostAuth, "main"); + if (conn.status == EXIT_DONE) { + resultCode = MessagingException.NO_ERROR; + } else if (conn.status == EXIT_LOGIN_FAILURE) { + resultCode = MessagingException.AUTHENTICATION_FAILED; + } + + bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode); + return bundle; + } + + public void loadFolderList() throws IOException { + HostAuth hostAuth = + HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); + if (hostAuth == null) return; + Connection conn = connectAndLogin(hostAuth, "folderList"); + if (conn.status == EXIT_DONE) { + setConnection(conn); + readFolderList(); + conn.socket.close(); + } + } + + private void setConnection(Connection conn) { + mConnection = conn; + mWriter = conn.writer; + mReader = conn.reader; + mSocket = conn.socket; + } + + @Override + public void resetCalendarSyncKey() { + // Not used by Imap2 + } + + public void ping() { + mIsServiceRequestPending = true; + Imap2SyncManager.runAwake(mMailbox.mId); + if (mSocket != null) { + try { + if (mIsIdle) { + userLog("breakIdle; sending DONE..."); + mWriter.write("DONE\r\n"); + mWriter.flush(); + } + } catch (SocketException e) { + } catch (IOException e) { + } + } + } + + public void stop () { + if (mSocket != null) + try { + if (mIsIdle) + ping(); + mSocket.close(); + } catch (IOException e) { + } + mStop = true; + } + + public String writeCommand (Writer out, String cmd) { + try { + Integer t = mWriterTag++; + String tag = "@@a" + t + ' '; + out.write(tag); + out.write(cmd); + out.write("\r\n"); + out.flush(); + if (!cmd.startsWith("login")) + userLog(tag + cmd); + return tag; + } catch (IOException e) { + userLog("IOException in writeCommand"); + } + return null; + } + + private long readLong (String str, int idx) { + char ch = str.charAt(idx); + long num = 0; + while (ch >= '0' && ch <= '9') { + num = (num * 10) + (ch - '0'); + ch = str.charAt(++idx); + } + return num; + } + + private void readUntagged(String str) { + // Skip the "* " + Parser p = new Parser(str, 2); + String type = p.parseAtom(); + int val = -1; + if (type != null) { + char c = type.charAt(0); + if (c >= '0' && c <= '9') + try { + val = Integer.parseInt(type); + type = p.parseAtom(); + if (p != null) { + if (type.toLowerCase().equals("exists")) + mLastExists = val; + } + } catch (NumberFormatException e) { + } else if (mMailbox.mSyncKey == null || mMailbox.mSyncKey == "0") { + str = str.toLowerCase(); + int idx = str.indexOf("uidvalidity"); + if (idx > 0) { + //*** 12? + long num = readLong(str, idx + 12); + mMailbox.mSyncKey = Long.toString(num); + ContentValues cv = new ContentValues(); + cv.put(MailboxColumns.SYNC_KEY, mMailbox.mSyncKey); + mContext.getContentResolver().update( + ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId), cv, + null, null); + } + } + } + + userLog("Untagged: " + type); + } + + private boolean caseInsensitiveStartsWith(String str, String tag) { + return str.toLowerCase().startsWith(tag.toLowerCase()); + } + + private String readResponse (ImapInputStream r, String tag) throws IOException { + return readResponse(r, tag, null); + } + + private String readResponse (ImapInputStream r, String tag, String command) throws IOException { + mImapResult = IMAP_ERR; + String str = null; + if (command != null) + mImapResponse = new ArrayList(); + while (true) { + str = r.readLine(); + userLog("< " + str); + if (caseInsensitiveStartsWith(str, tag)) { + // This is the response from the command named 'tag' + Parser p = new Parser(str, tag.length() - 1); + mImapResult = p.parseAtom(); + break; + } else if (str.charAt(0) == '*') { + if (command != null) { + Matcher m = IMAP_RESPONSE_PATTERN.matcher(str); + if (m.matches() && m.group(3).equals(command)) { + mImapResponse.add(str); + } else + readUntagged(str); + } else + readUntagged(str); + } else if (!mImapResponse.isEmpty()) { + // Continuation with string literal, perhaps? + int off = mImapResponse.size() - 1; + mImapResponse.set(off, mImapResponse.get(off) + "\r\n" + str); + } + } + + if (!mImapResult.equals(IMAP_OK)) { + userLog("$$$ Error result = " + mImapResult); + mImapErrorLine = str; + } + return mImapResult; + } + + String parseRecipientList (String str) { + if (str == null) + return null; + ArrayList
list = new ArrayList
(); + String r; + Parser p = new Parser(str); + while ((r = p.parseList()) != null) { + Parser rp = new Parser(r); + String displayName = rp.parseString(); + rp.parseString(); + String emailAddress = rp.parseString() + "@" + rp.parseString(); + list.add(new Address(emailAddress, displayName)); + } + return Address.pack(list.toArray(new Address[list.size()])); + } + + String parseRecipients (Parser p, Message msg) { + msg.mFrom = parseRecipientList(p.parseListOrNil()); + @SuppressWarnings("unused") + String senderList = parseRecipientList(p.parseListOrNil()); + msg.mReplyTo = parseRecipientList(p.parseListOrNil()); + msg.mTo = parseRecipientList(p.parseListOrNil()); + msg.mCc = parseRecipientList(p.parseListOrNil()); + msg.mBcc = parseRecipientList(p.parseListOrNil()); + return Address.toFriendly(Address.unpack(msg.mFrom)); + } + + private Message createMessage (String str) { + Parser p = new Parser(str, str.indexOf('(') + 1); + Date date = null; + String subject = null; + String sender = null; + boolean read = false; + int flag = 0; + String flags = null; + int uid = 0; + boolean bodystructure = false; + + Message msg = new Message(); + msg.mMailboxKey = mMailboxId; + + try { + while (true) { + String atm = p.parseAtom(); + // We're done if we have all of these, regardless of order + if (date != null && flags != null && bodystructure) + break; + // Not sure if this case is possible + if (atm == null) + break; + if (atm.equalsIgnoreCase("UID")) { + uid = p.parseInteger(); + //userLog("UID=" + uid); + } else if (atm.equalsIgnoreCase("ENVELOPE")) { + String envelope = p.parseList(); + Parser ep = new Parser(envelope); + ep.skipWhite(); + //date = parseDate(ep.parseString()); + ep.parseString(); + subject = ep.parseString(); + sender = parseRecipients(ep, msg); + } else if (atm.equalsIgnoreCase("FLAGS")) { + flags = p.parseList().toLowerCase(); + if (flags.indexOf("\\seen") >=0) + read = true; + if (flags.indexOf("\\flagged") >=0) + flag = 1; + } else if (atm.equalsIgnoreCase("BODYSTRUCTURE")) { + msg.mSyncData = p.parseList(); + bodystructure = true; + //parseBodystructure(msg, new Parser(bs), "", 1, parts); + } else if (atm.equalsIgnoreCase("INTERNALDATE")) { + date = parseInternaldate(p.parseString()); + } + } + } catch (Exception e) { + // Parsing error here. We've got one known one from EON + // in which BODYSTRUCTURE is ( "MIXED" (....) ) + if (sender == null) + sender = "Unknown sender"; + if (subject == null) + subject = "No subject"; + e.printStackTrace(); + } + + if (subject != null && subject.startsWith("=?")) + subject = MimeUtility.decode(subject); + msg.mSubject = subject; + + //msg.bodyId = 0; + //msg.parts = parts.toString(); + msg.mAccountKey = mAccount.mId; + + msg.mFlagLoaded = Message.FLAG_LOADED_UNLOADED; + msg.mFlags = flag; + if (read) + msg.mFlagRead = true; + msg.mTimeStamp = ((date != null) ? date : new Date()).getTime(); + msg.mServerId = Long.toString(uid); + return msg; + } + + private Date parseInternaldate (String str) { + if (str != null) { + SimpleDateFormat f = INTERNALDATE_FORMAT; + if (str.charAt(3) == ',') + f = GMAIL_INTERNALDATE_FORMAT; + try { + return f.parse(str); + } catch (ParseException e) { + userLog("Unparseable date: " + str); + } + } + return new Date(); + } + + private long getIdForUid(int uid) { + // TODO: Rename this + MAILBOX_SERVER_ID_ARGS[1] = Integer.toString(uid); + Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION, + MessageColumns.MAILBOX_KEY + "=? AND " + SyncColumns.SERVER_ID + "=?", + MAILBOX_SERVER_ID_ARGS, null); + try { + if (c != null && c.moveToFirst()) { + return c.getLong(Message.ID_COLUMNS_ID_COLUMN); + } + } finally { + if (c != null) { + c.close(); + } + } + return Message.NO_MESSAGE; + } + + private void processDelete(int uid) { + SERVER_DELETES.clear(); + SERVER_DELETES.add(uid); + processServerDeletes(SERVER_DELETES); + } + + /** + * Handle a single untagged line + * TODO: Perhaps batch operations for multiple lines into a single transaction + */ + private boolean handleUntagged (String line) { + line = line.toLowerCase(); + Matcher m = IMAP_RESPONSE_PATTERN.matcher(line); + boolean res = false; + if (m.matches()) { + // What kind of thing is this? + String type = m.group(3); + if (type.equals("fetch") || type.equals("expunge")) { + // This is a flag change or an expunge. First, find the UID + int uid = 0; + // TODO Get rid of hack to avoid uid... + int uidPos = line.indexOf("uid"); + if (uidPos > 0) { + Parser p = new Parser(line, uidPos + 3); + uid = p.parseInteger(); + } + + if (uid == 0) { + // This will be very inefficient + // Have to 1) break idle, 2) query the server for uid + return false; + } + long id = getIdForUid(uid); + if (id == Message.NO_MESSAGE) { + // Nothing to do; log + userLog("? No message found for uid " + uid); + return true; + } + + if (type.equals("fetch")) { + if (line.indexOf("\\deleted") > 0) { + processDelete(uid); + } else { + boolean read = line.indexOf("\\seen") > 0; + boolean flagged = line.indexOf("\\flagged") > 0; + // TODO: Reuse + ContentValues values = new ContentValues(); + values.put(MessageColumns.FLAG_READ, read); + values.put(MessageColumns.FLAG_FAVORITE, flagged); + mResolver.update(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id), + values, null, null); + } + userLog("<<< FLAGS " + uid); + } else { + userLog("<<< EXPUNGE " + uid); + processDelete(uid); + } + } else if (type.equals("exists")) { + int num = Integer.parseInt(m.group(2)); + if (mIsGmail && (num == mLastExists)) { + userLog("Gmail: nothing new..."); + return false; + } + else if (mIsGmail) + mLastExists = num; + res = true; + userLog("<<< EXISTS tag; new SEARCH"); + } + } + + return res; + } + + /** + * Prepends the folder name with the given prefix and UTF-7 encodes it. + */ + private String encodeFolderName(String name) { + // do NOT add the prefix to the special name "INBOX" + if ("inbox".equalsIgnoreCase(name)) return name; + + // Prepend prefix + if (mPrefix != null) { + name = mPrefix + 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; + } + + private static class ServerUpdate { + final long id; + final int serverId; + + ServerUpdate(long _id, int _serverId) { + id = _id; + serverId = _serverId; + } + } + + private ArrayList mUpdatedIds = new ArrayList(); + private ArrayList mDeletedIds = new ArrayList(); + private Stack mDeletes = new Stack(); + private Stack mReadUpdates = new Stack(); + private Stack mUnreadUpdates = new Stack(); + private Stack mFlaggedUpdates = new Stack(); + private Stack mUnflaggedUpdates = new Stack(); + + private Cursor getUpdatesCursor() { + Cursor c = mResolver.query(Message.UPDATED_CONTENT_URI, UPDATE_DELETE_PROJECTION, + MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null); + if (c == null || c.getCount() == 0) { + c.close(); + return null; + } + return c; + } + + private static final String[] UPDATE_DELETE_PROJECTION = + new String[] {MessageColumns.ID, SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, + MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE}; + private static final int UPDATE_DELETE_ID_COLUMN = 0; + private static final int UPDATE_DELETE_SERVER_ID_COLUMN = 1; + private static final int UPDATE_DELETE_MAILBOX_KEY_COLUMN = 2; + private static final int UPDATE_DELETE_READ_COLUMN = 3; + private static final int UPDATE_DELETE_FAVORITE_COLUMN = 4; + + private Cursor getDeletesCursor() { + Cursor c = mResolver.query(Message.DELETED_CONTENT_URI, UPDATE_DELETE_PROJECTION, + MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null); + if (c == null || c.getCount() == 0) { + c.close(); + return null; + } + return c; + } + + private void handleLocalDeletes() throws IOException { + Cursor c = getDeletesCursor(); + if (c == null) return; + mDeletes.clear(); + mDeletedIds.clear(); + + try { + while (c.moveToNext()) { + long id = c.getLong(UPDATE_DELETE_ID_COLUMN); + mDeletes.add(new ServerUpdate(id, c.getInt(UPDATE_DELETE_SERVER_ID_COLUMN))); + mDeletedIds.add(id); + } + sendUpdate(mDeletes, "+FLAGS (\\Deleted)"); + String tag = writeCommand(mConnection.writer, "expunge"); + readResponse(mConnection.reader, tag, "expunge"); + + // Delete the deletions now (we must go deeper!) + ArrayList ops = new ArrayList(); + for (long id: mDeletedIds) { + ops.add(ContentProviderOperation.newDelete( + ContentUris.withAppendedId( + Message.DELETED_CONTENT_URI, id)).build()); + } + applyBatch(ops); + } finally { + c.close(); + } + } + + private void handleLocalUpdates() throws IOException { + Cursor updatesCursor = getUpdatesCursor(); + if (updatesCursor == null) return; + + mUpdatedIds.clear(); + mReadUpdates.clear(); + mUnreadUpdates.clear(); + mFlaggedUpdates.clear(); + mUnflaggedUpdates.clear(); + + try { + while (updatesCursor.moveToNext()) { + long id = updatesCursor.getLong(UPDATE_DELETE_ID_COLUMN); + // Keep going if there's no serverId + int serverId = updatesCursor.getInt(UPDATE_DELETE_SERVER_ID_COLUMN); + if (serverId == 0) { + continue; + } + + // Say we've handled this update + mUpdatedIds.add(id); + // We have the id of the changed item. But first, we have to find out its current + // state, since the updated table saves the opriginal state + Cursor currentCursor = mResolver.query( + ContentUris.withAppendedId(Message.CONTENT_URI, id), + UPDATE_DELETE_PROJECTION, null, null, null); + try { + // If this item no longer exists (shouldn't be possible), just move along + if (!currentCursor.moveToFirst()) { + continue; + } + + boolean flagChange = false; + boolean readChange = false; + + long mailboxId = currentCursor.getLong(UPDATE_DELETE_MAILBOX_KEY_COLUMN); + // If the message is now in the trash folder, it has been deleted by the user + if (mailboxId != updatesCursor.getLong(UPDATE_DELETE_MAILBOX_KEY_COLUMN)) { + // The message has been moved to another mailbox + Mailbox newMailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); + if (newMailbox == null) { + continue; + } + copyMessage(serverId, newMailbox); + } + + // We can only send flag changes to the server in 12.0 or later + int flag = currentCursor.getInt(UPDATE_DELETE_FAVORITE_COLUMN); + if (flag != updatesCursor.getInt(UPDATE_DELETE_FAVORITE_COLUMN)) { + flagChange = true; + } + + int read = currentCursor.getInt(UPDATE_DELETE_READ_COLUMN); + if (read != updatesCursor.getInt(UPDATE_DELETE_READ_COLUMN)) { + readChange = true; + } + + if (!flagChange && !readChange) { + // In this case, we've got nothing to send to the server + continue; + } + + ServerUpdate update = new ServerUpdate(id, serverId); + if (readChange) { + if (read == 1) { + mReadUpdates.add(update); + } else { + mUnreadUpdates.add(update); + } + } + if (flagChange) { + if (flag == 1) { + mFlaggedUpdates.add(update); + } else { + mUnflaggedUpdates.add(update); + } + } + } finally { + currentCursor.close(); + } + } + } finally { + updatesCursor.close(); + } + + if (!mUpdatedIds.isEmpty()) { + sendUpdate(mReadUpdates, "+FLAGS (\\Seen)"); + sendUpdate(mUnreadUpdates, "-FLAGS (\\Seen)"); + sendUpdate(mFlaggedUpdates, "+FLAGS (\\Flagged)"); + sendUpdate(mUnflaggedUpdates, "-FLAGS (\\Flagged)"); + // Delete the updates now + ArrayList ops = new ArrayList(); + for (Long id: mUpdatedIds) { + ops.add(ContentProviderOperation.newDelete( + ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build()); + } + applyBatch(ops); + } + } + + private void sendUpdate(Stack updates, String command) throws IOException { + // First, generate the appropriate String + while (!updates.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20 && !updates.empty(); i++) { + ServerUpdate update = updates.pop(); + if (i != 0) { + sb.append(','); + } + sb.append(update.serverId); + } + String tag = + writeCommand(mConnection.writer, "uid store " + sb.toString() + " " + command); + if (!readResponse(mConnection.reader, tag, "STORE").equals(IMAP_OK)) { + errorLog("Server flag update failed?"); + return; + } + } + } + + private void copyMessage(int serverId, Mailbox mailbox) throws IOException { + String tag = writeCommand(mConnection.writer, "uid copy " + serverId + " \"" + + encodeFolderName(mailbox.mServerId) + "\""); + if (readResponse(mConnection.reader, tag, "COPY").equals(IMAP_OK)) { + tag = writeCommand(mConnection.writer, "uid store " + serverId + " +FLAGS(\\Deleted)"); + if (readResponse(mConnection.reader, tag, "STORE").equals(IMAP_OK)) { + tag = writeCommand(mConnection.writer, "expunge"); + readResponse(mConnection.reader, tag, "expunge"); + } + } else { + errorLog("Server copy failed?"); + } + } + + private void saveNewMessages (ArrayList msgList) { + // Cursor dc = getLocalDeletedCursor(); + // ArrayList dl = new ArrayList(); + // boolean newDeletions = false; + // try { + // if (dc.moveToFirst()) { + // do { + // dl.add(dc.getInt(Email.UID_COLUMN)); + // newDeletions = true; + // } while (dc.moveToNext()); + // } + // } finally { + // dc.close(); + // } + + ArrayList ops = new ArrayList(); + for (Message msg: msgList) { + //if (newDeletions && dl.contains(msg.uid)) { + // userLog("PHEW! Didn't save deleted message with uid: " + msg.uid); + // continue; + //} + msg.addSaveOps(ops); + } + applyBatch(ops); + } + + private String readTextPart (ImapInputStream in, String tag, Attachment att, boolean lastPart) + throws IOException { + String res = in.readLine(); + + int bstart = res.indexOf("body["); + if (bstart < 0) + bstart = res.indexOf("BODY["); + if (bstart < 0) + return ""; + int bend = res.indexOf(']', bstart); + if (bend < 0) + return ""; + + //String charset = getCharset(thisLoc); + boolean qp = att.mEncoding.equalsIgnoreCase("quoted-printable"); + + int br = res.indexOf('{'); + if (br > 0) { + Parser p = new Parser(res, br + 1); + int length = p.parseInteger(); + int len = length; + byte[] buf = new byte[len]; + int offs = 0; + while (len > 0) { + int rd = in.read(buf, offs, len); + offs += rd; + len -= rd; + } + + if (qp) { + length = QuotedPrintable.decode(buf, length); + } + + if (lastPart) { + String line = in.readLine(); + if (!line.endsWith(")")) { + userLog("Bad text part?"); + throw new IOException(); + } + line = in.readLine(); + if (!line.startsWith(tag)) { + userLog("Bad text part?"); + throw new IOException(); + } + } + return new String(buf, 0, length, Charset.forName("UTF8")); + + } else { + return ""; + } + } + + private Thread mBodyThread; + private Connection mConnection; + + private void parseBodystructure (Message msg, Parser p, String level, int cnt, + ArrayList viewables, ArrayList attachments) { + if (p.peekChar() == '(') { + // Multipart variant + while (true) { + String ps = p.parseList(); + if (ps == null) + break; + parseBodystructure(msg, + new Parser(ps), level + ((level.length() > 0) ? '.' : "") + cnt, 1, + viewables, attachments); + cnt++; + } + // Multipart type (MIXED/ALTERNATIVE/RELATED) + String mp = p.parseString(); + userLog("Multipart: " + mp); + } else { + boolean attachment = true; + String fileName = ""; + + // Here's an actual part... + // mime type + String type = p.parseString().toLowerCase(); + // mime subtype + String sub = p.parseString().toLowerCase(); + // parameter list or NIL + String paramList = p.parseList(); + if (paramList == null) + p.parseAtom(); + else { + Parser pp = new Parser(paramList); + String param; + while ((param = pp.parseString()) != null) { + String val = pp.parseString(); + if (param.equalsIgnoreCase("name")) { + fileName = val; + } else if (param.equalsIgnoreCase("charset")) { + // TODO: Do we need to handle this? + } + } + } + // contentId + String contentId = p.parseString(); + if (contentId != null) { + // Must remove the angle-bracket pair + contentId = contentId.substring(1, contentId.length() - 1); + fileName = ""; + } + + // contentName + p.parseString(); + // encoding + String encoding = p.parseString().toLowerCase(); + // length + Integer length = p.parseInteger(); + String lvl = level.length() > 0 ? level : String.valueOf(cnt); + + // body MD5 + p.parseStringOrAtom(); + + // disposition + paramList = p.parseList(); + if (paramList != null) { + //A parenthesized list, consisting of a disposition type + //string, followed by a parenthesized list of disposition + //attribute/value pairs as defined in [DISPOSITION]. + Parser pp = new Parser(paramList); + String param; + while ((param = pp.parseString()) != null) { + String val = pp.parseString(); + if (param.equalsIgnoreCase("name") || param.equalsIgnoreCase("filename")) { + fileName = val; + } + } + } + + // Don't waste time with Microsoft foolishness + if (!sub.equals("ms-tnef")) { + Attachment att = new Attachment(); + att.mLocation = lvl; + att.mMimeType = type + "/" + sub; + att.mSize = length; + att.mFileName = fileName; + att.mEncoding = encoding; + att.mContentId = contentId; + // TODO: charset? + + if ((!type.startsWith("text")) && attachment) { + //msg.encoding |= Email.ENCODING_HAS_ATTACHMENTS; + attachments.add(att); + } else { + viewables.add(att); + } + + userLog("Part " + lvl + ": " + type + "/" + sub); + } + + } + } + + private void fetchMessageData(Connection conn, Cursor c) throws IOException { + for (;;) { + try { + if (c == null) { + c = mResolver.query(Message.CONTENT_URI, Message.CONTENT_PROJECTION, + MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_UNLOADED, null, + MessageColumns.TIMESTAMP + " desc"); + if (c == null || c.getCount() == 0) { + return; + } + } + while (c.moveToNext()) { + // Parse the message's bodystructure + Message msg = new Message(); + msg.restore(c); + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + parseBodystructure(msg, new Parser(msg.mSyncData), "", 1, viewables, + attachments); + ContentValues values = new ContentValues(); + values.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE); + // Save the attachments... + for (Attachment att: attachments) { + att.mAccountKey = mAccount.mId; + att.mMessageKey = msg.mId; + att.save(mContext); + } + // Whether or not we have attachments + values.put(MessageColumns.FLAG_ATTACHMENT, !attachments.isEmpty()); + // Get the viewables + Attachment textViewable = null; + for (Attachment viewable: viewables) { + String mimeType = viewable.mMimeType; + if ("text/html".equalsIgnoreCase(mimeType)) { + textViewable = viewable; + } else if ("text/plain".equalsIgnoreCase(mimeType) && + textViewable == null) { + textViewable = viewable; + } + } + if (textViewable != null) { + // For now, just get single viewable + String tag = writeCommand(conn.writer, + "uid fetch " + msg.mServerId + " body.peek[" + + textViewable.mLocation + "]<0.200000>"); + String text = readTextPart(conn.reader, tag, textViewable, true); + userLog("Viewable " + textViewable.mMimeType + ", len = " + text.length()); + // Save it away + Body body = new Body(); + if (textViewable.mMimeType.equalsIgnoreCase("text/html")) { + body.mHtmlContent = text; + } else { + body.mTextContent = text; + } + body.mMessageKey = msg.mId; + body.save(mContext); + values.put(MessageColumns.SNIPPET, + TextUtilities.makeSnippetFromHtmlText(text)); + } else { + userLog("No viewable?"); + values.putNull(MessageColumns.SNIPPET); + } + mResolver.update(ContentUris.withAppendedId( + Message.CONTENT_URI, msg.mId), values, null, null); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + } + } + + private void fetchMessageData () throws IOException { + // If we're already loading messages on another thread, there's nothing to do + if (mBodyThread != null) return; + HostAuth hostAuth = + HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); + if (hostAuth == null) return; + // Find messages to load, if any + final Cursor unloaded = mResolver.query(Message.CONTENT_URI, Message.CONTENT_PROJECTION, + MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_UNLOADED, null, + MessageColumns.TIMESTAMP + " desc"); + int cnt = unloaded.getCount(); + // If there aren't any, we're done + if (cnt > 0) { + userLog("Found " + unloaded.getCount() + " messages requiring fetch"); + // If we have more than one, try a second thread + // Some servers may not allow this, so we fall back to loading text on the main thread + if (cnt > 1) { + final Connection conn = connectAndLogin(hostAuth, "body"); + if (conn.status == EXIT_DONE) { + mBodyThread = + new Thread(new Runnable() { + @Override + public void run() { + try { + fetchMessageData(conn, unloaded); + conn.socket.close(); + } catch (IOException e) { + } finally { + mBodyThread = null; + } + }}); + mBodyThread.start(); + } else { + fetchMessageData(mConnection, unloaded); + } + } else { + fetchMessageData(mConnection, unloaded); + } + } + } + + void readFolderList () throws IOException { + String tag = writeCommand(mWriter, "list \"\" *"); + String line; + char dchar = '/'; + + userLog("Loading folder list..."); + + ArrayList parentList = new ArrayList(); + ArrayList mailboxList = new ArrayList(); + while (true) { + line = mReader.readLine(); + userLog(line); + if (line.startsWith(tag)) { + // Done reading folder list + break; + } else { + Parser p = new Parser(line, 2); + String cmd = p.parseAtom(); + if (cmd.equalsIgnoreCase("list")) { + @SuppressWarnings("unused") + String props = p.parseListOrNil(); + String delim = p.parseString(); + if (delim == null) + delim = "~"; + if (delim.length() == 1) + dchar = delim.charAt(0); + String serverId = p.parseStringOrAtom(); + int lastDelim = serverId.lastIndexOf(delim); + String displayName; + String parentName; + if (lastDelim > 0) { + displayName = serverId.substring(lastDelim + 1); + parentName = serverId.substring(0, lastDelim); + } else { + displayName = serverId; + parentName = null; + + } + Mailbox m = new Mailbox(); + m.mDisplayName = displayName; + m.mAccountKey = mAccount.mId; + m.mServerId = serverId; + if (parentName != null && !parentList.contains(parentName)) { + parentList.add(parentName); + } + m.mFlagVisible = true; + m.mParentServerId = parentName; + m.mDelimiter = dchar; + m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; + mailboxList.add(m); + } else { + // WTF + } + } + } + + // TODO: Use narrower projection + Cursor c = mResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, + Mailbox.ACCOUNT_KEY + "=?", new String[] {Long.toString(mAccount.mId)}, + MailboxColumns.SERVER_ID + " asc"); + if (c == null) return; + int cnt = c.getCount(); + String[] serverIds = new String[cnt]; + long[] uidvals = new long[cnt]; + long[] ids = new long[cnt]; + int i = 0; + + try { + if (c.moveToFirst()) { + // Get arrays of information about existing mailboxes in account + do { + serverIds[i] = c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN); + uidvals[i] = c.getLong(Mailbox.CONTENT_SYNC_KEY_COLUMN); + ids[i] = c.getLong(Mailbox.CONTENT_ID_COLUMN); + i++; + } while (c.moveToNext()); + } + } finally { + c.close(); + } + + ArrayList addList = new ArrayList(); + + for (Mailbox m: mailboxList) { + int loc = Arrays.binarySearch(serverIds, m.mServerId); + if (loc >= 0) { + // It exists + if (uidvals[loc] == 0) { + // Good enough; a match that we've never visited! + // Mark this as touched (-1)... + uidvals[loc] = -1; + } else { + // Ok, now we need to see if this is the SAME mailbox... + // For now, assume it is; move on + // TODO: There's a problem if you've 1) visited this box and 2) another box now + // has its name, but how likely is that?? + uidvals[loc] = -1; + } + } else { + // We don't know about this mailbox, so we'll add it... + // BUT must see if it's a rename of one we've visited! + addList.add(m); + } + } + + // TODO: Flush this list every N (100?) in case there are zillions + ArrayList ops = new ArrayList(); + try { + for (i = 0; i < cnt; i++) { + String name = serverIds[i]; + long uidval = uidvals[i]; + // -1 means matched; ignore + // 0 means unmatched and never before seen + // > 0 means unmatched and HAS been seen. must find mWriter why + // TODO: Get rid of "Outbox" + if (uidval == 0 && !name.equals("Outbox") && + !name.equalsIgnoreCase(INBOX_SERVER_NAME)) { + // Ok, here's one we've never visited and it's not in the new list + ops.add(ContentProviderOperation.newDelete(ContentUris.withAppendedId( + Mailbox.CONTENT_URI, ids[i])).build()); + userLog("Deleting unseen mailbox; no match: " + name); + } else if (uidval > 0 && !name.equalsIgnoreCase(INBOX_SERVER_NAME)) { + boolean found = false; + for (Mailbox m : addList) { + tag = writeCommand(mWriter, "status \"" + m.mServerId + "\" (UIDVALIDITY)"); + if (readResponse(mReader, tag, "STATUS").equals(IMAP_OK)) { + String str = mImapResponse.get(0).toLowerCase(); + int idx = str.indexOf("uidvalidity"); + long num = readLong(str, idx + 12); + if (uidval == num) { +// try { +// // This is a renamed mailbox... +// c = Mailbox.getCursorWhere(mDatabase, "account=" + mAccount.id + " and serverName=?", name); +// if (c != null && c.moveToFirst()) { +// Mailbox existing = Mailbox.restoreFromCursor(c); +// userLog("Renaming existing mailbox: " + existing.mServerId + " to: " + m.mServerId); +// existing.mDisplayName = m.mDisplayName; +// existing.mServerId = m.mServerId; +// m.mHierarchicalName = m.mServerId; +// existing.mParentServerId = m.mParentServerId; +// existing.mFlags = m.mFlags; +// existing.save(mDatabase); +// // Mark this so that we don't save it below +// m.mServerId = null; +// } +// } finally { +// if (c != null) { +// c.close(); +// } +// } + found = true; + break; + } + } + } + if (!found) { + // There's no current mailbox with this uidval, so delete. + ops.add(ContentProviderOperation.newDelete(ContentUris.withAppendedId( + Mailbox.CONTENT_URI, ids[i])).build()); + userLog("Deleting uidval mailbox; no match: " + name); + } + } + } + for (Mailbox m : addList) { + String serverId = m.mServerId; + if (serverId == null) + continue; + if (!serverId.equalsIgnoreCase(INBOX_SERVER_NAME) + && !serverId.equalsIgnoreCase("Outbox")) { + m.mHierarchicalName = m.mServerId; + //*** For now, use Mail. We need a way to select the others... + m.mType = Mailbox.TYPE_MAIL; + ops.add(ContentProviderOperation.newInsert( + Mailbox.CONTENT_URI).withValues(m.toContentValues()).build()); + userLog("Adding new mailbox: " + m.mServerId); + } + } + + applyBatch(ops); + // Fixup parent stuff, flags... + MailboxUtilities.fixupUninitializedParentKeys(mContext, + Mailbox.ACCOUNT_KEY + "=" + mAccount.mId); + } finally { + SyncManager.kick("folder list"); + } + // TODO: Make sure UI is updated + } + + public int getDepth (String name, char delim) { + int depth = 0; + int last = -1; + while (true) { + last = name.indexOf(delim, last + 1); + if (last < 0) + return depth; + depth++; + } + } + + + private void applyBatch(ArrayList ops) { + try { + mResolver.applyBatch(EmailContent.AUTHORITY, ops); + } catch (RemoteException e) { + // Nothing to be done + } catch (OperationApplicationException e) { + // These operations are legal; this can't really happen + } + } + + private void processServerDeletes(ArrayList deleteList) { + int cnt = deleteList.size(); + if (cnt > 0) { + ArrayList ops = + new ArrayList(); + for (int i = 0; i < cnt; i++) { + MAILBOX_SERVER_ID_ARGS[1] = Long.toString(deleteList.get(i)); + Builder b = ContentProviderOperation.newDelete( + Message.SYNCED_SELECTION_CONTENT_URI); + b.withSelection(MessageColumns.MAILBOX_KEY + "=? AND " + + SyncColumns.SERVER_ID + "=?", MAILBOX_SERVER_ID_ARGS); + ops.add(b.build()); + } + applyBatch(ops); + } + } + + private void processServerUpdates(ArrayList deleteList, ContentValues values) { + int cnt = deleteList.size(); + if (cnt > 0) { + ArrayList ops = + new ArrayList(); + for (int i = 0; i < cnt; i++) { + MAILBOX_SERVER_ID_ARGS[1] = Long.toString(deleteList.get(i)); + Builder b = ContentProviderOperation.newUpdate( + Message.SYNCED_SELECTION_CONTENT_URI); + b.withSelection(MessageColumns.MAILBOX_KEY + "=? AND " + + SyncColumns.SERVER_ID + "=?", MAILBOX_SERVER_ID_ARGS); + b.withValues(values); + ops.add(b.build()); + } + applyBatch(ops); + } + } + + private static class Reconciled { + ArrayList insert; + ArrayList delete; + + Reconciled (ArrayList ins, ArrayList del) { + insert = ins; + delete = del; + } + } + + // Arrays must be sorted beforehand + public Reconciled reconcile (String name, int[] deviceList, int[] serverList) { + ArrayList loadList = new ArrayList(); + ArrayList deleteList = new ArrayList(); + int soff = 0; + int doff = 0; + int scnt = serverList.length; + int dcnt = deviceList.length; + + while (scnt > 0 || dcnt > 0) { + if (scnt == 0) { + for (; dcnt > 0; dcnt--) + deleteList.add(deviceList[doff++]); + break; + } else if (dcnt == 0) { + for (; scnt > 0; scnt--) + loadList.add(serverList[soff++]); + break; + } + int s = serverList[soff++]; + int d = deviceList[doff++]; + scnt--; + dcnt--; + if (s == d) { + continue; + } else if (s > d) { + deleteList.add(d); + scnt++; + soff--; + } else if (d > s) { + loadList.add(s); + dcnt++; + doff--; + } + } + + userLog("Reconciler " + name + "-> Insert: " + loadList.size() + + ", Delete: " + deleteList.size()); + return new Reconciled(loadList, deleteList); + } + + private static final String[] UID_PROJECTION = new String[] {SyncColumns.SERVER_ID}; + public int[] getUidList (String andClause) { + int offs = 0; + String ac = MessageColumns.MAILBOX_KEY + "=?"; + if (andClause != null) { + ac = ac + andClause; + } + Cursor c = mResolver.query(Message.CONTENT_URI, UID_PROJECTION, + ac, new String[] {Long.toString(mMailboxId)}, SyncColumns.SERVER_ID); + if (c != null) { + try { + int[] uids = new int[c.getCount()]; + if (c.moveToFirst()) { + do { + uids[offs++] = c.getInt(0); + } while (c.moveToNext()); + return uids; + } + } finally { + c.close(); + } + } + return new int[0]; + } + + public int[] getUnreadUidList () { + return getUidList(" and " + Message.FLAG_READ + "=0"); + } + + public int[] getFlaggedUidList () { + return getUidList(" and " + Message.FLAG_FAVORITE + "!=0"); + } + + private void reconcileState(int[] deviceList, String since, String flag, String search, + String column, boolean sense) throws IOException { + int[] serverList; + Parser p; + String msgs; + String tag = writeCommand(mWriter, "uid search undeleted " + search + " since " + since); + if (readResponse(mReader, tag, "SEARCH").equals(IMAP_OK)) { + if (mImapResponse.isEmpty()) { + serverList = new int[0]; + } else { + msgs = mImapResponse.get(0); + p = new Parser(msgs, 8); + serverList = p.gatherInts(); + Arrays.sort(serverList); + } + Reconciled r = reconcile(flag, deviceList, serverList); + ContentValues values = new ContentValues(); + values.put(column, sense); + processServerUpdates(r.delete, values); + values.put(column, !sense); + processServerUpdates(r.insert, values); + } + } + + private ArrayList getTokens(String str) { + ArrayList tokens = new ArrayList(); + Parser p = new Parser(str); + while(true) { + String capa = p.parseAtom(); + if (capa == null) { + break; + } + tokens.add(capa); + } + return tokens; + } + + public static class Connection { + Socket socket; + int status; + ImapInputStream reader; + BufferedWriter writer; + } + + private String mUserAgent; + + private Connection connectAndLogin(HostAuth hostAuth, String name) { + Connection conn = new Connection(); + Socket socket; + try { + socket = getSocket(hostAuth); + socket.setSoTimeout(SOCKET_TIMEOUT); + userLog(">>> IMAP CONNECTION SUCCESSFUL: " + name); + + ImapInputStream reader = new ImapInputStream(socket.getInputStream()); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( + socket.getOutputStream())); + // Get welcome string + reader.readLine(); + + String tag = writeCommand(writer, "CAPABILITY"); + if (readResponse(reader, tag, "CAPABILITY").equals(IMAP_OK)) { + // If CAPABILITY + if (!mImapResponse.isEmpty()) { + String capa = mImapResponse.get(0).toLowerCase(); + ArrayList tokens = getTokens(capa); + if (tokens.contains("starttls")) { + // Handle STARTTLS + userLog("[Supports STARTTLS]"); + } + if (tokens.contains("id")) { + String hostAddress = hostAuth.mAddress; + // Never send ID to *.secureserver.net + // Hackish, yes, but we've been doing this for years... :-( + if (!hostAddress.toLowerCase().endsWith(".secureserver.net")) { + // Assign user-agent string (for RFC2971 ID command) + if (mUserAgent == null) { + mUserAgent = ImapId.getImapId(mContext, hostAuth.mLogin, + hostAddress, null); + } + tag = writeCommand(writer, "ID (" + mUserAgent + ")"); + // We learn nothing useful from the response + readResponse(reader, tag); + } + } + } + } + + tag = writeCommand(writer, + "login " + hostAuth.mLogin + ' ' + hostAuth.mPassword); + if (!readResponse(reader, tag).equals(IMAP_OK)) { + conn.status = EXIT_LOGIN_FAILURE; + } else { + conn.socket = socket; + conn.reader = reader; + conn.writer = writer; + conn.status = EXIT_DONE; + userLog(">>> LOGGED IN: " + name); + if (mMailboxName != null) { + tag = writeCommand(conn.writer, "select \"" + encodeFolderName(mMailboxName) + + '\"'); + if (!readResponse(conn.reader, tag).equals(IMAP_OK)) { + // Select failed + userLog("Select failed?"); + conn.status = EXIT_EXCEPTION; + } else { + userLog(">>> SELECTED"); + } + } + } + } catch (CertificateValidationException e) { + conn.status = EXIT_LOGIN_FAILURE; + } catch (IOException e) { + conn.status = EXIT_IO_ERROR; + } + return conn; + } + + private void setMailboxSyncStatus(long id, int status) { + ContentValues values = new ContentValues(); + values.put(Mailbox.UI_SYNC_STATUS, status); + // Make sure we're always showing a "success" value. A failure wouldn't get set here, but + // rather via SyncService.done() + values.put(Mailbox.UI_LAST_SYNC_RESULT, Mailbox.LAST_SYNC_RESULT_SUCCESS); + mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null, null); + } + + private void idle() throws IOException { + mIsIdle = true; + mThread.setName(mMailboxName + ":IDLE[" + mAccount.mDisplayName + "]"); + userLog("Entering idle..."); + String tag = writeCommand(mWriter, "idle"); + + try { + while (true) { + String resp = mReader.readLine(); + if (resp.startsWith("+")) + break; + // Remember to handle untagged responses here (sigh, and elsewhere) + if (resp.startsWith("* ")) + handleUntagged(resp); + else { + userLog("Error in IDLE response: " + resp); + //*** How to handle this? + return; + } + } + + // Server has accepted IDLE + long idleStartTime = System.currentTimeMillis(); + + // Let the socket time out a minute after we expect to terminate it ourselves + mSocket.setSoTimeout(IDLE_ASLEEP_MILLIS + (1*MINS)); + // Say we're no longer syncing (turn off indeterminate progress in the UI) + setMailboxSyncStatus(mMailboxId, UIProvider.SyncStatus.NO_SYNC); + // Set an alarm for one minute before our timeout our expected IDLE time + Imap2SyncManager.runAsleep(mMailboxId, IDLE_ASLEEP_MILLIS); + + while (true) { + String line = null; + try { + line = mReader.readLine(); + userLog(line); + } catch (SocketTimeoutException e) { + userLog("Socket timeout"); + } finally { + Imap2SyncManager.runAwake(mMailboxId); + // Say we're syncing again + setMailboxSyncStatus(mMailboxId, UIProvider.SyncStatus.BACKGROUND_SYNC); + } + if (line == null || line.startsWith("* ")) { + boolean finish = (line == null) ? true : handleUntagged(line); + if (!finish) { + long timeSinceIdle = + System.currentTimeMillis() - idleStartTime; + // If we're nearing the end of IDLE time, let's just reset the IDLE while + // we've got the processor awake + if (timeSinceIdle > IDLE_ASLEEP_MILLIS - (2*MINS)) { + userLog("Time to reset IDLE..."); + finish = true; + } + } + if (finish) { + mWriter.write("DONE\r\n"); + mWriter.flush(); + } + } else if (line.startsWith(tag)) { + Parser p = new Parser(line, tag.length() - 1); + mImapResult = p.parseAtom(); + mIsIdle = false; + break; + } + } + } finally { + // We might have left IDLE due to an exception + if (mSocket != null) { + // Reset the standard timeout + mSocket.setSoTimeout(20 * 1000); + } + mIsIdle = false; + mThread.setName(mMailboxName + "[" + mAccount.mDisplayName + "]"); + } + } + + // Upload sent messages to server + + // void foo() { + // Cursor c = ServerUploads.getCursorWhere(mDatabase, "account=" + mAccount.id); + // ArrayList uploaded = new ArrayList(); + // try { + // if (c.moveToFirst()) { + // do{ + // Mailbox m = Mailbox.restoreFromId(mDatabase, c.getLong(ServerUploads.TO_MAILBOX_COLUMN)); + // String fn = c.getString(ServerUploads.FILENAME_COLUMN); + // if (m != null) { + // FileInputStream fi = null; + // try { + // fi = mContext.openFileInput(fn); + // } catch (Exception e) { + // logException(e); + // } + // if (fi != null) { + // BufferedInputStream bin = new BufferedInputStream(fi); + // mWriter.flush(); + // BufferedOutputStream bos = new BufferedOutputStream(mSocket.getOutputStream()); + // int len = fi.available(); + // byte[] buf; + // try { + // tag = writeCommand(mWriter, "append \"" + m.serverName + "\" (\\seen) {" + len + '}'); + // String line = in.readLine(); + // buf = new byte[CHUNK_SIZE]; + // if (line.startsWith("+")) { + // userLog("append response: " + line); + // + // while (len > 0) { + // int rlen = (len > CHUNK_SIZE) ? CHUNK_SIZE : len; + // int rd = bin.read(buf, 0, rlen); + // if (rd > 0) { + // bos.write(buf, 0, rd); + // } else if (rd < 0) + // break; + // len -= rd; + // } + // + // bos.flush(); + // mWriter.write("\r\n"); + // mWriter.flush(); + // bin.close(); + // if (readResponse(in, tag).equals(IMAP_OK)) { + // uploaded.add(c.getLong(ServerUploads.ID_COLUMN)); + // File f = mContext.getFileStreamPath(fn); + // if (f.delete()) { + // userLog("Upload file deleted: " + fn); + // } + // } else { + // userLog("Append failed?"); + // uploaded.add(c.getLong(ServerUploads.ID_COLUMN)); + // } + // } else { + // userLog("Append failed: " + line); + // uploaded.add(c.getLong(ServerUploads.ID_COLUMN)); + // } + // } catch (Exception e) { + // logException(e); + // uploaded.add(c.getLong(ServerUploads.ID_COLUMN)); + // } + // + // buf = null; + // if (m.name.equals(Mailbox.DRAFTS_NAME)) + // MailService.serviceRequest(m.id, 3000L); + // } else { + // userLog("Can't find file to upload, deleting upload record: " + fn); + // uploaded.add(c.getLong(ServerUploads.ID_COLUMN)); + // } + // } + // } while (c.moveToNext()); + // } + // } finally { + // // Delete the upload records for those completed + // for (Long id: uploaded) { + // ServerUploads.deleteById(mDatabase, id); + // } + // c.close(); + // } + // } + + private void processRequests() throws IOException { + while (!mRequestQueue.isEmpty()) { + Request req = mRequestQueue.peek(); + + // Our two request types are PartRequest (loading attachment) and + // MeetingResponseRequest (respond to a meeting request) + if (req instanceof PartRequest) { + TrafficStats.setThreadStatsTag( + TrafficFlags.getAttachmentFlags(mContext, mAccount)); + new AttachmentLoader(this, + (PartRequest)req).loadAttachment(mConnection); + TrafficStats.setThreadStatsTag( + TrafficFlags.getSyncFlags(mContext, mAccount)); + } + + // If there's an exception handling the request, we'll throw it + // Otherwise, we remove the request + mRequestQueue.remove(); + } + } + + private void sync () throws IOException { + mThread = Thread.currentThread(); + + HostAuth hostAuth = + HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); + if (hostAuth == null) return; + Connection conn = connectAndLogin(hostAuth, "main"); + if (conn.status != EXIT_DONE) { + mExitStatus = conn.status; + return; + } + setConnection(conn); + + // The account might have changed!! + //*** Determine how to often to do this + if (mMailboxName.equalsIgnoreCase("inbox")) { + long startTime = System.currentTimeMillis(); + readFolderList(); + userLog("Folder list processed in " + (System.currentTimeMillis() - startTime) + + "ms"); + } + + while (!mStop) { + try { + while (!mStop) { + mIsServiceRequestPending = false; + + // Now, handle various requests + processRequests(); + + long days; + if (mMailbox.mSyncLookback == SyncWindow.SYNC_WINDOW_UNKNOWN) { + days = 14; + } else + days = SyncWindow.toDays(mMailbox.mSyncLookback); + + long time = System.currentTimeMillis() - (days*DAYS); + Date date = new Date(time); + String since = IMAP_DATE_FORMAT.format(date); + String tag = writeCommand(mWriter, "uid search undeleted since " + since); + + // TODO Handle multi-line search result (google) + if (!readResponse(mReader, tag, "SEARCH").equals(IMAP_OK)) { + userLog("$$$ WHOA! Search failed? "); + } + + userLog(">>> SEARCH RESULT"); + int[] serverList; + String msgs; + Parser p; + if (mImapResponse.isEmpty()) { + serverList = new int[0]; + } else { + msgs = mImapResponse.get(0); + //*** Magic number? + p = new Parser(msgs, 8); + serverList = p.gatherInts(); + } + + Arrays.sort(serverList); + int[] deviceList = getUidList(null); + Reconciled r = + reconcile("MESSAGES", deviceList, serverList); + ArrayList loadList = r.insert; + ArrayList deleteList = r.delete; + serverList = null; + deviceList = null; + int cnt = loadList.size(); + + // We load message headers 20 at a time at this point... + int idx= 1; + boolean loadedSome = false; + while (idx <= cnt) { + ArrayList tmsgList = new ArrayList (); + int tcnt = 0; + StringBuilder tsb = new StringBuilder("uid fetch "); + for (tcnt = 0; tcnt < HEADER_BATCH_COUNT && idx <= cnt; tcnt++, idx++) { + // Load most recent first + if (tcnt > 0) + tsb.append(','); + tsb.append(loadList.get(cnt - idx)); + } + tsb.append(" (uid internaldate flags envelope bodystructure)"); + tag = writeCommand(mWriter, tsb.toString()); + if (readResponse(mReader, tag, "FETCH").equals(IMAP_OK)) { + // Create message and store + for (int j = 0; j < tcnt; j++) { + Message msg = createMessage(mImapResponse.get(j)); + tmsgList.add(msg); + } + saveNewMessages(tmsgList); + } + + fetchMessageData(); + loadedSome = true; + } + // TODO: Use loader to watch for changes on unloaded body cursor + if (!loadedSome) { + fetchMessageData(); + } + + // Reflect server deletions on device; do them all at once + processServerDeletes(deleteList); + + handleLocalUpdates(); + + handleLocalDeletes(); + + reconcileState(getUnreadUidList(), since, "UNREAD", "unseen", + MessageColumns.FLAG_READ, true); + reconcileState(getFlaggedUidList(), since, "FLAGGED", "flagged", + MessageColumns.FLAG_FAVORITE, false); + + // We're done if not pushing... + if (mMailbox.mSyncInterval != Mailbox.CHECK_INTERVAL_PUSH) { + mExitStatus = EXIT_DONE; + return; + } + + // If new requests have come in, process them + if (mIsServiceRequestPending) + continue; + + idle(); + } + + } finally { + if (mSocket != null) { + try { + mSocket.close(); + } catch (IOException e) { + } + } + } + } + } + + @Override + public void run() { + try { + // If we've been stopped, we're done + if (mStop) return; + + // Whether or not we're the account mailbox + try { + if ((mMailbox == null) || (mAccount == null)) { + return; + } else { + int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount); + TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_EMAIL); + + // We loop because someone might have put a request in while we were syncing + // and we've missed that opportunity... + do { + if (mRequestTime != 0) { + userLog("Looping for user request..."); + mRequestTime = 0; + } + if (mSyncReason >= Imap2SyncManager.SYNC_CALLBACK_START) { + try { + Imap2SyncManager.callback().syncMailboxStatus(mMailboxId, + EmailServiceStatus.IN_PROGRESS, 0); + } catch (RemoteException e1) { + // Don't care if this fails + } + } + sync(); + } while (mRequestTime != 0); + } + } catch (IOException e) { + String message = e.getMessage(); + userLog("Caught IOException: ", (message == null) ? "No message" : message); + mExitStatus = EXIT_IO_ERROR; + } catch (Exception e) { + userLog("Uncaught exception in EasSyncService", e); + } finally { + int status; + Imap2SyncManager.done(this); + if (!mStop) { + userLog("Sync finished"); + switch (mExitStatus) { + case EXIT_IO_ERROR: + status = EmailServiceStatus.CONNECTION_ERROR; + break; + case EXIT_DONE: + status = EmailServiceStatus.SUCCESS; + ContentValues cv = new ContentValues(); + cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); + String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount; + cv.put(Mailbox.SYNC_STATUS, s); + mContext.getContentResolver().update( + ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId), + cv, null, null); + break; + case EXIT_LOGIN_FAILURE: + status = EmailServiceStatus.LOGIN_FAILED; + break; + default: + status = EmailServiceStatus.REMOTE_EXCEPTION; + errorLog("Sync ended due to an exception."); + break; + } + } else { + userLog("Stopped sync finished."); + status = EmailServiceStatus.SUCCESS; + } + + // Send a callback (doesn't matter how the sync was started) + try { + // Unless the user specifically asked for a sync, we don't want to report + // connection issues, as they are likely to be transient. In this case, we + // simply report success, so that the progress indicator terminates without + // putting up an error banner + //*** + if (mSyncReason != Imap2SyncManager.SYNC_UI_REQUEST && + status == EmailServiceStatus.CONNECTION_ERROR) { + status = EmailServiceStatus.SUCCESS; + } + Imap2SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0); + } catch (RemoteException e1) { + // Don't care if this fails + } + + // Make sure ExchangeService knows about this + Imap2SyncManager.kick("sync finished"); + } + } catch (ProviderUnavailableException e) { + Log.e(TAG, "EmailProvider unavailable; sync ended prematurely"); + } + } + + private Socket getSocket(HostAuth hostAuth) throws CertificateValidationException, IOException { + Socket socket; + try { + boolean ssl = (hostAuth.mFlags & HostAuth.FLAG_SSL) != 0; + boolean trust = (hostAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0; + SocketAddress socketAddress = new InetSocketAddress(hostAuth.mAddress, hostAuth.mPort); + if (ssl) { + socket = SSLUtils.getSSLSocketFactory(trust).createSocket(); + } else { + socket = new Socket(); + } + socket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); + // After the socket connects to an SSL server, confirm that the hostname is as expected + if (ssl && !trust) { + verifyHostname(socket, hostAuth.mAddress); + } + } catch (SSLException e) { + errorLog(e.toString()); + throw new CertificateValidationException(e.getMessage(), e); + } + return socket; + } + + /** + * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this + * service but is not in the public API. + * + * Verify the hostname of the certificate used by the other end of a + * connected socket. You MUST call this if you did not supply a hostname + * to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method + * redundantly if the hostname has already been verified. + * + *

Wildcard certificates are allowed to verify any matching hostname, + * so "foo.bar.example.com" is verified if the peer has a certificate + * for "*.example.com". + * + * @param socket An SSL socket which has been connected to a server + * @param hostname The expected hostname of the remote server + * @throws IOException if something goes wrong handshaking with the server + * @throws SSLPeerUnverifiedException if the server cannot prove its identity + */ + private void verifyHostname(Socket socket, String hostname) throws IOException { + // The code at the start of OpenSSLSocketImpl.startHandshake() + // ensures that the call is idempotent, so we can safely call it. + SSLSocket ssl = (SSLSocket) socket; + ssl.startHandshake(); + + SSLSession session = ssl.getSession(); + if (session == null) { + throw new SSLException("Cannot verify SSL socket without session"); + } + // TODO: Instead of reporting the name of the server we think we're connecting to, + // we should be reporting the bad name in the certificate. Unfortunately this is buried + // in the verifier code and is not available in the verifier API, and extracting the + // CN & alts is beyond the scope of this patch. + if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)) { + throw new SSLPeerUnverifiedException( + "Certificate hostname not useable for server: " + hostname); + } + } +} diff --git a/imap2/src/com/android/imap2/ImapId.java b/imap2/src/com/android/imap2/ImapId.java new file mode 100644 index 000000000..f94a46546 --- /dev/null +++ b/imap2/src/com/android/imap2/ImapId.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2012 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.imap2; + +import android.content.Context; +import android.os.Build; +import android.telephony.TelephonyManager; +import android.util.Base64; +import android.util.Log; + +import com.android.emailcommon.Device; +import com.android.emailcommon.Logging; +import com.android.emailcommon.VendorPolicyLoader; +import com.google.common.annotations.VisibleForTesting; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.regex.Pattern; + +public class ImapId { + private static String sImapId; + + /** + * 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 (ImapId.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 = Device.getConsistentDeviceId(context); + 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 - _ + = ; : . , / + // 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(); + } + +} diff --git a/imap2/src/com/android/imap2/ImapInputStream.java b/imap2/src/com/android/imap2/ImapInputStream.java new file mode 100644 index 000000000..1730a117d --- /dev/null +++ b/imap2/src/com/android/imap2/ImapInputStream.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 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.imap2; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ImapInputStream extends FilterInputStream { + + public ImapInputStream(InputStream in) { + super(in); + } + + public String readLine () throws IOException { + StringBuilder sb = new StringBuilder(); + while (true) { + int b = read(); + // Line ends with \n; ignore \r + // I'm not sure this is the right thing with a raw \r (no \n following) + if (b < 0) + throw new IOException("Socket closed in readLine"); + if (b == '\n') + return sb.toString(); + else if (b != '\r') { + sb.append((char)b); + } + } + } + + public boolean ready () throws IOException { + return this.available() > 0; + } +} diff --git a/imap2/src/com/android/imap2/Parser.java b/imap2/src/com/android/imap2/Parser.java new file mode 100644 index 000000000..4eb809981 --- /dev/null +++ b/imap2/src/com/android/imap2/Parser.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2012 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.imap2; + +public class Parser { + String str; + int pos; + int len; + static final String white = "\r\n \t"; + + public Parser (String _str) { + str = _str; + pos = 0; + len = str.length(); + } + + public Parser (String _str, int start) { + str = _str; + pos = start; + len = str.length(); + } + + public void skipWhite () { + while ((pos < len) && white.indexOf(str.charAt(pos)) >= 0) + pos++; + } + + public String parseAtom () { + skipWhite(); + int start = pos; + while ((pos < len) && white.indexOf(str.charAt(pos)) < 0) + pos++; + if (pos > start) + return str.substring(start, pos); + return null; + } + + public char nextChar () { + if (pos >= len) + return 0; + else + return str.charAt(pos++); + } + + public char peekChar () { + if (pos >= len) + return 0; + else + return str.charAt(pos); + } + + public String parseString () { + return parseString(false); + } + + public String parseStringOrAtom () { + return parseString(true); + } + + public String parseString (boolean orAtom) { + skipWhite(); + char c = nextChar(); + if (c != '\"') { + if (c == '{') { + int cnt = parseInteger(); + c = nextChar(); + if (c != '}') + return null; + int start = pos + 2; + int end = start + cnt; + String s = str.substring(start, end); + pos = end; + return s; + } else if (orAtom) { + backChar(); + return parseAtom(); + } else if (c == 'n' || c == 'N') { + parseAtom(); + return null; + } else + return null; + } + int start = pos; + boolean quote = false; + while (true) { + c = nextChar(); + if (c == 0) + return null; + else if (quote) + quote = false; + else if (c == '\\') + quote = true; + else if (c == '\"') + break; + } + return str.substring(start, pos - 1); + } + + public void backChar () { + if (pos > 0) + pos--; + } + + public String parseListOrNil () { + String list = parseList(); + if (list == null) { + parseAtom(); + list = ""; + } + return list; + } + + public String parseList () { + skipWhite(); + if (nextChar() != '(') { + backChar(); + return null; + } + int start = pos; + int level = 0; + boolean quote = false; + boolean string = false; + while (true) { + char c = nextChar(); + if (c == 0) + return null; + else if (quote) + quote = false; + else if (c == '\\' && string) + quote = true; + else if (c == '\"') + string = !string; + else if (c == '(' && !string) + level++; + else if (c == ')' && !string) { + if (level-- == 0) + break; + } + } + return str.substring(start, pos - 1); + } + + public Integer parseInteger () { + skipWhite(); + int start = pos; + while (pos < len) { + char c = str.charAt(pos); + if (c >= '0' && c <= '9') + pos++; + else + break; + } + if (pos > start) { + try { + Integer i = Integer.parseInt(str.substring(start, pos)); + return i; + } catch (NumberFormatException e) { + return -1; + } + } else + return -1; + } + + public int[] gatherInts () { + int[] list = new int[128]; + int size = 128; + int offs = 0; + while (true) { + // TODO Slow; handle this inline rather than calling the method + Integer i = parseInteger(); + if (i >= 0) { + if (offs == size) { + // Double the size of the array as necessary + size <<= 1; + int[] tmp = new int[size]; + System.arraycopy(list, 0, tmp, 0, offs); + list = tmp; + } + list[offs++] = i; + } + else + break; + } + int[] res = new int[offs]; + System.arraycopy(list, 0, res, 0, offs); + return res; + } +} diff --git a/imap2/src/com/android/imap2/QuotedPrintable.java b/imap2/src/com/android/imap2/QuotedPrintable.java new file mode 100644 index 000000000..6171d9224 --- /dev/null +++ b/imap2/src/com/android/imap2/QuotedPrintable.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2012 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.imap2; + +public class QuotedPrintable { + static public String toString (String str) { + int len = str.length(); + // Make sure we don't get an index out of bounds error with the = character + int max = len - 2; + StringBuilder sb = new StringBuilder(len); + try { + for (int i = 0; i < len; i++) { + char c = str.charAt(i); + if (c == '=') { + if (i < max) { + char n = str.charAt(++i); + if (n == '\r') { + n = str.charAt(++i); + if (n == '\n') + continue; + else + System.err.println("Not valid QP"); + } else { + // Must be less than 0x80, right? + int a; + if (n >= '0' && n <= '9') + a = (n - '0') << 4; + else + a = (10 + (n - 'A')) << 4; + + n = str.charAt(++i); + if (n >= '0' && n <= '9') + c = (char) (a + (n - '0')); + else + c = (char) (a + 10 + (n - 'A')); + } + } if (i + 1 == len) + continue; + } + + sb.append(c); + } + } catch (IndexOutOfBoundsException e) { + } + String ret = sb.toString(); + return ret; + } + + static public String encode (String str) { + int len = str.length(); + StringBuffer sb = new StringBuffer(len + len>>2); + int i = 0; + while (i < len) { + char c = str.charAt(i++); + if (c < 0x80) { + sb.append(c); + } else { + sb.append('&'); + sb.append('#'); + sb.append((int)c); + sb.append(';'); + } + } + return sb.toString(); + } + + static public int decode (byte[] bytes, int len) { + // Make sure we don't get an index out of bounds error with the = character + int max = len - 2; + int pos = 0; + try { + for (int i = 0; i < len; i++) { + char c = (char)bytes[i]; + if (c == '=') { + if (i < max) { + char n = (char)bytes[++i]; + if (n == '\r') { + n = (char)bytes[++i]; + if (n == '\n') + continue; + else + System.err.println("Not valid QP"); + } else { + // Must be less than 0x80, right? + int a; + if (n >= '0' && n <= '9') + a = (n - '0') << 4; + else + a = (10 + (n - 'A')) << 4; + + n = (char)bytes[++i]; + if (n >= '0' && n <= '9') + c = (char) (a + (n - '0')); + else + c = (char) (a + 10 + (n - 'A')); + } + } if (i + 1 > len) + continue; + } + + bytes[pos++] = (byte)c; + } + } catch (IndexOutOfBoundsException e) { + } + return pos; + } +} diff --git a/res/values/strings.xml b/res/values/strings.xml index 237ff1164..7ad3bcb2d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1300,5 +1300,13 @@ as %s. No messages. + + + Push IMAP + + Picky, picky, picky! + Select trash folder + Create folder + diff --git a/res/xml/imap2_authenticator.xml b/res/xml/imap2_authenticator.xml new file mode 100644 index 000000000..622add2cc --- /dev/null +++ b/res/xml/imap2_authenticator.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/res/xml/providers.xml b/res/xml/providers.xml index 2541fe0da..8f372c69d 100644 --- a/res/xml/providers.xml +++ b/res/xml/providers.xml @@ -146,6 +146,11 @@ + + + + + diff --git a/res/xml/services.xml b/res/xml/services.xml index 5cbb1f9a3..e52ea1cf4 100644 --- a/res/xml/services.xml +++ b/res/xml/services.xml @@ -101,4 +101,21 @@ email:syncContacts="true" email:syncCalendar="true" /> + diff --git a/src/com/android/email/activity/setup/AccountSettingsUtils.java b/src/com/android/email/activity/setup/AccountSettingsUtils.java index 16e9df148..2613a53f6 100644 --- a/src/com/android/email/activity/setup/AccountSettingsUtils.java +++ b/src/com/android/email/activity/setup/AccountSettingsUtils.java @@ -26,9 +26,10 @@ import android.util.Log; import android.widget.EditText; import com.android.email.R; -import com.android.email.VendorPolicyLoader; import com.android.email.provider.AccountBackupRestore; import com.android.emailcommon.Logging; +import com.android.emailcommon.VendorPolicyLoader; +import com.android.emailcommon.VendorPolicyLoader.Provider; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.EmailContent.AccountColumns; @@ -231,50 +232,6 @@ public class AccountSettingsUtils { } } - public static class Provider implements Serializable { - private static final long serialVersionUID = 8511656164616538989L; - - public String id; - public String label; - public String domain; - public String incomingUriTemplate; - public String incomingUsernameTemplate; - public String outgoingUriTemplate; - public String outgoingUsernameTemplate; - public String incomingUri; - public String incomingUsername; - public String outgoingUri; - public String outgoingUsername; - public String note; - - /** - * Expands templates in all of the provider fields that support them. Currently, - * templates are used in 4 fields -- incoming and outgoing URI and user name. - * @param email user-specified data used to replace template values - */ - public void expandTemplates(String email) { - String[] emailParts = email.split("@"); - String user = emailParts[0]; - - incomingUri = expandTemplate(incomingUriTemplate, email, user); - incomingUsername = expandTemplate(incomingUsernameTemplate, email, user); - outgoingUri = expandTemplate(outgoingUriTemplate, email, user); - outgoingUsername = expandTemplate(outgoingUsernameTemplate, email, user); - } - - /** - * Replaces all parameterized values in the given template. The values replaced are - * $domain, $user and $email. - */ - private String expandTemplate(String template, String email, String user) { - String returnString = template; - returnString = returnString.replaceAll("\\$email", email); - returnString = returnString.replaceAll("\\$user", user); - returnString = returnString.replaceAll("\\$domain", domain); - return returnString; - } - } - /** * Infer potential email server addresses from domain names * diff --git a/src/com/android/email/activity/setup/AccountSetupBasics.java b/src/com/android/email/activity/setup/AccountSetupBasics.java index 506c82706..cfb9fe1d5 100644 --- a/src/com/android/email/activity/setup/AccountSetupBasics.java +++ b/src/com/android/email/activity/setup/AccountSetupBasics.java @@ -45,10 +45,10 @@ import com.android.email.EmailAddressValidator; import com.android.email.R; import com.android.email.activity.ActivityHelper; import com.android.email.activity.UiUtilities; -import com.android.email.activity.setup.AccountSettingsUtils.Provider; import com.android.email.service.EmailServiceUtils; import com.android.email.service.EmailServiceUtils.EmailServiceInfo; import com.android.emailcommon.Logging; +import com.android.emailcommon.VendorPolicyLoader.Provider; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.HostAuth; diff --git a/src/com/android/email/activity/setup/AccountSetupOptions.java b/src/com/android/email/activity/setup/AccountSetupOptions.java index 7bd5ea2fe..1c1044657 100644 --- a/src/com/android/email/activity/setup/AccountSetupOptions.java +++ b/src/com/android/email/activity/setup/AccountSetupOptions.java @@ -44,7 +44,6 @@ import com.android.email.service.MailService; import com.android.email2.ui.MailActivityEmail; import com.android.emailcommon.Logging; import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Policy; import com.android.emailcommon.service.SyncWindow; import com.android.emailcommon.utility.Utility; @@ -244,6 +243,7 @@ public class AccountSetupOptions extends AccountSetupActivity implements OnClick final boolean email2 = email; final boolean calendar2 = calendar; final boolean contacts2 = contacts; + Utility.runAsync(new Runnable() { @Override public void run() { diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java index a01105d07..4125cd634 100644 --- a/src/com/android/email/mail/store/ImapStore.java +++ b/src/com/android/email/mail/store/ImapStore.java @@ -27,7 +27,6 @@ import android.util.Log; import com.android.email.LegacyConversions; import com.android.email.Preferences; import com.android.email.R; -import com.android.email.VendorPolicyLoader; import com.android.email.mail.Store; import com.android.email.mail.Transport; import com.android.email.mail.store.imap.ImapConstants; @@ -35,6 +34,7 @@ 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; @@ -182,8 +182,8 @@ public class ImapStore extends Store { * @param capabilities a list of the capabilities from the server * @return a String for use in an IMAP ID message. */ - @VisibleForTesting - static String getImapId(Context context, String userName, String host, String capabilities) { + 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) { diff --git a/src/com/android/email/provider/DBHelper.java b/src/com/android/email/provider/DBHelper.java index 8df131ea6..330b1ad5e 100644 --- a/src/com/android/email/provider/DBHelper.java +++ b/src/com/android/email/provider/DBHelper.java @@ -126,8 +126,9 @@ public final class DBHelper { // Version 100 is first Email2 version // Version 101 SHOULD NOT BE USED // Version 102&103: Add hierarchicalName to Mailbox + // Version 104&105: add syncData to Message - public static final int DATABASE_VERSION = 103; + public static final int DATABASE_VERSION = 105; // Any changes to the database format *must* include update-in-place code. // Original version: 2 @@ -172,7 +173,8 @@ public final class DBHelper { + MessageColumns.MEETING_INFO + " text, " + MessageColumns.SNIPPET + " text, " + MessageColumns.PROTOCOL_SEARCH_INFO + " text, " - + MessageColumns.THREAD_TOPIC + " text" + + MessageColumns.THREAD_TOPIC + " text, " + + MessageColumns.SYNC_DATA + " text" + ");"; // This String and the following String MUST have the same columns, except for the type @@ -968,10 +970,32 @@ public final class DBHelper { + " add " + MailboxColumns.HIERARCHICAL_NAME + " text"); } catch (SQLException e) { // Shouldn't be needed unless we're debugging and interrupt the process - Log.w(TAG, "Exception upgrading EmailProviderBody.db from v6 to v8", e); + Log.w(TAG, "Exception upgrading EmailProviderBody.db from v10x to v103", e); } oldVersion = 103; } + if (oldVersion == 103) { + try { + db.execSQL("alter table " + Message.TABLE_NAME + + " add " + MessageColumns.SYNC_DATA + " text"); + } catch (SQLException e) { + // Shouldn't be needed unless we're debugging and interrupt the process + Log.w(TAG, "Exception upgrading EmailProviderBody.db from v103 to v104", e); + } + oldVersion = 104; + } + if (oldVersion == 104) { + try { + db.execSQL("alter table " + Message.UPDATED_TABLE_NAME + + " add " + MessageColumns.SYNC_DATA + " text"); + db.execSQL("alter table " + Message.DELETED_TABLE_NAME + + " add " + MessageColumns.SYNC_DATA + " text"); + } catch (SQLException e) { + // Shouldn't be needed unless we're debugging and interrupt the process + Log.w(TAG, "Exception upgrading EmailProviderBody.db from v104 to v105", e); + } + oldVersion = 105; + } } @Override diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index 5eef4b6b3..f12237c25 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -77,12 +77,17 @@ import com.android.emailcommon.service.IEmailServiceCallback; import com.android.emailcommon.service.SearchParams; import com.android.emailcommon.utility.AttachmentUtilities; import com.android.emailcommon.utility.Utility; +import com.android.mail.providers.Conversation; +import com.android.mail.providers.Folder; import com.android.mail.providers.UIProvider; import com.android.mail.providers.UIProvider.AccountCapabilities; import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; import com.android.mail.providers.UIProvider.ConversationPriority; import com.android.mail.providers.UIProvider.ConversationSendingState; import com.android.mail.providers.UIProvider.DraftType; +import com.android.mail.ui.ConversationUpdater; +import com.android.mail.ui.DestructiveAction; +import com.android.mail.ui.FoldersSelectionDialog; import com.android.mail.utils.LogUtils; import com.android.mail.utils.MatrixCursorWithExtra; import com.android.mail.utils.Utils; @@ -181,6 +186,7 @@ public class EmailProvider extends ContentProvider { private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 4; private static final int ACCOUNT_DEFAULT_ID = ACCOUNT_BASE + 5; private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 6; + private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 7; private static final int MAILBOX_BASE = 0x1000; private static final int MAILBOX = MAILBOX_BASE; @@ -194,6 +200,7 @@ public class EmailProvider extends ContentProvider { private static final int MESSAGE = MESSAGE_BASE; private static final int MESSAGE_ID = MESSAGE_BASE + 1; private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; + private static final int SYNCED_MESSAGE_SELECTION = MESSAGE_BASE + 3; private static final int ATTACHMENT_BASE = 0x3000; private static final int ATTACHMENT = ATTACHMENT_BASE; @@ -412,6 +419,8 @@ public class EmailProvider extends ContentProvider { * TO A SERVER VIA A SYNC ADAPTER */ matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); + matcher.addURI(EmailContent.AUTHORITY, "syncedMessageSelection", + SYNCED_MESSAGE_SELECTION); /** * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY @@ -465,6 +474,7 @@ public class EmailProvider extends ContentProvider { matcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS); matcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#", UI_DEFAULT_RECENT_FOLDERS); + matcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#", ACCOUNT_PICK_TRASH_FOLDER); } /** @@ -784,6 +794,21 @@ public class EmailProvider extends ContentProvider { return uiDeleteAccountData(uri); case UI_ACCOUNT: return uiDeleteAccount(uri); + case SYNCED_MESSAGE_SELECTION: + Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, + selectionArgs, null, null, null); + try { + if (findCursor.moveToFirst()) { + return delete(ContentUris.withAppendedId( + Message.SYNCED_CONTENT_URI, + findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), + null, null); + } else { + return 0; + } + } finally { + findCursor.close(); + } // These are cases in which one or more Messages might get deleted, either by // cascade or explicitly case MAILBOX_ID: @@ -1627,13 +1652,10 @@ public class EmailProvider extends ContentProvider { String id = "0"; try { - if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { - if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { - notifyUIConversation(uri); - } - } outer: switch (match) { + case ACCOUNT_PICK_TRASH_FOLDER: + return pickTrashFolder(uri); case UI_FOLDER: return uiUpdateFolder(uri, values); case UI_RECENT_FOLDERS: @@ -1706,6 +1728,21 @@ outer: } } break; + case SYNCED_MESSAGE_SELECTION: + Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, + selectionArgs, null, null, null); + try { + if (findCursor.moveToFirst()) { + return update(ContentUris.withAppendedId( + Message.SYNCED_CONTENT_URI, + findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), + values, null, null); + } else { + return 0; + } + } finally { + findCursor.close(); + } case SYNCED_MESSAGE_ID: case UPDATED_MESSAGE_ID: case MESSAGE_ID: @@ -1741,7 +1778,11 @@ outer: cache.unlock(id, values); } } - if (match == ATTACHMENT_ID) { + if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { + if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { + notifyUIConversation(uri); + } + } else if (match == ATTACHMENT_ID) { long attId = Integer.parseInt(id); if (values.containsKey(Attachment.FLAGS)) { int flags = values.getAsInteger(Attachment.FLAGS); @@ -3698,8 +3739,12 @@ outer: return update(ourUri, ourValues, null, null); } + public static final String PICKER_UI_ACCOUNT = "picker_ui_account"; + public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type"; + public static final String PICKER_MESSAGE_ID = "picker_message_id"; + private int uiDeleteMessage(Uri uri) { - Context context = getContext(); + final Context context = getContext(); Message msg = getMessageFromLastSegment(uri); if (msg == null) return 0; Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); @@ -3709,17 +3754,42 @@ outer: AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId); notifyUI(UIPROVIDER_FOLDER_NOTIFIER, mailbox.mId); return context.getContentResolver().delete( - ContentUris.withAppendedId(Message.CONTENT_URI, msg.mId), null, null); + ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null); } Mailbox trashMailbox = Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); - if (trashMailbox == null) return 0; + if (trashMailbox == null) { + return 0; + } ContentValues values = new ContentValues(); values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId); notifyUI(UIPROVIDER_FOLDER_NOTIFIER, mailbox.mId); return uiUpdateMessage(uri, values); } + private int pickTrashFolder(Uri uri) { + Context context = getContext(); + Long acctId = Long.parseLong(uri.getLastPathSegment()); + // For push imap, for example, we want the user to select the trash mailbox + Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION, + null, null, null); + try { + if (ac.moveToFirst()) { + final com.android.mail.providers.Account uiAccount = + new com.android.mail.providers.Account(ac); + Intent intent = new Intent(context, FolderPickerActivity.class); + intent.putExtra(PICKER_UI_ACCOUNT, uiAccount); + intent.putExtra(PICKER_MAILBOX_TYPE, Mailbox.TYPE_TRASH); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + return 1; + } + return 0; + } finally { + ac.close(); + } + } + private Cursor uiUndo(String[] projection) { // First see if we have any operations saved // TODO: Make sure seq matches diff --git a/src/com/android/email/provider/FolderPickerActivity.java b/src/com/android/email/provider/FolderPickerActivity.java new file mode 100644 index 000000000..1c6b825ca --- /dev/null +++ b/src/com/android/email/provider/FolderPickerActivity.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012 Google Inc. + * Licensed to 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.provider; + +import android.app.Activity; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Intent; +import android.os.Bundle; + +import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.provider.EmailContent.MailboxColumns; +import com.android.mail.providers.Folder; + +public class FolderPickerActivity extends Activity implements FolderPickerCallback { + private long mAccountId; + private int mMailboxType; + + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + Intent i = getIntent(); + com.android.mail.providers.Account account = + i.getParcelableExtra(EmailProvider.PICKER_UI_ACCOUNT); + mAccountId = Long.parseLong(account.uri.getLastPathSegment()); + mMailboxType = i.getIntExtra(EmailProvider.PICKER_MAILBOX_TYPE, -1); + new FolderSelectionDialog(this, account, this).show(); + } + + @Override + public void select(Folder folder) { + String folderId = folder.uri.getLastPathSegment(); + Long id = Long.parseLong(folderId); + ContentValues values = new ContentValues(); + + // If we already have a mailbox of this type, change it back to generic mail type + Mailbox ofType = Mailbox.restoreMailboxOfType(this, mAccountId, mMailboxType); + if (ofType != null) { + values.put(MailboxColumns.TYPE, Mailbox.TYPE_MAIL); + getContentResolver().update( + ContentUris.withAppendedId(Mailbox.CONTENT_URI, ofType.mId), values, + null, null); + } + + // Change this mailbox to be of the desired type + Mailbox mailbox = Mailbox.restoreMailboxWithId(this, id); + if (mailbox != null) { + values.put(MailboxColumns.TYPE, mMailboxType); + getContentResolver().update( + ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailbox.mId), values, + null, null); + } + finish(); + } + + @Override + public void create() { + // TODO: Not sure about this... + finish(); + } +} diff --git a/src/com/android/email/provider/FolderPickerCallback.java b/src/com/android/email/provider/FolderPickerCallback.java new file mode 100644 index 000000000..b84d13801 --- /dev/null +++ b/src/com/android/email/provider/FolderPickerCallback.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2012 Google Inc. + * Licensed to 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.provider; + +import com.android.mail.providers.Folder; + +public interface FolderPickerCallback { + public void select(Folder folder); + public void create(); +} diff --git a/src/com/android/email/provider/FolderSelectionDialog.java b/src/com/android/email/provider/FolderSelectionDialog.java new file mode 100644 index 000000000..b15a455f8 --- /dev/null +++ b/src/com/android/email/provider/FolderSelectionDialog.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2012 Google Inc. + * Licensed to 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.provider; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnMultiChoiceClickListener; +import android.database.Cursor; +import android.view.View; +import android.widget.AdapterView; + +import com.android.mail.R; +import com.android.mail.providers.Account; +import com.android.mail.providers.Folder; +import com.android.mail.providers.UIProvider; +import com.android.mail.ui.FolderSelectorAdapter; +import com.android.mail.ui.FolderSelectorAdapter.FolderRow; +import com.android.mail.ui.HierarchicalFolderSelectorAdapter; +import com.android.mail.ui.SeparatedFolderListAdapter; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map.Entry; + +public class FolderSelectionDialog implements OnClickListener, OnMultiChoiceClickListener { + private AlertDialog mDialog; + private HashMap mCheckedState; + private SeparatedFolderListAdapter mAdapter; + final private FolderPickerCallback mCallback; + + public FolderSelectionDialog(final Context context, Account account, + FolderPickerCallback callback) { + mCallback = callback; + // Mapping of a folder's uri to its checked state + mCheckedState = new HashMap(); + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.trash_folder_selection_title); + builder.setPositiveButton(R.string.ok, this); + builder.setNegativeButton(R.string.create_new_folder, this); + final Cursor foldersCursor = context.getContentResolver().query( + account.fullFolderListUri != null ? account.fullFolderListUri + : account.folderListUri, UIProvider.FOLDERS_PROJECTION, null, null, null); + try { + mAdapter = new SeparatedFolderListAdapter(context); + String[] headers = context.getResources() + .getStringArray(R.array.moveto_folder_sections); + mAdapter.addSection(headers[2], new HierarchicalFolderSelectorAdapter(context, + foldersCursor, new HashSet(), true)); + builder.setAdapter(mAdapter, this); + } finally { + foldersCursor.close(); + } + mDialog = builder.create(); + } + + public void show() { + mDialog.show(); + mDialog.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Object item = mAdapter.getItem(position); + if (item instanceof FolderRow) { + update((FolderRow) item); + } + } + }); + } + + /** + * Call this to update the state of folders as a result of them being + * selected / de-selected. + * + * @param row The item being updated. + */ + public void update(FolderSelectorAdapter.FolderRow row) { + // Update the UI + final boolean add = !row.isPresent(); + if (!add) { + // This would remove the check on a single radio button, so just + // return. + return; + } + // Clear any other checked items. + mAdapter.getCount(); + for (int i = 0; i < mAdapter.getCount(); i++) { + Object item = mAdapter.getItem(i); + if (item instanceof FolderRow) { + ((FolderRow)item).setIsPresent(false); + } + } + mCheckedState.clear(); + row.setIsPresent(add); + mAdapter.notifyDataSetChanged(); + mCheckedState.put(row.getFolder(), add); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + Folder folder = null; + for (Entry entry : mCheckedState.entrySet()) { + if (entry.getValue()) { + folder = entry.getKey(); + break; + } + } + mCallback.select(folder); + break; + case DialogInterface.BUTTON_NEGATIVE: + mCallback.create(); + break; + default: + onClick(dialog, which, true); + break; + } + } + + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + final FolderRow row = (FolderRow) mAdapter.getItem(which); + // Clear any other checked items. + mCheckedState.clear(); + isChecked = true; + mCheckedState.put(row.getFolder(), isChecked); + mDialog.getListView().setItemChecked(which, false); + } +} diff --git a/src/com/android/email/service/AccountService.java b/src/com/android/email/service/AccountService.java index 9ca434ee2..56f432a7d 100644 --- a/src/com/android/email/service/AccountService.java +++ b/src/com/android/email/service/AccountService.java @@ -26,11 +26,11 @@ import android.os.IBinder; import com.android.email.NotificationController; import com.android.email.ResourceHelper; -import com.android.email.VendorPolicyLoader; import com.android.email.provider.AccountReconciler; import com.android.email2.ui.MailActivityEmail; import com.android.emailcommon.Configuration; import com.android.emailcommon.Device; +import com.android.emailcommon.VendorPolicyLoader; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.service.IAccountService; diff --git a/src/com/android/email/service/EmailBroadcastProcessorService.java b/src/com/android/email/service/EmailBroadcastProcessorService.java index a3d2ec05f..ecf8569af 100644 --- a/src/com/android/email/service/EmailBroadcastProcessorService.java +++ b/src/com/android/email/service/EmailBroadcastProcessorService.java @@ -32,9 +32,9 @@ import android.util.Log; import com.android.email.NotificationController; import com.android.email.Preferences; import com.android.email.SecurityPolicy; -import com.android.email.VendorPolicyLoader; import com.android.email.activity.setup.AccountSettings; import com.android.emailcommon.Logging; +import com.android.emailcommon.VendorPolicyLoader; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.HostAuth; diff --git a/src/com/android/email/service/Imap2AuthenticatorService.java b/src/com/android/email/service/Imap2AuthenticatorService.java new file mode 100644 index 000000000..f19104920 --- /dev/null +++ b/src/com/android/email/service/Imap2AuthenticatorService.java @@ -0,0 +1,23 @@ +/* + * 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; + +/** + * This service needs to be declared separately from the base service + */ +public class Imap2AuthenticatorService extends AuthenticatorService { +} diff --git a/src/com/android/email/service/ImapService.java b/src/com/android/email/service/ImapService.java index 693f419b0..c18f2e57d 100644 --- a/src/com/android/email/service/ImapService.java +++ b/src/com/android/email/service/ImapService.java @@ -56,6 +56,7 @@ import com.android.emailcommon.provider.EmailContent.MailboxColumns; import com.android.emailcommon.provider.EmailContent.MessageColumns; import com.android.emailcommon.provider.EmailContent.SyncColumns; import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.service.EmailServiceCallback; import com.android.emailcommon.service.EmailServiceStatus; import com.android.emailcommon.service.IEmailServiceCallback; import com.android.emailcommon.service.SearchParams; @@ -106,102 +107,8 @@ public class ImapService extends Service { private static final RemoteCallbackList mCallbackList = new RemoteCallbackList(); - private interface ServiceCallbackWrapper { - public void call(IEmailServiceCallback cb) throws RemoteException; - } - - /** - * Proxy that can be used by various sync adapters to tie into ExchangeService's callback system - * Used this way: ExchangeService.callback().callbackMethod(args...); - * The proxy wraps checking for existence of a ExchangeService instance - * Failures of these callbacks can be safely ignored. - */ - static private final IEmailServiceCallback.Stub sCallbackProxy = - new IEmailServiceCallback.Stub() { - - /** - * Broadcast a callback to the everyone that's registered - * - * @param wrapper the ServiceCallbackWrapper used in the broadcast - */ - private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { - RemoteCallbackList callbackList = mCallbackList; - if (callbackList != null) { - // Call everyone on our callback list - int count = callbackList.beginBroadcast(); - try { - for (int i = 0; i < count; i++) { - try { - wrapper.call(callbackList.getBroadcastItem(i)); - } catch (RemoteException e) { - // Safe to ignore - } catch (RuntimeException e) { - // We don't want an exception in one call to prevent other calls, so - // we'll just log this and continue - Log.e(TAG, "Caught RuntimeException in broadcast", e); - } - } - } finally { - // No matter what, we need to finish the broadcast - callbackList.finishBroadcast(); - } - } - } - - @Override - public void loadAttachmentStatus(final long messageId, final long attachmentId, - final int status, final int progress) { - broadcastCallback(new ServiceCallbackWrapper() { - @Override - public void call(IEmailServiceCallback cb) throws RemoteException { - cb.loadAttachmentStatus(messageId, attachmentId, status, progress); - } - }); - } - - @Override - public void loadMessageStatus(final long messageId, final int status, final int progress) { - broadcastCallback(new ServiceCallbackWrapper() { - @Override - public void call(IEmailServiceCallback cb) throws RemoteException { - cb.loadMessageStatus(messageId, status, progress); - } - }); - } - - @Override - public void sendMessageStatus(final long messageId, final String subject, final int status, - final int progress) { - broadcastCallback(new ServiceCallbackWrapper() { - @Override - public void call(IEmailServiceCallback cb) throws RemoteException { - cb.sendMessageStatus(messageId, subject, status, progress); - } - }); - } - - @Override - public void syncMailboxListStatus(final long accountId, final int status, - final int progress) { - broadcastCallback(new ServiceCallbackWrapper() { - @Override - public void call(IEmailServiceCallback cb) throws RemoteException { - cb.syncMailboxListStatus(accountId, status, progress); - } - }); - } - - @Override - public void syncMailboxStatus(final long mailboxId, final int status, - final int progress) { - broadcastCallback(new ServiceCallbackWrapper() { - @Override - public void call(IEmailServiceCallback cb) throws RemoteException { - cb.syncMailboxStatus(mailboxId, status, progress); - } - }); - } - }; + private static final EmailServiceCallback sCallbackProxy = + new EmailServiceCallback(mCallbackList); /** * Create our EmailService implementation here. @@ -243,10 +150,7 @@ public class ImapService extends Service { } private static void sendMailboxStatus(Mailbox mailbox, int status) { - try { - sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0); - } catch (RemoteException e) { - } + sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0); } /** diff --git a/src/com/beetstra/ThirdPartyProject.prop b/src/com/beetstra/ThirdPartyProject.prop deleted file mode 100644 index 421e8cca6..000000000 --- a/src/com/beetstra/ThirdPartyProject.prop +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright 2010 Google Inc. All Rights Reserved. -#Fri Jul 16 10:03:09 PDT 2010 -currentVersion=1.0.0 -version=1.0.0 -isNative=false -name=utf7_support -keywords=utf7 support -onDevice=true -homepage=http\://sourceforge.net/projects/jutf7/