/** M: Wipe the records from MessageStateChange table which belong to mMailbox */
 private void wipeMessageStateChanges() {
   Cursor c =
       mContentResolver.query(
           Message.CONTENT_URI,
           new String[] {MessageColumns.MESSAGE_ID},
           MessageColumns.MAILBOX_KEY + "=?",
           new String[] {String.valueOf(mMailbox.mId)},
           null);
   if (c == null) {
     LogUtils.i(Eas.BSK_TAG, "Get message cursor failed");
     return;
   }
   try {
     ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
     while (c.moveToNext()) {
       ops.add(
           ContentProviderOperation.newDelete(MessageStateChange.CONTENT_URI)
               .withSelection(
                   MessageChangeLogTable.MESSAGE_KEY + "=?", new String[] {c.getString(0)})
               .build());
       applyBatchIfNeeded(ops, MAX_OPS_PER_BATCH, false);
     }
     applyBatchIfNeeded(ops, MAX_OPS_PER_BATCH, true);
   } catch (RemoteException e) {
     LogUtils.i(Eas.BSK_TAG, "RemoteException when wipeMessageStateChanges");
   } catch (OperationApplicationException e) {
     LogUtils.i(Eas.BSK_TAG, "OperationApplicationException when wipeMessageStateChanges");
   } finally {
     c.close();
   }
 }
