@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    if (!isUserVisible()) {
      // Unclear how this is happening. Current theory is that this fragment was scheduled
      // to be removed, but the remove transaction failed. When the Activity is later
      // restored, the FragmentManager restores this fragment, but Fragment.mMenuVisible is
      // stuck at its initial value (true), which makes this zombie fragment eligible for
      // menu item clicks.
      //
      // Work around this by relying on the (properly restored) extra user visible hint.
      LogUtils.e(
          LOG_TAG, "ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this);
      if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
        LogUtils.e(LOG_TAG, Utils.dumpFragment(this)); // the dump has '%' chars in it...
      }
      return false;
    }

    boolean handled = false;
    final int itemId = item.getItemId();
    if (itemId == R.id.inside_conversation_unread) {
      markUnread();
      handled = true;
    } else if (itemId == R.id.show_original) {
      showUntransformedConversation();
      handled = true;
    }
    return handled;
  }
コード例 #2
0
  @Override
  public void loadMore(long messageId) throws RemoteException {
    /// M: We Can't load more in low storage state @{
    if (StorageLowState.checkIfStorageLow(mContext)) {
      LogUtils.e(Logging.LOG_TAG, "Can't load more due to low storage");
      return;
    }
    /// @}
    // Load a message for view...
    try {
      // 1. Resample the message, in case it disappeared or synced while
      // this command was in queue
      final EmailContent.Message message =
          EmailContent.Message.restoreMessageWithId(mContext, messageId);
      if (message == null) {
        return;
      }
      if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) {
        // We should NEVER get here
        return;
      }

      // 2. Open the remote folder.
      // TODO combine with common code in loadAttachment
      final Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
      final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
      if (account == null || mailbox == null) {
        // mListeners.loadMessageForViewFailed(messageId, "null account or mailbox");
        return;
      }
      TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));

      final Store remoteStore = Store.getInstance(account, mContext);
      final String remoteServerId;
      // If this is a search result, use the protocolSearchInfo field to get the
      // correct remote location
      if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
        remoteServerId = message.mProtocolSearchInfo;
      } else {
        remoteServerId = mailbox.mServerId;
      }
      final Folder remoteFolder = remoteStore.getFolder(remoteServerId);
      remoteFolder.open(OpenMode.READ_WRITE);

      // 3. Set up to download the entire message
      final Message remoteMessage = remoteFolder.getMessage(message.mServerId);
      final FetchProfile fp = new FetchProfile();
      fp.add(FetchProfile.Item.BODY);
      remoteFolder.fetch(new Message[] {remoteMessage}, fp, null);

      // 4. Write to provider
      Utilities.copyOneMessageToProvider(
          mContext, remoteMessage, account, mailbox, EmailContent.Message.FLAG_LOADED_COMPLETE);
    } catch (MessagingException me) {
      if (Logging.LOGD) LogUtils.v(Logging.LOG_TAG, "", me);

    } catch (RuntimeException rte) {
      LogUtils.d(Logging.LOG_TAG, "RTE During loadMore");
    }
  }
コード例 #3
0
  /** Restores preferences from a backup. */
  public void restorePreferences(final List<BackupSharedPreference> preferences) {
    for (final BackupSharedPreference preference : preferences) {
      final String key = preference.getKey();
      final Object value = preference.getValue();

      if (!canBackup(key) || value == null) {
        continue;
      }

      final Object restoreValue = getRestoreValue(key, value);

      if (restoreValue instanceof Boolean) {
        getEditor().putBoolean(key, (Boolean) restoreValue);
        LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
      } else if (restoreValue instanceof Float) {
        getEditor().putFloat(key, (Float) restoreValue);
        LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
      } else if (restoreValue instanceof Integer) {
        getEditor().putInt(key, (Integer) restoreValue);
        LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
      } else if (restoreValue instanceof Long) {
        getEditor().putLong(key, (Long) restoreValue);
        LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
      } else if (restoreValue instanceof String) {
        getEditor().putString(key, (String) restoreValue);
        LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
      } else if (restoreValue instanceof Set) {
        getEditor().putStringSet(key, (Set<String>) restoreValue);
      } else {
        LogUtils.e(LOG_TAG, "Unknown MailPrefs preference data type: %s", value.getClass());
      }
    }

    getEditor().apply();
  }
 // BEGIN conversation header callbacks
 @Override
 public void onFoldersClicked() {
   if (mChangeFoldersMenuItem == null) {
     LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
     return;
   }
   mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
 }
