EmailServiceStub.java revision 9a1f00bee4c50c128df320a3795cfc8295d5e011
1/* Copyright (C) 2012 The Android Open Source Project 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16package com.android.email.service; 17 18import android.content.ContentResolver; 19import android.content.ContentUris; 20import android.content.ContentValues; 21import android.content.Context; 22import android.database.Cursor; 23import android.net.TrafficStats; 24import android.net.Uri; 25import android.os.Bundle; 26import android.os.RemoteException; 27import android.text.TextUtils; 28 29import com.android.email.NotificationController; 30import com.android.email.mail.Sender; 31import com.android.email.mail.Store; 32import com.android.email.provider.Utilities; 33import com.android.email.service.EmailServiceUtils.EmailServiceInfo; 34import com.android.email2.ui.MailActivityEmail; 35import com.android.emailcommon.Api; 36import com.android.emailcommon.Logging; 37import com.android.emailcommon.TrafficFlags; 38import com.android.emailcommon.internet.MimeBodyPart; 39import com.android.emailcommon.internet.MimeHeader; 40import com.android.emailcommon.internet.MimeMultipart; 41import com.android.emailcommon.mail.AuthenticationFailedException; 42import com.android.emailcommon.mail.FetchProfile; 43import com.android.emailcommon.mail.Folder; 44import com.android.emailcommon.mail.Folder.MessageRetrievalListener; 45import com.android.emailcommon.mail.Folder.OpenMode; 46import com.android.emailcommon.mail.Message; 47import com.android.emailcommon.mail.MessagingException; 48import com.android.emailcommon.provider.Account; 49import com.android.emailcommon.provider.EmailContent; 50import com.android.emailcommon.provider.EmailContent.Attachment; 51import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 52import com.android.emailcommon.provider.EmailContent.Body; 53import com.android.emailcommon.provider.EmailContent.BodyColumns; 54import com.android.emailcommon.provider.EmailContent.MailboxColumns; 55import com.android.emailcommon.provider.EmailContent.MessageColumns; 56import com.android.emailcommon.provider.HostAuth; 57import com.android.emailcommon.provider.Mailbox; 58import com.android.emailcommon.service.EmailServiceStatus; 59import com.android.emailcommon.service.IEmailService; 60import com.android.emailcommon.service.IEmailServiceCallback; 61import com.android.emailcommon.service.SearchParams; 62import com.android.emailcommon.utility.AttachmentUtilities; 63import com.android.emailcommon.utility.Utility; 64import com.android.mail.providers.UIProvider; 65import com.android.mail.utils.LogUtils; 66 67import java.util.HashSet; 68 69/** 70 * EmailServiceStub is an abstract class representing an EmailService 71 * 72 * This class provides legacy support for a few methods that are common to both 73 * IMAP and POP3, including startSync, loadMore, loadAttachment, and sendMail 74 */ 75public abstract class EmailServiceStub extends IEmailService.Stub implements IEmailService { 76 77 private static final int MAILBOX_COLUMN_ID = 0; 78 private static final int MAILBOX_COLUMN_SERVER_ID = 1; 79 private static final int MAILBOX_COLUMN_TYPE = 2; 80 81 /** System folders that should always exist. */ 82 private final int[] DEFAULT_FOLDERS = { 83 Mailbox.TYPE_INBOX, 84 Mailbox.TYPE_DRAFTS, 85 Mailbox.TYPE_OUTBOX, 86 Mailbox.TYPE_SENT, 87 Mailbox.TYPE_TRASH 88 }; 89 90 /** Small projection for just the columns required for a sync. */ 91 private static final String[] MAILBOX_PROJECTION = new String[] { 92 MailboxColumns.ID, 93 MailboxColumns.SERVER_ID, 94 MailboxColumns.TYPE, 95 }; 96 97 protected Context mContext; 98 private IEmailServiceCallback.Stub mCallback; 99 100 protected void init(Context context, IEmailServiceCallback.Stub callbackProxy) { 101 mContext = context; 102 mCallback = callbackProxy; 103 } 104 105 @Override 106 public Bundle validate(HostAuth hostauth) throws RemoteException { 107 // TODO Auto-generated method stub 108 return null; 109 } 110 111 @Deprecated 112 @Override 113 public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount) 114 throws RemoteException { 115 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 116 if (mailbox == null) return; 117 Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); 118 if (account == null) return; 119 EmailServiceInfo info = EmailServiceUtils.getServiceInfoForAccount(mContext, account.mId); 120 android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress, 121 info.accountType); 122 Bundle extras = new Bundle(); 123 if (userRequest) { 124 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 125 extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); 126 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); 127 } 128 extras.putLong(Mailbox.SYNC_EXTRA_MAILBOX_ID, mailboxId); 129 if (deltaMessageCount != 0) { 130 extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount); 131 } 132 ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras); 133 } 134 135 @Override 136 public void stopSync(long mailboxId) throws RemoteException { 137 // Not required 138 } 139 140 @Override 141 public void loadMore(long messageId) throws RemoteException { 142 // Load a message for view... 143 try { 144 // 1. Resample the message, in case it disappeared or synced while 145 // this command was in queue 146 EmailContent.Message message = 147 EmailContent.Message.restoreMessageWithId(mContext, messageId); 148 if (message == null) { 149 mCallback.loadMessageStatus(messageId, 150 EmailServiceStatus.MESSAGE_NOT_FOUND, 0); 151 return; 152 } 153 if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) { 154 // We should NEVER get here 155 mCallback.loadMessageStatus(messageId, 0, 100); 156 return; 157 } 158 159 // 2. Open the remote folder. 160 // TODO combine with common code in loadAttachment 161 Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); 162 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); 163 if (account == null || mailbox == null) { 164 //mListeners.loadMessageForViewFailed(messageId, "null account or mailbox"); 165 return; 166 } 167 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); 168 169 Store remoteStore = Store.getInstance(account, mContext); 170 String remoteServerId = mailbox.mServerId; 171 // If this is a search result, use the protocolSearchInfo field to get the 172 // correct remote location 173 if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) { 174 remoteServerId = message.mProtocolSearchInfo; 175 } 176 Folder remoteFolder = remoteStore.getFolder(remoteServerId); 177 remoteFolder.open(OpenMode.READ_WRITE); 178 179 // 3. Set up to download the entire message 180 Message remoteMessage = remoteFolder.getMessage(message.mServerId); 181 FetchProfile fp = new FetchProfile(); 182 fp.add(FetchProfile.Item.BODY); 183 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 184 185 // 4. Write to provider 186 Utilities.copyOneMessageToProvider(mContext, remoteMessage, account, mailbox, 187 EmailContent.Message.FLAG_LOADED_COMPLETE); 188 189 // 5. Notify UI 190 mCallback.loadMessageStatus(messageId, 0, 100); 191 192 } catch (MessagingException me) { 193 if (Logging.LOGD) LogUtils.v(Logging.LOG_TAG, "", me); 194 mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0); 195 } catch (RuntimeException rte) { 196 mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0); 197 } 198 } 199 200 private void doProgressCallback(long messageId, long attachmentId, int progress) { 201 try { 202 mCallback.loadAttachmentStatus(messageId, attachmentId, 203 EmailServiceStatus.IN_PROGRESS, progress); 204 } catch (RemoteException e) { 205 // No danger if the client is no longer around 206 } 207 } 208 209 // TODO: Switch from using setCallback to the explicitly passed callback. 210 @Override 211 public void loadAttachment(final IEmailServiceCallback cb, final long attachmentId, 212 final boolean background) throws RemoteException { 213 try { 214 //1. Check if the attachment is already here and return early in that case 215 Attachment attachment = 216 Attachment.restoreAttachmentWithId(mContext, attachmentId); 217 if (attachment == null) { 218 mCallback.loadAttachmentStatus(0, attachmentId, 219 EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0); 220 return; 221 } 222 long messageId = attachment.mMessageKey; 223 224 EmailContent.Message message = 225 EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey); 226 if (message == null) { 227 mCallback.loadAttachmentStatus(messageId, attachmentId, 228 EmailServiceStatus.MESSAGE_NOT_FOUND, 0); 229 } 230 231 // If the message is loaded, just report that we're finished 232 if (Utility.attachmentExists(mContext, attachment)) { 233 mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 234 0); 235 return; 236 } 237 238 // Say we're starting... 239 doProgressCallback(messageId, attachmentId, 0); 240 241 // 2. Open the remote folder. 242 Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); 243 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); 244 245 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 246 long sourceId = Utility.getFirstRowLong(mContext, Body.CONTENT_URI, 247 new String[] {BodyColumns.SOURCE_MESSAGE_KEY}, 248 BodyColumns.MESSAGE_KEY + "=?", 249 new String[] {Long.toString(messageId)}, null, 0, -1L); 250 if (sourceId != -1 ) { 251 EmailContent.Message sourceMsg = 252 EmailContent.Message.restoreMessageWithId(mContext, sourceId); 253 if (sourceMsg != null) { 254 mailbox = Mailbox.restoreMailboxWithId(mContext, sourceMsg.mMailboxKey); 255 message.mServerId = sourceMsg.mServerId; 256 } 257 } 258 } 259 260 if (account == null || mailbox == null) { 261 // If the account/mailbox are gone, just report success; the UI handles this 262 mCallback.loadAttachmentStatus(messageId, attachmentId, 263 EmailServiceStatus.SUCCESS, 0); 264 return; 265 } 266 TrafficStats.setThreadStatsTag( 267 TrafficFlags.getAttachmentFlags(mContext, account)); 268 269 Store remoteStore = Store.getInstance(account, mContext); 270 Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); 271 remoteFolder.open(OpenMode.READ_WRITE); 272 273 // 3. Generate a shell message in which to retrieve the attachment, 274 // and a shell BodyPart for the attachment. Then glue them together. 275 Message storeMessage = remoteFolder.createMessage(message.mServerId); 276 MimeBodyPart storePart = new MimeBodyPart(); 277 storePart.setSize((int)attachment.mSize); 278 storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, 279 attachment.mLocation); 280 storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, 281 String.format("%s;\n name=\"%s\"", 282 attachment.mMimeType, 283 attachment.mFileName)); 284 // TODO is this always true for attachments? I think we dropped the 285 // true encoding along the way 286 storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); 287 288 MimeMultipart multipart = new MimeMultipart(); 289 multipart.setSubType("mixed"); 290 multipart.addBodyPart(storePart); 291 292 storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); 293 storeMessage.setBody(multipart); 294 295 // 4. Now ask for the attachment to be fetched 296 FetchProfile fp = new FetchProfile(); 297 fp.add(storePart); 298 remoteFolder.fetch(new Message[] { storeMessage }, fp, 299 new MessageRetrievalListenerBridge(messageId, attachmentId)); 300 301 // If we failed to load the attachment, throw an Exception here, so that 302 // AttachmentDownloadService knows that we failed 303 if (storePart.getBody() == null) { 304 throw new MessagingException("Attachment not loaded."); 305 } 306 307 // Save the attachment to wherever it's going 308 AttachmentUtilities.saveAttachment(mContext, storePart.getBody().getInputStream(), 309 attachment); 310 311 // 6. Report success 312 mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); 313 314 // Close the connection 315 remoteFolder.close(false); 316 } 317 catch (MessagingException me) { 318 if (Logging.LOGD) LogUtils.v(Logging.LOG_TAG, "", me); 319 // TODO: Fix this up; consider the best approach 320 321 ContentValues cv = new ContentValues(); 322 cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED); 323 Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); 324 mContext.getContentResolver().update(uri, cv, null, null); 325 326 mCallback.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.CONNECTION_ERROR, 0); 327 } 328 329 } 330 331 /** 332 * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and 333 * pass down to {@link Result}. 334 */ 335 public class MessageRetrievalListenerBridge implements MessageRetrievalListener { 336 private final long mMessageId; 337 private final long mAttachmentId; 338 339 public MessageRetrievalListenerBridge(long messageId, long attachmentId) { 340 mMessageId = messageId; 341 mAttachmentId = attachmentId; 342 } 343 344 @Override 345 public void loadAttachmentProgress(int progress) { 346 doProgressCallback(mMessageId, mAttachmentId, progress); 347 } 348 349 @Override 350 public void messageRetrieved(com.android.emailcommon.mail.Message message) { 351 } 352 } 353 354 @Override 355 public void updateFolderList(long accountId) throws RemoteException { 356 Account account = Account.restoreAccountWithId(mContext, accountId); 357 if (account == null) return; 358 long inboxId = -1; 359 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); 360 Cursor localFolderCursor = null; 361 try { 362 // Step 0: Make sure the default system mailboxes exist. 363 for (int type : DEFAULT_FOLDERS) { 364 if (Mailbox.findMailboxOfType(mContext, accountId, type) == Mailbox.NO_MAILBOX) { 365 Mailbox mailbox = Mailbox.newSystemMailbox(mContext, accountId, type); 366 mailbox.save(mContext); 367 if (type == Mailbox.TYPE_INBOX) { 368 inboxId = mailbox.mId; 369 } 370 } 371 } 372 373 // Step 1: Get remote mailboxes 374 Store store = Store.getInstance(account, mContext); 375 Folder[] remoteFolders = store.updateFolders(); 376 HashSet<String> remoteFolderNames = new HashSet<String>(); 377 for (int i = 0, count = remoteFolders.length; i < count; i++) { 378 remoteFolderNames.add(remoteFolders[i].getName()); 379 } 380 381 // Step 2: Get local mailboxes 382 localFolderCursor = mContext.getContentResolver().query( 383 Mailbox.CONTENT_URI, 384 MAILBOX_PROJECTION, 385 EmailContent.MailboxColumns.ACCOUNT_KEY + "=?", 386 new String[] { String.valueOf(account.mId) }, 387 null); 388 389 // Step 3: Remove any local mailbox not on the remote list 390 while (localFolderCursor.moveToNext()) { 391 String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID); 392 // Short circuit if we have a remote mailbox with the same name 393 if (remoteFolderNames.contains(mailboxPath)) { 394 continue; 395 } 396 397 int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE); 398 long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID); 399 switch (mailboxType) { 400 case Mailbox.TYPE_INBOX: 401 case Mailbox.TYPE_DRAFTS: 402 case Mailbox.TYPE_OUTBOX: 403 case Mailbox.TYPE_SENT: 404 case Mailbox.TYPE_TRASH: 405 case Mailbox.TYPE_SEARCH: 406 // Never, ever delete special mailboxes 407 break; 408 default: 409 // Drop all attachment files related to this mailbox 410 AttachmentUtilities.deleteAllMailboxAttachmentFiles( 411 mContext, accountId, mailboxId); 412 // Delete the mailbox; database triggers take care of related 413 // Message, Body and Attachment records 414 Uri uri = ContentUris.withAppendedId( 415 Mailbox.CONTENT_URI, mailboxId); 416 mContext.getContentResolver().delete(uri, null, null); 417 break; 418 } 419 } 420 } catch (MessagingException e) { 421 // We'll hope this is temporary 422 } finally { 423 if (localFolderCursor != null) { 424 localFolderCursor.close(); 425 } 426 // If we just created the inbox, sync it 427 if (inboxId != -1) { 428 startSync(inboxId, true, 0); 429 } 430 } 431 } 432 433 @Override 434 public boolean createFolder(long accountId, String name) throws RemoteException { 435 // Not required 436 return false; 437 } 438 439 @Override 440 public boolean deleteFolder(long accountId, String name) throws RemoteException { 441 // Not required 442 return false; 443 } 444 445 @Override 446 public boolean renameFolder(long accountId, String oldName, String newName) 447 throws RemoteException { 448 // Not required 449 return false; 450 } 451 452 @Override 453 public void setCallback(IEmailServiceCallback cb) throws RemoteException { 454 // Not required 455 } 456 457 @Override 458 public void setLogging(int on) throws RemoteException { 459 // Not required 460 } 461 462 @Override 463 public void hostChanged(long accountId) throws RemoteException { 464 // Not required 465 } 466 467 @Override 468 public Bundle autoDiscover(String userName, String password) throws RemoteException { 469 // Not required 470 return null; 471 } 472 473 @Override 474 public void sendMeetingResponse(long messageId, int response) throws RemoteException { 475 // Not required 476 } 477 478 @Override 479 public void deleteAccountPIMData(final String emailAddress) throws RemoteException { 480 MailService.reconcileLocalAccountsSync(mContext); 481 } 482 483 @Override 484 public int getApiLevel() throws RemoteException { 485 return Api.LEVEL; 486 } 487 488 @Override 489 public int searchMessages(long accountId, SearchParams params, long destMailboxId) 490 throws RemoteException { 491 // Not required 492 return 0; 493 } 494 495 @Override 496 public void sendMail(long accountId) throws RemoteException { 497 sendMailImpl(mContext, accountId); 498 } 499 500 public static void sendMailImpl(Context context, long accountId) { 501 Account account = Account.restoreAccountWithId(context, accountId); 502 TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(context, account)); 503 NotificationController nc = NotificationController.getInstance(context); 504 // 1. Loop through all messages in the account's outbox 505 long outboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX); 506 if (outboxId == Mailbox.NO_MAILBOX) { 507 return; 508 } 509 ContentResolver resolver = context.getContentResolver(); 510 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 511 EmailContent.Message.ID_COLUMN_PROJECTION, 512 EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) }, 513 null); 514 try { 515 // 2. exit early 516 if (c.getCount() <= 0) { 517 return; 518 } 519 Sender sender = Sender.getInstance(context, account); 520 Store remoteStore = Store.getInstance(account, context); 521 boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder(); 522 ContentValues moveToSentValues = null; 523 if (requireMoveMessageToSentFolder) { 524 Mailbox sentFolder = 525 Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SENT); 526 moveToSentValues = new ContentValues(); 527 moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId); 528 } 529 530 // 3. loop through the available messages and send them 531 while (c.moveToNext()) { 532 long messageId = -1; 533 if (moveToSentValues != null) { 534 moveToSentValues.remove(EmailContent.MessageColumns.FLAGS); 535 } 536 try { 537 messageId = c.getLong(0); 538 // Don't send messages with unloaded attachments 539 if (Utility.hasUnloadedAttachments(context, messageId)) { 540 if (MailActivityEmail.DEBUG) { 541 LogUtils.d(Logging.LOG_TAG, "Can't send #" + messageId + 542 "; unloaded attachments"); 543 } 544 continue; 545 } 546 sender.sendMessage(messageId); 547 } catch (MessagingException me) { 548 // report error for this message, but keep trying others 549 if (me instanceof AuthenticationFailedException) { 550 nc.showLoginFailedNotification(account.mId); 551 } 552 continue; 553 } 554 // 4. move to sent, or delete 555 final Uri syncedUri = 556 ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); 557 // Delete all cached files 558 AttachmentUtilities.deleteAllCachedAttachmentFiles(context, account.mId, messageId); 559 if (requireMoveMessageToSentFolder) { 560 // If this is a forwarded message and it has attachments, delete them, as they 561 // duplicate information found elsewhere (on the server). This saves storage. 562 final EmailContent.Message msg = 563 EmailContent.Message.restoreMessageWithId(context, messageId); 564 if (msg != null && 565 ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) { 566 AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, 567 messageId); 568 } 569 final int flags = msg.mFlags & ~(EmailContent.Message.FLAG_TYPE_REPLY | 570 EmailContent.Message.FLAG_TYPE_FORWARD); 571 moveToSentValues.put(EmailContent.MessageColumns.FLAGS, flags); 572 resolver.update(syncedUri, moveToSentValues, null, null); 573 } else { 574 AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, 575 messageId); 576 final Uri uri = 577 ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); 578 resolver.delete(uri, null, null); 579 resolver.delete(syncedUri, null, null); 580 } 581 } 582 nc.cancelLoginFailedNotification(account.mId); 583 } catch (MessagingException me) { 584 if (me instanceof AuthenticationFailedException) { 585 nc.showLoginFailedNotification(account.mId); 586 } 587 } finally { 588 c.close(); 589 } 590 591 } 592} 593