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