コード例 #5
0
 public void setEnabled(boolean enabled) {
   mEnabled = enabled;
   if (!mEnabled) {
     int count = mCount.getAndSet(0);
     if (count > 0) {
       LogUtils.e(LOG_TAG, "Disable UiHandler. Dropping %d Runnables.", count);
     }
     mHandler.removeCallbacksAndMessages(null);
   }
 }
コード例 #6
0
 public void viewAttachment() {
   if (this.mAttachment.contentUri == null) {
     LogUtils.e(LOG_TAG, "viewAttachment with null content uri", new Object[0]);
     return;
   }
   Intent localIntent = new Intent("android.intent.action.VIEW");
   localIntent.setFlags(524289);
   Utils.setIntentDataAndTypeAndNormalize(
       localIntent, this.mAttachment.contentUri, this.mAttachment.contentType);
   try {
     getContext().startActivity(localIntent);
     return;
   } catch (ActivityNotFoundException localActivityNotFoundException) {
     LogUtils.e(
         LOG_TAG,
         localActivityNotFoundException,
         "Couldn't find Activity for intent",
         new Object[0]);
   }
 }
コード例 #7
0
 /**
  * Converts the prefs value into an index useful for configuring the UI widget, falling back to
  * the default value if the value from the prefs can't be found for some reason. If neither can be
  * found, it throws an {@link java.lang.IllegalArgumentException}
  *
  * @param conversionArray An array of prefs values, in widget order
  * @param prefValue Value of the preference
  * @param defaultValue Default value, as a fallback if we can't map the real value
  * @return Index of the entry (or fallback) in the conversion array
  */
 @VisibleForTesting
 static int prefValueToWidgetIndex(int[] conversionArray, int prefValue, int defaultValue) {
   for (int i = 0; i < conversionArray.length; i++) {
     if (conversionArray[i] == prefValue) {
       return i;
     }
   }
   LogUtils.e(LogUtils.TAG, "Can't map preference value " + prefValue);
   for (int i = 0; i < conversionArray.length; i++) {
     if (conversionArray[i] == defaultValue) {
       return i;
     }
   }
   throw new IllegalArgumentException("Can't map default preference value " + prefValue);
 }
コード例 #8
0
  public static Intent generateProxyIntent(
      Context context, Uri proxyUri, Uri uri, String accountName) {
    final Intent intent = new Intent(Intent.ACTION_VIEW, proxyUri);
    intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ORIGINAL_URI, uri);
    intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ACCOUNT_NAME, accountName);

    PackageManager manager = null;
    // We need to catch the exception to make CanvasConversationHeaderView
    // test pass.  Bug: http://b/issue?id=3470653.
    try {
      manager = context.getPackageManager();
    } catch (UnsupportedOperationException e) {
      LogUtils.e(LOG_TAG, e, "Error getting package manager");
    }

    if (manager != null) {
      // Try and resolve the intent, to find an activity from this package
      final List<ResolveInfo> resolvedActivities =
          manager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);

      final String packageName = context.getPackageName();

      // Now try and find one that came from this package, if one is not found, the UI
      // provider must have specified an intent that is to be handled by a different apk.
      // In that case, the class name will not be set on the intent, so the default
      // intent resolution will be used.
      for (ResolveInfo resolveInfo : resolvedActivities) {
        final ActivityInfo activityInfo = resolveInfo.activityInfo;
        if (packageName.equals(activityInfo.packageName)) {
          intent.setClassName(activityInfo.packageName, activityInfo.name);
          break;
        }
      }
    }

    return intent;
  }