예제 #2
0
 @Deprecated
 @Override
 public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount)
     throws RemoteException {
   final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
   if (mailbox == null) return;
   final Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
   if (account == null) return;
   final EmailServiceInfo info = EmailServiceUtils.getServiceInfoForAccount(mContext, account.mId);
   final android.accounts.Account acct =
       new android.accounts.Account(account.mEmailAddress, info.accountType);
   final Bundle extras = Mailbox.createSyncBundle(mailboxId);
   if (userRequest) {
     extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
     extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
     extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
   }
   if (deltaMessageCount != 0) {
     extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount);
   }
   ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras);
   LogUtils.i(
       Logging.LOG_TAG,
       "requestSync EmailServiceStub startSync %s, %s",
       account.toString(),
       extras.toString());
 }
    @Override
    public void onLoadFinished(
        Loader<ObjectCursor<ConversationMessage>> loader, ObjectCursor<ConversationMessage> data) {
      // ignore truly duplicate results
      // this can happen when restoring after rotation
      if (mCursor == data) {
        return;
      } else {
        final MessageCursor messageCursor = (MessageCursor) data;

        // bind the cursor to this fragment so it can access to the current list controller
        messageCursor.setController(AbstractConversationViewFragment.this);

        if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
          LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
        }

        // We have no messages: exit conversation view.
        if (messageCursor.getCount() == 0
            && (!CursorStatus.isWaitingForResults(messageCursor.getStatus()) || mIsDetached)) {
          if (mUserVisible) {
            onError();
          } else {
            // we expect that the pager adapter will remove this
            // conversation fragment on its own due to a separate
            // conversation cursor update (we might get here if the
            // message list update fires first. nothing to do
            // because we expect to be torn down soon.)
            LogUtils.i(
                LOG_TAG,
                "CVF: offscreen conv has no messages, ignoring update"
                    + " in anticipation of conv cursor update. c=%s",
                mConversation.uri);
          }
          // existing mCursor will imminently be closed, must stop referencing it
          // since we expect to be kicked out soon, it doesn't matter what mCursor
          // becomes
          mCursor = null;
          return;
        }

        // ignore cursors that are still loading results
        if (!messageCursor.isLoaded()) {
          // existing mCursor will imminently be closed, must stop referencing it
          // in this case, the new cursor is also no good, and since don't expect to get
          // here except in initial load situations, it's safest to just ensure the
          // reference is null
          mCursor = null;
          return;
        }
        final MessageCursor oldCursor = mCursor;
        mCursor = messageCursor;
        onMessageCursorLoadFinished(loader, mCursor, oldCursor);
      }
    }
 private void onError() {
   // need to exit this view- conversation may have been
   // deleted, or for whatever reason is now invalid (e.g.
   // discard single draft)
   //
   // N.B. this may involve a fragment transaction, which
   // FragmentManager will refuse to execute directly
   // within onLoadFinished. Make sure the controller knows.
   LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode");
   // TODO(mindyp): handle ERROR status by showing an error
   // message to the user that there are no messages in
   // this conversation
   popOut();
 }
 @Override
 protected void wipe() {
   LogUtils.i(Eas.BSK_TAG, "Wipe the change log of mailbox " + mMailbox);
   /**
    * M: With the "Bad Sync Key" recovery mechanism, we no longer wipe all the local mails
    * considering the network data usage for re-synchronizing. We just ignore all the local changes
    * before the recovery @{
    */
   mContentResolver.delete(
       MessageMove.CONTENT_URI, MessageMove.SRC_FOLDER_KEY + "=" + mMailbox.mId, null);
   wipeMessageStateChanges();
   /** @} */
   //        Mailbox.resyncMailbox(mContentResolver, new
   // android.accounts.Account(mAccount.mEmailAddress,
   //                Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mMailbox.mId);
 }
 private static void syncAccount(final Context context, final Account account) {
   final EmailServiceUtils.EmailServiceInfo info =
       EmailServiceUtils.getServiceInfo(context, account.getProtocol(context));
   final android.accounts.Account amAccount =
       new android.accounts.Account(account.mEmailAddress, info.accountType);
   final Bundle extras = new Bundle(3);
   extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
   extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
   extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
   ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
   LogUtils.i(
       TAG,
       "requestSync SecurityPolicy syncAccount %s, %s",
       account.toString(),
       extras.toString());
 }
  public void commitImpl(int maxOpsPerBatch) throws RemoteException, OperationApplicationException {
    // Use a batch operation to handle the changes
    ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();

    // Maximum size of message text per fetch
    int numFetched = fetchedEmails.size();
    LogUtils.d(
        TAG,
        "commitImpl: maxOpsPerBatch=%d numFetched=%d numNew=%d " + "numDeleted=%d numChanged=%d",
        maxOpsPerBatch,
        numFetched,
        newEmails.size(),
        deletedEmails.size(),
        changedEmails.size());
    for (EmailContent.Message msg : fetchedEmails) {
      // Find the original message's id (by serverId and mailbox)
      Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
      String id = null;
      try {
        if (c.moveToFirst()) {
          id = c.getString(EmailContent.ID_PROJECTION_COLUMN);
          while (c.moveToNext()) {
            // This shouldn't happen, but clean up if it does
            Long dupId = Long.parseLong(c.getString(EmailContent.ID_PROJECTION_COLUMN));
            userLog("Delete duplicate with id: " + dupId);
            deletedEmails.add(dupId);
          }
        }
      } finally {
        c.close();
      }

      // If we find one, we do two things atomically: 1) set the body text for the
      // message, and 2) mark the message loaded (i.e. completely loaded)
      if (id != null) {
        LogUtils.i(TAG, "Fetched body successfully for %s", id);
        final String[] bindArgument = new String[] {id};
        ops.add(
            ContentProviderOperation.newUpdate(EmailContent.Body.CONTENT_URI)
                .withSelection(EmailContent.Body.SELECTION_BY_MESSAGE_KEY, bindArgument)
                .withValue(EmailContent.BodyColumns.TEXT_CONTENT, msg.mText)
                .build());
        ops.add(
            ContentProviderOperation.newUpdate(EmailContent.Message.CONTENT_URI)
                .withSelection(MessageColumns._ID + "=?", bindArgument)
                .withValue(MessageColumns.FLAG_LOADED, EmailContent.Message.FLAG_LOADED_COMPLETE)
                .build());
      }
      applyBatchIfNeeded(ops, maxOpsPerBatch, false);
    }

    /** M: "Bad Sync Key" recovery process @{ */
    if (mMailbox.mId == Exchange.sBadSyncKeyMailboxId && newEmails.size() > 0) {
      LogUtils.i(Eas.BSK_TAG, "newEmails count:" + newEmails.size());
      // Delete the local mails older than the oldest mail timestamp in the 1st window
      ExchangePreferences pref = ExchangePreferences.getPreferences(mContext);
      boolean isStaleMailsRemoved = pref.getRemovedStaleMails();
      if (!isStaleMailsRemoved) {
        // Get the oldest mail's time stamp
        long oldestTimestamp = 0;
        oldestTimestamp = newEmails.get(0).mTimeStamp;
        for (Message msg : newEmails) {
          if (msg.mTimeStamp < oldestTimestamp) {
            oldestTimestamp = msg.mTimeStamp;
          }
        }
        LogUtils.i(Eas.BSK_TAG, "Oldest timestamp: " + oldestTimestamp);

        // Delete all the local mails older than the time stamp
        int rowDeleted =
            mContentResolver.delete(
                Message.CONTENT_URI,
                WHERE_MESSAGE_TIMESTAMP_LESS_THAN,
                new String[] {String.valueOf(oldestTimestamp), String.valueOf(mMailbox.mId)});
        LogUtils.i(Eas.BSK_TAG, rowDeleted + " local stale mails were deleted");

        // Set all mails of this mailbox as "dirty" at first. Then matching local mail
        // with the new mail by their timestamp. If found, clear the "dirty" flag. In
        // this way, we can ultimately mark the stale local mails and remove them
        ContentValues cv = new ContentValues();
        cv.put(MessageColumns.DIRTY, "1");
        mContentResolver.update(
            Message.CONTENT_URI,
            cv,
            WHERE_MAILBOX_KEY,
            new String[] {String.valueOf(mMailbox.mId)});
        pref.setRemovedStaleMails(true);
      }

      // Remove the stale flag of the local mails which can be found in the new-synchronized mails
      // (by compare their time-stamp). Note that server id can not regarded as the identity of
      // a mail because it might be changed by server in the process of the full re-sync
      LogUtils.i(Eas.BSK_TAG, "Finding all the local mails with the same timestamp");
      StringBuilder timestampList = new StringBuilder("(");
      for (Message msg : newEmails) {
        timestampList.append(msg.mTimeStamp);
        timestampList.append(',');
      }
      // Delete the last comma
      if (!newEmails.isEmpty()) {
        timestampList.deleteCharAt(timestampList.length() - 1);
      }
      timestampList.append(")");

      String selection =
          MessageColumns.MAILBOX_KEY
              + "="
              + Long.toString(mMailbox.mId)
              + " AND "
              + MessageColumns.TIMESTAMP
              + " IN "
              + timestampList.toString();
      LogUtils.i(Eas.BSK_TAG, "selection clause: " + selection);
      Cursor c =
          mContentResolver.query(
              Message.CONTENT_URI,
              new String[] {MessageColumns._ID, MessageColumns.TIMESTAMP},
              selection,
              null,
              null);
      try {
        if (c != null) {
          final int columnMessageId = 0;
          final int columnTimestamp = 1;
          ArrayList<ContentProviderOperation> bskOps = new ArrayList<ContentProviderOperation>();
          LogUtils.i(
              Eas.BSK_TAG,
              "For current window, found " + c.getCount() + " mails existed in local DB");

          while (c.moveToNext()) {
            long timestamp = c.getLong(columnTimestamp);
            for (Message msg : newEmails) {
              if (msg.mTimeStamp == timestamp) {
                // Update the properties of local mails
                ContentValues cv = new ContentValues();
                cv.put(MessageColumns.SERVER_ID, msg.mServerId);
                cv.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite);
                cv.put(MessageColumns.FLAGS, msg.mFlags);
                cv.put(MessageColumns.FLAG_READ, msg.mFlagRead);
                cv.put(MessageColumns.DIRTY, "0");
                bskOps.add(
                    ContentProviderOperation.newUpdate(
                            ContentUris.withAppendedId(
                                Message.CONTENT_URI, c.getInt(columnMessageId)))
                        .withValues(cv)
                        .build());

                // Remove the existed mail from new mail list, then it would not be store again
                newEmails.remove(msg);
                applyBatchIfNeeded(bskOps, maxOpsPerBatch, false);
                break;
              }
            }
          }
          applyBatchIfNeeded(bskOps, maxOpsPerBatch, true);
        }
      } catch (RemoteException e) {
        // There is nothing to be done here; fail by returning null
        LogUtils.i(Eas.BSK_TAG, "RemoteException when applyBatch");
      } catch (OperationApplicationException e) {
        // There is nothing to be done here; fail by returning null
        LogUtils.i(Eas.BSK_TAG, "OperationApplicationException when applyBatch");
      } finally {
        if (c != null) {
          c.close();
        }
      }

      LogUtils.i(
          Eas.BSK_TAG,
          "There are " + newEmails.size() + " mail(s) remaining in the newEmails list");
    }
    /** @} */

    /// M: For smart push, record new mails' coming
    DataCollectUtils.recordNewMails(mContext, newEmails);
    for (EmailContent.Message msg : newEmails) {
      msg.addSaveOps(ops);
      applyBatchIfNeeded(ops, maxOpsPerBatch, false);
    }
    /// M: Log receive new message. @{
    EmailContent.Message.logMessageReceived(mContext, newEmails.toArray(new Message[] {}));
    /// @}

    for (Long id : deletedEmails) {
      /**
       * M: 1. The attachments were belonged to the com.android.email application, delete
       * attachments here, it would fail, so mask the function call to
       * AttachmentUtilities.deleteAllAttachmentFiles here and do the attachment delete operation on
       * Email provider. 2. Add a new parameter (account id) to URI for delete attachments on Email
       * side. @{
       */
      Uri.Builder builder = EmailContent.Message.CONTENT_URI.buildUpon();
      builder.appendEncodedPath(String.valueOf(id));
      builder.appendQueryParameter(
          AttachmentUtilities.KEY_ACCOUNT_ID, String.valueOf(mAccount.mId));
      ops.add(ContentProviderOperation.newDelete(builder.build()).build());
      //            AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
      /** @} */
      applyBatchIfNeeded(ops, maxOpsPerBatch, false);
    }

    if (!changedEmails.isEmpty()) {
      // Server wins in a conflict...
      for (ServerChange change : changedEmails) {
        ContentValues cv = new ContentValues();
        if (change.read != null) {
          cv.put(EmailContent.MessageColumns.FLAG_READ, change.read);
        }
        if (change.flag != null) {
          cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, change.flag);
        }
        if (change.flags != null) {
          cv.put(EmailContent.MessageColumns.FLAGS, change.flags);
        }
        ops.add(
            ContentProviderOperation.newUpdate(
                    ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, change.id))
                .withValues(cv)
                .build());
      }
      applyBatchIfNeeded(ops, maxOpsPerBatch, false);
    }

    // We only want to update the sync key here
    ContentValues mailboxValues = new ContentValues();
    mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
    ops.add(
        ContentProviderOperation.newUpdate(
                ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
            .withValues(mailboxValues)
            .build());

    applyBatchIfNeeded(ops, maxOpsPerBatch, true);
    userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
  }
