1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.email.service; 18 19 import android.app.Service; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.database.Cursor; 26 import android.net.TrafficStats; 27 import android.net.Uri; 28 import android.os.IBinder; 29 import android.os.RemoteException; 30 31 import com.android.email.NotificationController; 32 import com.android.email.mail.Store; 33 import com.android.email.mail.store.Pop3Store; 34 import com.android.email.mail.store.Pop3Store.Pop3Folder; 35 import com.android.email.mail.store.Pop3Store.Pop3Message; 36 import com.android.email.provider.Utilities; 37 import com.android.email2.ui.MailActivityEmail; 38 import com.android.emailcommon.Logging; 39 import com.android.emailcommon.TrafficFlags; 40 import com.android.emailcommon.mail.AuthenticationFailedException; 41 import com.android.emailcommon.mail.Folder.OpenMode; 42 import com.android.emailcommon.mail.MessagingException; 43 import com.android.emailcommon.provider.Account; 44 import com.android.emailcommon.provider.EmailContent; 45 import com.android.emailcommon.provider.EmailContent.Attachment; 46 import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 47 import com.android.emailcommon.provider.EmailContent.Message; 48 import com.android.emailcommon.provider.EmailContent.MessageColumns; 49 import com.android.emailcommon.provider.EmailContent.SyncColumns; 50 import com.android.emailcommon.provider.Mailbox; 51 import com.android.emailcommon.service.EmailServiceStatus; 52 import com.android.emailcommon.service.IEmailServiceCallback; 53 import com.android.emailcommon.utility.AttachmentUtilities; 54 import com.android.mail.providers.UIProvider; 55 import com.android.mail.providers.UIProvider.AccountCapabilities; 56 import com.android.mail.providers.UIProvider.AttachmentState; 57 import com.android.mail.utils.LogUtils; 58 59 import org.apache.james.mime4j.EOLConvertingInputStream; 60 61 import java.io.IOException; 62 import java.util.ArrayList; 63 import java.util.HashMap; 64 import java.util.HashSet; 65 66 public class Pop3Service extends Service { 67 private static final String TAG = "Pop3Service"; 68 private static final int DEFAULT_SYNC_COUNT = 100; 69 70 @Override 71 public int onStartCommand(Intent intent, int flags, int startId) { 72 return Service.START_STICKY; 73 } 74 75 /** 76 * Create our EmailService implementation here. 77 */ 78 private final EmailServiceStub mBinder = new EmailServiceStub() { 79 @Override 80 public int getCapabilities(Account acct) throws RemoteException { 81 return AccountCapabilities.UNDO | 82 AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; 83 } 84 85 @Override 86 public void loadAttachment(final IEmailServiceCallback callback, final long attachmentId, 87 final boolean background) throws RemoteException { 88 Attachment att = Attachment.restoreAttachmentWithId(mContext, attachmentId); 89 if (att == null || att.mUiState != AttachmentState.DOWNLOADING) return; 90 long inboxId = Mailbox.findMailboxOfType(mContext, att.mAccountKey, Mailbox.TYPE_INBOX); 91 if (inboxId == Mailbox.NO_MAILBOX) return; 92 // We load attachments during a sync 93 startSync(inboxId, true, 0); 94 } 95 96 @Override 97 public void serviceUpdated(String emailAddress) throws RemoteException { 98 // Not required for POP3 99 } 100 }; 101 102 @Override 103 public IBinder onBind(Intent intent) { 104 mBinder.init(this); 105 return mBinder; 106 } 107 108 /** 109 * Start foreground synchronization of the specified folder. This is called 110 * by synchronizeMailbox or checkMail. TODO this should use ID's instead of 111 * fully-restored objects 112 * 113 * @param account 114 * @param folder 115 * @param deltaMessageCount the requested change in number of messages to sync. 116 * @return The status code for whether this operation succeeded. 117 * @throws MessagingException 118 */ 119 public static int synchronizeMailboxSynchronous(Context context, final Account account, 120 final Mailbox folder, final int deltaMessageCount) throws MessagingException { 121 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account)); 122 NotificationController nc = NotificationController.getInstance(context); 123 try { 124 synchronizePop3Mailbox(context, account, folder, deltaMessageCount); 125 // Clear authentication notification for this account 126 nc.cancelLoginFailedNotification(account.mId); 127 } catch (MessagingException e) { 128 if (Logging.LOGD) { 129 LogUtils.v(Logging.LOG_TAG, "synchronizeMailbox", e); 130 } 131 if (e instanceof AuthenticationFailedException) { 132 // Generate authentication notification 133 nc.showLoginFailedNotification(account.mId); 134 } 135 throw e; 136 } 137 // TODO: Rather than use exceptions as logic aobve, return the status and handle it 138 // correctly in caller. 139 return EmailServiceStatus.SUCCESS; 140 } 141 142 /** 143 * Lightweight record for the first pass of message sync, where I'm just 144 * seeing if the local message requires sync. Later (for messages that need 145 * syncing) we'll do a full readout from the DB. 146 */ 147 private static class LocalMessageInfo { 148 private static final int COLUMN_ID = 0; 149 private static final int COLUMN_FLAG_LOADED = 1; 150 private static final int COLUMN_SERVER_ID = 2; 151 private static final String[] PROJECTION = new String[] { 152 EmailContent.RECORD_ID, MessageColumns.FLAG_LOADED, SyncColumns.SERVER_ID 153 }; 154 155 final long mId; 156 final int mFlagLoaded; 157 final String mServerId; 158 159 public LocalMessageInfo(Cursor c) { 160 mId = c.getLong(COLUMN_ID); 161 mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED); 162 mServerId = c.getString(COLUMN_SERVER_ID); 163 // Note: mailbox key and account key not needed - they are projected 164 // for the SELECT 165 } 166 } 167 168 /** 169 * Load the structure and body of messages not yet synced 170 * 171 * @param account the account we're syncing 172 * @param remoteFolder the (open) Folder we're working on 173 * @param unsyncedMessages an array of Message's we've got headers for 174 * @param toMailbox the destination mailbox we're syncing 175 * @throws MessagingException 176 */ 177 static void loadUnsyncedMessages(final Context context, final Account account, 178 Pop3Folder remoteFolder, ArrayList<Pop3Message> unsyncedMessages, 179 final Mailbox toMailbox) throws MessagingException { 180 181 if (MailActivityEmail.DEBUG) { 182 LogUtils.d(TAG, "Loading " + unsyncedMessages.size() + " unsynced messages"); 183 } 184 185 try { 186 int cnt = unsyncedMessages.size(); 187 // They are in most recent to least recent order, process them that way. 188 for (int i = 0; i < cnt; i++) { 189 final Pop3Message message = unsyncedMessages.get(i); 190 remoteFolder.fetchBody(message, Pop3Store.FETCH_BODY_SANE_SUGGESTED_SIZE / 76, 191 null); 192 int flag = EmailContent.Message.FLAG_LOADED_COMPLETE; 193 if (!message.isComplete()) { 194 // TODO: when the message is not complete, this should mark the message as 195 // partial. When that change is made, we need to make sure that: 196 // 1) Partial messages are shown in the conversation list 197 // 2) We are able to download the rest of the message/attachment when the 198 // user requests it. 199 flag = EmailContent.Message.FLAG_LOADED_PARTIAL; 200 } 201 if (MailActivityEmail.DEBUG) { 202 LogUtils.d(TAG, "Message is " + (message.isComplete() ? "" : "NOT ") 203 + "complete"); 204 } 205 // If message is incomplete, create a "fake" attachment 206 Utilities.copyOneMessageToProvider(context, message, account, toMailbox, flag); 207 } 208 } catch (IOException e) { 209 throw new MessagingException(MessagingException.IOERROR); 210 } 211 } 212 213 private static class FetchCallback implements EOLConvertingInputStream.Callback { 214 private final ContentResolver mResolver; 215 private final Uri mAttachmentUri; 216 private final ContentValues mContentValues = new ContentValues(); 217 218 FetchCallback(ContentResolver resolver, Uri attachmentUri) { 219 mResolver = resolver; 220 mAttachmentUri = attachmentUri; 221 } 222 223 @Override 224 public void report(int bytesRead) { 225 mContentValues.put(AttachmentColumns.UI_DOWNLOADED_SIZE, bytesRead); 226 mResolver.update(mAttachmentUri, mContentValues, null, null); 227 } 228 } 229 230 /** 231 * Synchronizer 232 * 233 * @param account the account to sync 234 * @param mailbox the mailbox to sync 235 * @param deltaMessageCount the requested change to number of messages to sync 236 * @throws MessagingException 237 */ 238 private synchronized static void synchronizePop3Mailbox(final Context context, final Account account, 239 final Mailbox mailbox, final int deltaMessageCount) throws MessagingException { 240 // TODO Break this into smaller pieces 241 ContentResolver resolver = context.getContentResolver(); 242 243 // We only sync Inbox 244 if (mailbox.mType != Mailbox.TYPE_INBOX) { 245 return; 246 } 247 248 // Get the message list from EmailProvider and create an index of the uids 249 250 Cursor localUidCursor = null; 251 HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>(); 252 253 try { 254 localUidCursor = resolver.query( 255 EmailContent.Message.CONTENT_URI, 256 LocalMessageInfo.PROJECTION, 257 MessageColumns.MAILBOX_KEY + "=?", 258 new String[] { 259 String.valueOf(mailbox.mId) 260 }, 261 null); 262 while (localUidCursor.moveToNext()) { 263 LocalMessageInfo info = new LocalMessageInfo(localUidCursor); 264 localMessageMap.put(info.mServerId, info); 265 } 266 } finally { 267 if (localUidCursor != null) { 268 localUidCursor.close(); 269 } 270 } 271 272 // Open the remote folder and create the remote folder if necessary 273 274 Pop3Store remoteStore = (Pop3Store)Store.getInstance(account, context); 275 // The account might have been deleted 276 if (remoteStore == null) 277 return; 278 Pop3Folder remoteFolder = (Pop3Folder)remoteStore.getFolder(mailbox.mServerId); 279 280 // Open the remote folder. This pre-loads certain metadata like message 281 // count. 282 remoteFolder.open(OpenMode.READ_WRITE); 283 284 String[] accountIdArgs = new String[] { Long.toString(account.mId) }; 285 long trashMailboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_TRASH); 286 Cursor updates = resolver.query( 287 EmailContent.Message.UPDATED_CONTENT_URI, 288 EmailContent.Message.ID_COLUMN_PROJECTION, 289 EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, 290 null); 291 try { 292 // loop through messages marked as deleted 293 while (updates.moveToNext()) { 294 long id = updates.getLong(Message.ID_COLUMNS_ID_COLUMN); 295 EmailContent.Message currentMsg = 296 EmailContent.Message.restoreMessageWithId(context, id); 297 if (currentMsg.mMailboxKey == trashMailboxId) { 298 // Delete this on the server 299 Pop3Message popMessage = 300 (Pop3Message)remoteFolder.getMessage(currentMsg.mServerId); 301 if (popMessage != null) { 302 remoteFolder.deleteMessage(popMessage); 303 } 304 } 305 // Finally, delete the update 306 Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, id); 307 context.getContentResolver().delete(uri, null, null); 308 } 309 } finally { 310 updates.close(); 311 } 312 313 // Get the remote message count. 314 final int remoteMessageCount = remoteFolder.getMessageCount(); 315 316 // Save the folder message count. 317 mailbox.updateMessageCount(context, remoteMessageCount); 318 319 // Create a list of messages to download 320 Pop3Message[] remoteMessages = new Pop3Message[0]; 321 final ArrayList<Pop3Message> unsyncedMessages = new ArrayList<Pop3Message>(); 322 HashMap<String, Pop3Message> remoteUidMap = new HashMap<String, Pop3Message>(); 323 324 if (remoteMessageCount > 0) { 325 /* 326 * Get all messageIds in the mailbox. 327 * We don't necessarily need to sync all of them. 328 */ 329 remoteMessages = remoteFolder.getMessages(remoteMessageCount, remoteMessageCount); 330 LogUtils.d(Logging.LOG_TAG, "remoteMessageCount " + remoteMessageCount); 331 332 /* 333 * TODO: It would be nicer if the default sync window were time based rather than 334 * count based, but POP3 does not support time based queries, and the UIDL command 335 * does not report timestamps. To handle this, we would need to load a block of 336 * Ids, sync those messages to get the timestamps, and then load more Ids until we 337 * have filled out our window. 338 */ 339 int count = 0; 340 int countNeeded = DEFAULT_SYNC_COUNT; 341 for (final Pop3Message message : remoteMessages) { 342 final String uid = message.getUid(); 343 remoteUidMap.put(uid, message); 344 } 345 346 /* 347 * Figure out which messages we need to sync. Start at the most recent ones, and keep 348 * going until we hit one of four end conditions: 349 * 1. We currently have zero local messages. In this case, we will sync the most recent 350 * DEFAULT_SYNC_COUNT, then stop. 351 * 2. We have some local messages, and after encountering them, we find some older 352 * messages that do not yet exist locally. In this case, we will load whichever came 353 * before the ones we already had locally, and also deltaMessageCount additional 354 * older messages. 355 * 3. We have some local messages, but after examining the most recent 356 * DEFAULT_SYNC_COUNT remote messages, we still have not encountered any that exist 357 * locally. In this case, we'll stop adding new messages to sync, leaving a gap between 358 * the ones we've just loaded and the ones we already had. 359 * 4. We examine all of the remote messages before running into any of our count 360 * limitations. 361 */ 362 for (final Pop3Message message : remoteMessages) { 363 final String uid = message.getUid(); 364 final LocalMessageInfo localMessage = localMessageMap.get(uid); 365 if (localMessage == null) { 366 count++; 367 } else { 368 // We have found a message that already exists locally. We may or may not 369 // need to keep looking, depending on what deltaMessageCount is. 370 LogUtils.d(Logging.LOG_TAG, "found a local message, need " + 371 deltaMessageCount + " more remote messages"); 372 countNeeded = deltaMessageCount; 373 count = 0; 374 } 375 376 // localMessage == null -> message has never been created (not even headers) 377 // mFlagLoaded != FLAG_LOADED_COMPLETE -> message failed to sync completely 378 if (localMessage == null || 379 (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE && 380 localMessage.mFlagLoaded != Message.FLAG_LOADED_PARTIAL)) { 381 LogUtils.d(Logging.LOG_TAG, "need to sync " + uid); 382 unsyncedMessages.add(message); 383 } else { 384 LogUtils.d(Logging.LOG_TAG, "don't need to sync " + uid); 385 } 386 387 if (count >= countNeeded) { 388 LogUtils.d(Logging.LOG_TAG, "loaded " + count + " messages, stopping"); 389 break; 390 } 391 } 392 } else { 393 if (MailActivityEmail.DEBUG) { 394 LogUtils.d(TAG, "*** Message count is zero??"); 395 } 396 remoteFolder.close(false); 397 return; 398 } 399 400 // Get "attachments" to be loaded 401 Cursor c = resolver.query(Attachment.CONTENT_URI, Attachment.CONTENT_PROJECTION, 402 AttachmentColumns.ACCOUNT_KEY + "=? AND " + 403 AttachmentColumns.UI_STATE + "=" + AttachmentState.DOWNLOADING, 404 new String[] {Long.toString(account.mId)}, null); 405 try { 406 final ContentValues values = new ContentValues(); 407 while (c.moveToNext()) { 408 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED); 409 Attachment att = new Attachment(); 410 att.restore(c); 411 Message msg = Message.restoreMessageWithId(context, att.mMessageKey); 412 if (msg == null || (msg.mFlagLoaded == Message.FLAG_LOADED_COMPLETE)) { 413 values.put(AttachmentColumns.UI_DOWNLOADED_SIZE, att.mSize); 414 resolver.update(ContentUris.withAppendedId(Attachment.CONTENT_URI, att.mId), 415 values, null, null); 416 continue; 417 } else { 418 String uid = msg.mServerId; 419 Pop3Message popMessage = remoteUidMap.get(uid); 420 if (popMessage != null) { 421 Uri attUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, att.mId); 422 try { 423 remoteFolder.fetchBody(popMessage, -1, 424 new FetchCallback(resolver, attUri)); 425 } catch (IOException e) { 426 throw new MessagingException(MessagingException.IOERROR); 427 } 428 429 // Say we've downloaded the attachment 430 values.put(AttachmentColumns.UI_STATE, AttachmentState.SAVED); 431 resolver.update(attUri, values, null, null); 432 433 int flag = EmailContent.Message.FLAG_LOADED_COMPLETE; 434 if (!popMessage.isComplete()) { 435 LogUtils.e(TAG, "How is this possible?"); 436 } 437 Utilities.copyOneMessageToProvider( 438 context, popMessage, account, mailbox, flag); 439 // Get rid of the temporary attachment 440 resolver.delete(attUri, null, null); 441 442 } else { 443 // TODO: Should we mark this attachment as failed so we don't 444 // keep trying to download? 445 LogUtils.e(TAG, "Could not find message for attachment " + uid); 446 } 447 } 448 } 449 } finally { 450 c.close(); 451 } 452 453 // Remove any messages that are in the local store but no longer on the remote store. 454 HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet()); 455 localUidsToDelete.removeAll(remoteUidMap.keySet()); 456 for (String uidToDelete : localUidsToDelete) { 457 LogUtils.d(Logging.LOG_TAG, "need to delete " + uidToDelete); 458 LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete); 459 460 // Delete associated data (attachment files) 461 // Attachment & Body records are auto-deleted when we delete the 462 // Message record 463 AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, 464 infoToDelete.mId); 465 466 // Delete the message itself 467 Uri uriToDelete = ContentUris.withAppendedId( 468 EmailContent.Message.CONTENT_URI, infoToDelete.mId); 469 resolver.delete(uriToDelete, null, null); 470 471 // Delete extra rows (e.g. synced or deleted) 472 Uri updateRowToDelete = ContentUris.withAppendedId( 473 EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId); 474 resolver.delete(updateRowToDelete, null, null); 475 Uri deleteRowToDelete = ContentUris.withAppendedId( 476 EmailContent.Message.DELETED_CONTENT_URI, infoToDelete.mId); 477 resolver.delete(deleteRowToDelete, null, null); 478 } 479 480 LogUtils.d(TAG, "loadUnsynchedMessages " + unsyncedMessages.size()); 481 // Load messages we need to sync 482 loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox); 483 484 // Clean up and report results 485 remoteFolder.close(false); 486 } 487 } 488