コード例 #9
0
  public static void sendMailImpl(Context context, long accountId) {
    /// M: We Can't send mail in low storage state @{
    if (StorageLowState.checkIfStorageLow(context)) {
      LogUtils.e(Logging.LOG_TAG, "Can't send mail due to low storage");
      return;
    }
    /// @}

    /** M: Get the sendable mails count of the account and notify this sending @{ */
    final int count = getSendableMessageCount(context, accountId);
    LogUtils.logFeature(LogTag.SENDMAIL_TAG, "sendable message count [%d]", count);
    if (count <= 0) {
      return;
    }
    SendNotificationProxy.getInstance(context)
        .showSendingNotification(accountId, NotificationController.SEND_MAIL, count);
    /** @} */
    final Account account = Account.restoreAccountWithId(context, accountId);
    TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(context, account));
    final NotificationController nc = NotificationController.getInstance(context);
    // 1.  Loop through all messages in the account's outbox
    final long outboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX);
    if (outboxId == Mailbox.NO_MAILBOX) {
      return;
    }
    final ContentResolver resolver = context.getContentResolver();
    final Cursor c =
        resolver.query(
            EmailContent.Message.CONTENT_URI,
            EmailContent.Message.ID_COLUMN_PROJECTION,
            EmailContent.Message.MAILBOX_KEY + "=?",
            new String[] {Long.toString(outboxId)},
            null);
    try {
      // 2.  exit early
      if (c.getCount() <= 0) {
        return;
      }
      final Sender sender = Sender.getInstance(context, account);
      final Store remoteStore = Store.getInstance(account, context);
      final ContentValues moveToSentValues;
      if (remoteStore.requireCopyMessageToSentFolder()) {
        Mailbox sentFolder = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SENT);
        moveToSentValues = new ContentValues();
        moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId);
      } else {
        moveToSentValues = null;
      }

      // 3.  loop through the available messages and send them
      /** M: mark should we cancel the Login Failed Notification. */
      boolean shouldCancelNf = false;
      while (c.moveToNext()) {
        long messageId = -1;
        if (moveToSentValues != null) {
          moveToSentValues.remove(EmailContent.MessageColumns.FLAGS);
        }
        try {
          messageId = c.getLong(0);
          // Don't send messages with unloaded attachments
          if (Utility.hasUnloadedAttachments(context, messageId)) {
            LogUtils.logFeature(
                LogTag.SENDMAIL_TAG, "Can't send #" + messageId + "; unloaded attachments");
            continue;
          }
          sender.sendMessage(messageId);
        } catch (MessagingException me) {
          LogUtils.logFeature(
              LogTag.SENDMAIL_TAG,
              "<<< Smtp send message failed id [%s], exception: %s",
              messageId,
              me);
          // report error for this message, but keep trying others
          if (me instanceof AuthenticationFailedException) {
            shouldCancelNf = false;
            nc.showLoginFailedNotification(account.mId);
          }
          /// M: One mail sent failed
          SendNotificationProxy.getInstance(context)
              .showSendingNotification(account.mId, NotificationController.SEND_FAILED, 1);
          continue;
        }

        /// M: One mail sent complete
        SendNotificationProxy.getInstance(context)
            .showSendingNotification(account.mId, NotificationController.SEND_COMPLETE, 1);

        // 4. move to sent, or delete
        final Uri syncedUri =
            ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
        // Delete all cached files
        AttachmentUtilities.deleteAllCachedAttachmentFiles(context, account.mId, messageId);
        if (moveToSentValues != null) {
          // If this is a forwarded message and it has attachments, delete them, as they
          // duplicate information found elsewhere (on the server).  This saves storage.
          final EmailContent.Message msg =
              EmailContent.Message.restoreMessageWithId(context, messageId);
          if ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0) {
            AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, messageId);
          }
          /// M: un-mark sending status after sending
          final int flags =
              msg.mFlags
                  & ~(EmailContent.Message.FLAG_TYPE_REPLY
                      | EmailContent.Message.FLAG_TYPE_FORWARD
                      | EmailContent.Message.FLAG_TYPE_REPLY_ALL
                      | EmailContent.Message.FLAG_TYPE_ORIGINAL
                      | EmailContent.Message.FLAG_STATUS_SENDING);

          moveToSentValues.put(EmailContent.MessageColumns.FLAGS, flags);
          resolver.update(syncedUri, moveToSentValues, null, null);
        } else {
          AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, messageId);
          final Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
          resolver.delete(uri, null, null);
          resolver.delete(syncedUri, null, null);
        }
        shouldCancelNf = true;
      }
      if (shouldCancelNf) {
        nc.cancelLoginFailedNotification(account.mId);
      }
    } catch (MessagingException me) {
      if (me instanceof AuthenticationFailedException) {
        nc.showLoginFailedNotification(account.mId);
      }
      /// M: All mails failed to be sent, caused by fail to get instance of store
      SendNotificationProxy.getInstance(context)
          .showSendingNotification(account.mId, NotificationController.SEND_FAILED, c.getCount());
    } finally {
      c.close();
    }
  }