예제 #8
0
  /// M: The folder sync must be synchronized, otherwise the folders may be duplicate
  @Override
  public synchronized void updateFolderList(long accountId) throws RemoteException {
    /// M: We Can't updateFolderList in low storage state @{
    if (StorageLowState.checkIfStorageLow(mContext)) {
      LogUtils.e(Logging.LOG_TAG, "Can't updateFolderList due to low storage");
      return;
    }
    /// @}
    final Account account = Account.restoreAccountWithId(mContext, accountId);
    if (account == null) {
      return;
    }
    long inboxId = -1;
    TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
    Cursor localFolderCursor = null;
    try {
      // Step 0: Make sure the default system mailboxes exist.
      for (final int type : Mailbox.REQUIRED_FOLDER_TYPES) {
        if (Mailbox.findMailboxOfType(mContext, accountId, type) == Mailbox.NO_MAILBOX) {
          final Mailbox mailbox = Mailbox.newSystemMailbox(mContext, accountId, type);
          mailbox.save(mContext);
          if (type == Mailbox.TYPE_INBOX) {
            inboxId = mailbox.mId;
          }
        }
      }

      // Step 1: Get remote mailboxes
      final Store store = Store.getInstance(account, mContext);
      final Folder[] remoteFolders = store.updateFolders();
      final HashSet<String> remoteFolderNames = new HashSet<String>();
      for (final Folder remoteFolder : remoteFolders) {
        remoteFolderNames.add(remoteFolder.getName());
      }

      // Step 2: Get local mailboxes
      localFolderCursor =
          mContext
              .getContentResolver()
              .query(
                  Mailbox.CONTENT_URI,
                  MAILBOX_PROJECTION,
                  EmailContent.MailboxColumns.ACCOUNT_KEY + "=?",
                  new String[] {String.valueOf(account.mId)},
                  null);

      // Step 3: Remove any local mailbox not on the remote list
      while (localFolderCursor.moveToNext()) {
        final String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID);
        // Short circuit if we have a remote mailbox with the same name
        if (remoteFolderNames.contains(mailboxPath)) {
          continue;
        }

        final int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE);
        final long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID);
        switch (mailboxType) {
          case Mailbox.TYPE_INBOX:
          case Mailbox.TYPE_DRAFTS:
          case Mailbox.TYPE_OUTBOX:
          case Mailbox.TYPE_SENT:
          case Mailbox.TYPE_TRASH:
          case Mailbox.TYPE_SEARCH:
            // Never, ever delete special mailboxes
            break;
          default:
            // Drop all attachment files related to this mailbox
            AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext, accountId, mailboxId);
            // Delete the mailbox; database triggers take care of related
            // Message, Body and Attachment records
            Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
            mContext.getContentResolver().delete(uri, null, null);
            break;
        }
      }
    } catch (MessagingException me) {
      LogUtils.i(Logging.LOG_TAG, me, "Error in updateFolderList");
      // We'll hope this is temporary
    } finally {
      if (localFolderCursor != null) {
        localFolderCursor.close();
      }
      // If we just created the inbox, sync it
      if (inboxId != -1) {
        startSync(inboxId, true, 0);
      }
    }
  }
