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