コード例 #10
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);
      }
    }
  }
コード例 #11
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);
      }
    }
  }
コード例 #12
0
  /**
   * Read a complete Provider message into a legacy message (for IMAP upload). This is basically the
   * equivalent of LocalFolder.getMessages() + LocalFolder.fetch().
   */
  public static Message makeMessage(final Context context, final EmailContent.Message localMessage)
      throws MessagingException {
    final MimeMessage message = new MimeMessage();

    // LocalFolder.getMessages() equivalent:  Copy message fields
    message.setSubject(localMessage.mSubject == null ? "" : localMessage.mSubject);
    final Address[] from = Address.fromHeader(localMessage.mFrom);
    if (from.length > 0) {
      message.setFrom(from[0]);
    }
    message.setSentDate(new Date(localMessage.mTimeStamp));
    message.setUid(localMessage.mServerId);
    message.setFlag(
        Flag.DELETED, localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_DELETED);
    message.setFlag(Flag.SEEN, localMessage.mFlagRead);
    message.setFlag(Flag.FLAGGED, localMessage.mFlagFavorite);
    //      message.setFlag(Flag.DRAFT, localMessage.mMailboxKey == draftMailboxKey);
    message.setRecipients(RecipientType.TO, Address.fromHeader(localMessage.mTo));
    message.setRecipients(RecipientType.CC, Address.fromHeader(localMessage.mCc));
    message.setRecipients(RecipientType.BCC, Address.fromHeader(localMessage.mBcc));
    message.setReplyTo(Address.fromHeader(localMessage.mReplyTo));
    message.setInternalDate(new Date(localMessage.mServerTimeStamp));
    message.setMessageId(localMessage.mMessageId);

    // LocalFolder.fetch() equivalent: build body parts
    message.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
    final MimeMultipart mp = new MimeMultipart();
    mp.setSubType("mixed");
    message.setBody(mp);

    try {
      addTextBodyPart(
          mp,
          "text/html",
          EmailContent.Body.restoreBodyHtmlWithMessageId(context, localMessage.mId));
    } catch (RuntimeException rte) {
      LogUtils.d(Logging.LOG_TAG, "Exception while reading html body " + rte.toString());
    }

    try {
      addTextBodyPart(
          mp,
          "text/plain",
          EmailContent.Body.restoreBodyTextWithMessageId(context, localMessage.mId));
    } catch (RuntimeException rte) {
      LogUtils.d(Logging.LOG_TAG, "Exception while reading text body " + rte.toString());
    }

    // Attachments
    final Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
    final Cursor attachments =
        context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION, null, null, null);

    try {
      while (attachments != null && attachments.moveToNext()) {
        final Attachment att = new Attachment();
        att.restore(attachments);
        try {
          final InputStream content;
          if (att.mContentBytes != null) {
            // This is generally only the case for synthetic attachments, such as those
            // generated by unit tests or calendar invites
            content = new ByteArrayInputStream(att.mContentBytes);
          } else {
            String contentUriString = att.getCachedFileUri();
            if (TextUtils.isEmpty(contentUriString)) {
              contentUriString = att.getContentUri();
            }
            if (TextUtils.isEmpty(contentUriString)) {
              content = null;
            } else {
              final Uri contentUri = Uri.parse(contentUriString);
              content = context.getContentResolver().openInputStream(contentUri);
            }
          }
          final String mimeType = att.mMimeType;
          final Long contentSize = att.mSize;
          final String contentId = att.mContentId;
          final String filename = att.mFileName;
          if (content != null) {
            addAttachmentPart(mp, mimeType, contentSize, filename, contentId, content);
          } else {
            LogUtils.e(LogUtils.TAG, "Could not open attachment file for upsync");
          }
        } catch (final FileNotFoundException e) {
          LogUtils.e(
              LogUtils.TAG,
              "File Not Found error on %s while upsyncing message",
              att.getCachedFileUri());
        }
      }
    } finally {
      if (attachments != null) {
        attachments.close();
      }
    }

    return message;
  }