예제 #9
0
  @Override
  public void loadAttachment(
      final IEmailServiceCallback cb, final long attachmentId, final boolean background)
      throws RemoteException {
    /// M: We Can't load attachment in low storage state @{
    if (StorageLowState.checkIfStorageLow(mContext)) {
      LogUtils.e(Logging.LOG_TAG, "Can't load attachment due to low storage");
      cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.SUCCESS, 0);
      return;
    }
    /// @}
    Folder remoteFolder = null;
    try {
      // 1. Check if the attachment is already here and return early in that case
      Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
      if (attachment == null) {
        cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0);
        return;
      }
      final long messageId = attachment.mMessageKey;

      final EmailContent.Message message =
          EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey);
      if (message == null) {
        cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
        return;
      }

      // If the message is loaded, just report that we're finished
      if (Utility.attachmentExists(mContext, attachment)
          && attachment.mUiState == UIProvider.AttachmentState.SAVED) {
        cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0);
        return;
      }

      // Say we're starting...
      cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.IN_PROGRESS, 0);

      // 2. Open the remote folder.
      final Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
      Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);

      if (mailbox.mType == Mailbox.TYPE_OUTBOX
          /// M: View an attachment which comes from refMessage need sourceKey to identify
          || mailbox.mType == Mailbox.TYPE_DRAFTS) {
        long sourceId =
            Utility.getFirstRowLong(
                mContext,
                Body.CONTENT_URI,
                new String[] {BodyColumns.SOURCE_MESSAGE_KEY},
                BodyColumns.MESSAGE_KEY + "=?",
                new String[] {Long.toString(messageId)},
                null,
                0,
                -1L);
        if (sourceId != -1) {
          EmailContent.Message sourceMsg =
              EmailContent.Message.restoreMessageWithId(mContext, sourceId);
          if (sourceMsg != null) {
            mailbox = Mailbox.restoreMailboxWithId(mContext, sourceMsg.mMailboxKey);
            message.mServerId = sourceMsg.mServerId;
          }
        }
      } else if (mailbox.mType == Mailbox.TYPE_SEARCH && message.mMainMailboxKey != 0) {
        mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMainMailboxKey);
      }

      if (account == null || mailbox == null) {
        // If the account/mailbox are gone, just report success; the UI handles this
        cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0);
        return;
      }
      TrafficStats.setThreadStatsTag(TrafficFlags.getAttachmentFlags(mContext, account));

      final Store remoteStore = Store.getInstance(account, mContext);
      remoteFolder = remoteStore.getFolder(mailbox.mServerId);
      remoteFolder.open(OpenMode.READ_WRITE);

      // 3. Generate a shell message in which to retrieve the attachment,
      // and a shell BodyPart for the attachment.  Then glue them together.
      final Message storeMessage = remoteFolder.createMessage(message.mServerId);
      final MimeBodyPart storePart = new MimeBodyPart();
      storePart.setSize((int) attachment.mSize);
      storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, attachment.mLocation);
      storePart.setHeader(
          MimeHeader.HEADER_CONTENT_TYPE,
          String.format("%s;\n name=\"%s\"", attachment.mMimeType, attachment.mFileName));

      // TODO is this always true for attachments?  I think we dropped the
      // true encoding along the way
      /// M: set encoding type according to data base record.
      String encoding = attachment.mEncoding;
      if (TextUtils.isEmpty(encoding)) {
        encoding = "base64";
      }
      storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding);

      final MimeMultipart multipart = new MimeMultipart();
      multipart.setSubType("mixed");
      multipart.addBodyPart(storePart);

      storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
      storeMessage.setBody(multipart);

      // 4. Now ask for the attachment to be fetched
      final FetchProfile fp = new FetchProfile();
      fp.add(storePart);
      remoteFolder.fetch(
          new Message[] {storeMessage},
          fp,
          new MessageRetrievalListenerBridge(messageId, attachmentId, cb));

      // If we failed to load the attachment, throw an Exception here, so that
      // AttachmentDownloadService knows that we failed
      if (storePart.getBody() == null) {
        throw new MessagingException("Attachment not loaded.");
      }

      // Save the attachment to wherever it's going
      AttachmentUtilities.saveAttachment(
          mContext, storePart.getBody().getInputStream(), attachment);

      // 6. Report success
      cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0);

    } catch (MessagingException me) {
      LogUtils.i(Logging.LOG_TAG, me, "Error loading attachment");

      final ContentValues cv = new ContentValues(1);
      cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED);
      final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
      mContext.getContentResolver().update(uri, cv, null, null);

      cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.CONNECTION_ERROR, 0);
    } finally {
      if (remoteFolder != null) {
        remoteFolder.close(false);
      }
    }
  }