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