/**
   * Parse a message from the server stream.
   *
   * @return the parsed Message
   * @throws IOException
   */
  private EmailContent.Message addParser(final int endingTag)
      throws IOException, CommandStatusException {
    EmailContent.Message msg = new EmailContent.Message();
    msg.mAccountKey = mAccount.mId;
    msg.mMailboxKey = mMailbox.mId;
    msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_COMPLETE;
    // Default to 1 (success) in case we don't get this tag
    int status = 1;

    while (nextTag(endingTag) != END) {
      switch (tag) {
        case Tags.SYNC_SERVER_ID:
          msg.mServerId = getValue();
          LogUtils.d(TAG, "ServerId: %s", msg.mServerId);
          break;
        case Tags.SYNC_STATUS:
          status = getValueInt();
          break;
        case Tags.SYNC_APPLICATION_DATA:
          addData(msg, tag);
          break;
        default:
          skipTag();
      }
    }
    // For sync, status 1 = success
    if (status != 1) {
      throw new CommandStatusException(status, msg.mServerId);
    }
    return msg;
  }
  /**
   * Add a single attachment part to the message
   *
   * <p>This will skip adding attachments if they are already found in the attachments table. The
   * heuristic for this will fail (false-positive) if two identical attachments are included in a
   * single POP3 message. TODO: Fix that, by (elsewhere) simulating an mLocation value based on the
   * attachments position within the list of multipart/mixed elements. This would make every POP3
   * attachment unique, and might also simplify the code (since we could just look at the positions,
   * and ignore the filename, etc.)
   *
   * <p>TODO: Take a closer look at encoding and deal with it if necessary.
   *
   * @param context a context for file operations
   * @param localMessage the attachments will be built against this message
   * @param part a single attachment part from POP or IMAP
   */
  public static void addOneAttachment(
      final Context context, final EmailContent.Message localMessage, final Part part)
      throws MessagingException, IOException {
    final Attachment localAttachment = mimePartToAttachment(part);
    localAttachment.mMessageKey = localMessage.mId;
    localAttachment.mAccountKey = localMessage.mAccountKey;

    if (DEBUG_ATTACHMENTS) {
      LogUtils.d(Logging.LOG_TAG, "Add attachment " + localAttachment);
    }

    // To prevent duplication - do we already have a matching attachment?
    // The fields we'll check for equality are:
    //  mFileName, mMimeType, mContentId, mMessageKey, mLocation
    // NOTE:  This will false-positive if you attach the exact same file, twice, to a POP3
    // message.  We can live with that - you'll get one of the copies.
    final Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
    final Cursor cursor =
        context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION, null, null, null);
    boolean attachmentFoundInDb = false;
    try {
      while (cursor.moveToNext()) {
        final Attachment dbAttachment = new Attachment();
        dbAttachment.restore(cursor);
        // We test each of the fields here (instead of in SQL) because they may be
        // null, or may be strings.
        if (!TextUtils.equals(dbAttachment.mFileName, localAttachment.mFileName)
            || !TextUtils.equals(dbAttachment.mMimeType, localAttachment.mMimeType)
            || !TextUtils.equals(dbAttachment.mContentId, localAttachment.mContentId)
            || !TextUtils.equals(dbAttachment.mLocation, localAttachment.mLocation)) {
          continue;
        }
        // We found a match, so use the existing attachment id, and stop looking/looping
        attachmentFoundInDb = true;
        localAttachment.mId = dbAttachment.mId;
        if (DEBUG_ATTACHMENTS) {
          LogUtils.d(Logging.LOG_TAG, "Skipped, found db attachment " + dbAttachment);
        }
        break;
      }
    } finally {
      cursor.close();
    }

    // Save the attachment (so far) in order to obtain an id
    if (!attachmentFoundInDb) {
      localAttachment.save(context);
    }

    // If an attachment body was actually provided, we need to write the file now
    saveAttachmentBody(context, part, localAttachment, localMessage.mAccountKey);

    if (localMessage.mAttachments == null) {
      localMessage.mAttachments = new ArrayList<Attachment>();
    }
    localMessage.mAttachments.add(localAttachment);
    localMessage.mFlagAttachment = true;
  }
  @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");
    }
  }
  private void bodyParser(EmailContent.Message msg) throws IOException {
    String bodyType = Eas.BODY_PREFERENCE_TEXT;
    String body = "";
    while (nextTag(Tags.BASE_BODY) != END) {
      switch (tag) {
        case Tags.BASE_TYPE:
          bodyType = getValue();
          break;
        case Tags.BASE_DATA:
          body = getValue();
          break;
        case Tags.BASE_TRUNCATED:
          // M: Message is partial loaded if AirSyncBaseBody::Truncated is True;
          // Otherwise message is complete loaded if False
          String bodyTruncated = getValue();
          if ("1".equals(bodyTruncated) || "true".equals(bodyTruncated)) {
            msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
          } else {
            msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_COMPLETE;
          }
          LogUtils.d(TAG, "_____________ EAS 12+ body truncated: " + bodyTruncated);
          break;
        default:
          skipTag();
      }
    }
    // We always ask for TEXT or HTML; there's no third option
    if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) {
      msg.mHtml = body;
    } else {
      msg.mText = body;
    }

    /**
     * M: What if a mail with bodyTruncated tag, but both body html and text are empty? In this
     * case, client should set a suitable load flag: FLAG_LOADED_COMPLETE. @{
     */
    if (TextUtils.isEmpty(msg.mHtml) && TextUtils.isEmpty(msg.mText)) {
      LogUtils.d(TAG, " for empty mailbody, reset load complete tag.");
      msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_COMPLETE;
    }
    /** @} */
  }
 /**
  * Copy attachments from MimeMessage to provider Message.
  *
  * @param context a context for file operations
  * @param localMessage the attachments will be built against this message
  * @param attachments the attachments to add
  */
 public static void updateAttachments(
     final Context context,
     final EmailContent.Message localMessage,
     final ArrayList<Part> attachments)
     throws MessagingException, IOException {
   localMessage.mAttachments = null;
   for (Part attachmentPart : attachments) {
     addOneAttachment(context, localMessage, attachmentPart);
   }
 }
 /**
  * Parses untruncated MIME data, saving away the text parts
  *
  * @param msg the message we're building
  * @param mimeData the MIME data we've received from the server
  * @throws IOException
  */
 private static void mimeBodyParser(EmailContent.Message msg, String mimeData) throws IOException {
   try {
     ByteArrayInputStream in = new ByteArrayInputStream(mimeData.getBytes());
     // The constructor parses the message
     MimeMessage mimeMessage = new MimeMessage(in);
     // Now process body parts & attachments
     ArrayList<Part> viewables = new ArrayList<Part>();
     // We'll ignore the attachments, as we'll get them directly from EAS
     ArrayList<Part> attachments = new ArrayList<Part>();
     MimeUtility.collectParts(mimeMessage, viewables, attachments);
     // parseBodyFields fills in the content fields of the Body
     ConversionUtilities.BodyFieldData data = ConversionUtilities.parseBodyFields(viewables);
     // But we need them in the message itself for handling during commit()
     msg.setFlags(data.isQuotedReply, data.isQuotedForward);
     msg.mSnippet = data.snippet;
     msg.mHtml = data.htmlContent;
     msg.mText = data.textContent;
   } catch (MessagingException e) {
     // This would most likely indicate a broken stream
     throw new IOException(e);
   }
 }
 /**
  * Set up the meetingInfo field in the message with various pieces of information gleaned from
  * MeetingRequest tags. This information will be used later to generate an appropriate reply email
  * if the user chooses to respond
  *
  * @param msg the Message being built
  * @throws IOException
  */
 private void meetingRequestParser(EmailContent.Message msg) throws IOException {
   PackedString.Builder packedString = new PackedString.Builder();
   while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) {
     switch (tag) {
       case Tags.EMAIL_DTSTAMP:
         packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue());
         break;
       case Tags.EMAIL_START_TIME:
         packedString.put(MeetingInfo.MEETING_DTSTART, getValue());
         break;
       case Tags.EMAIL_END_TIME:
         packedString.put(MeetingInfo.MEETING_DTEND, getValue());
         break;
       case Tags.EMAIL_ORGANIZER:
         packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue());
         break;
       case Tags.EMAIL_LOCATION:
         packedString.put(MeetingInfo.MEETING_LOCATION, getValue());
         break;
       case Tags.EMAIL_GLOBAL_OBJID:
         packedString.put(
             MeetingInfo.MEETING_UID, CalendarUtilities.getUidFromGlobalObjId(getValue()));
         break;
       case Tags.EMAIL_CATEGORIES:
         skipParser(tag);
         break;
       case Tags.EMAIL_RECURRENCES:
         recurrencesParser();
         break;
       case Tags.EMAIL_RESPONSE_REQUESTED:
         packedString.put(MeetingInfo.MEETING_RESPONSE_REQUESTED, getValue());
         break;
       case Tags.EMAIL_ALL_DAY_EVENT:
         if (getValueInt() == 1) {
           packedString.put(MeetingInfo.MEETING_ALL_DAY, "1");
         }
         break;
       default:
         skipTag();
     }
   }
   if (msg.mSubject != null) {
     packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject);
   }
   msg.mMeetingInfo = packedString.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);
  }
  private void attachmentParser(
      final ArrayList<EmailContent.Attachment> atts,
      final EmailContent.Message msg,
      final int endingTag)
      throws IOException {
    String fileName = null;
    String length = null;
    String location = null;
    boolean isInline = false;
    String contentId = null;

    while (nextTag(endingTag) != END) {
      switch (tag) {
          // We handle both EAS 2.5 and 12.0+ attachments here
        case Tags.EMAIL_DISPLAY_NAME:
        case Tags.BASE_DISPLAY_NAME:
          fileName = getValue();
          break;
        case Tags.EMAIL_ATT_NAME:
        case Tags.BASE_FILE_REFERENCE:
          location = getValue();
          break;
        case Tags.EMAIL_ATT_SIZE:
        case Tags.BASE_ESTIMATED_DATA_SIZE:
          length = getValue();
          break;
        case Tags.BASE_IS_INLINE:
          String isInlineStr = getValue();
          isInline = "true".equalsIgnoreCase(isInlineStr) || "1".equals(isInlineStr);
          break;
        case Tags.BASE_CONTENT_ID:
          contentId = getValue();
          break;
        default:
          skipTag();
      }
    }

    if ((fileName != null) && (length != null) && (location != null)) {
      EmailContent.Attachment att = new EmailContent.Attachment();
      att.mEncoding = "base64";
      att.mSize = Long.parseLong(length);
      att.mFileName = fileName;
      att.mLocation = location;
      att.mMimeType = getMimeTypeFromFileName(fileName);
      att.mAccountKey = mAccount.mId;
      // Save away the contentId, if we've got one (for inline images); note that the
      // EAS docs appear to be wrong about the tags used; inline images come with
      // contentId rather than contentLocation, when sent from Ex03, Ex07, and Ex10
      if (isInline && !TextUtils.isEmpty(contentId)) {
        att.mContentId = contentId;
      }
      // Check if this attachment can't be downloaded due to an account policy
      if (mPolicy != null) {
        if (mPolicy.mDontAllowAttachments
            || (mPolicy.mMaxAttachmentSize > 0 && (att.mSize > mPolicy.mMaxAttachmentSize))) {
          att.mFlags = EmailContent.Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
        }
      }
      atts.add(att);
      msg.mFlagAttachment = true;
    }
  }
  public void addData(EmailContent.Message msg, int endingTag) throws IOException {
    ArrayList<EmailContent.Attachment> atts = new ArrayList<EmailContent.Attachment>();
    boolean truncated = false;

    while (nextTag(endingTag) != END) {
      switch (tag) {
        case Tags.EMAIL_ATTACHMENTS:
        case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up
          attachmentsParser(atts, msg, tag);
          break;
        case Tags.EMAIL_TO:
          msg.mTo = Address.pack(Address.parse(getValue(), false));
          break;
        case Tags.EMAIL_FROM:
          Address[] froms = Address.parse(getValue(), false);
          if (froms != null && froms.length > 0) {
            msg.mDisplayName = froms[0].toFriendly();
          }
          msg.mFrom = Address.toString(froms);
          break;
        case Tags.EMAIL_CC:
          msg.mCc = Address.pack(Address.parse(getValue(), false));
          break;
        case Tags.EMAIL_REPLY_TO:
          msg.mReplyTo = Address.pack(Address.parse(getValue(), false));
          break;
        case Tags.EMAIL_DATE_RECEIVED:
          try {
            msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue());
          } catch (ParseException e) {
            LogUtils.w(TAG, "Parse error for EMAIL_DATE_RECEIVED tag.", e);
          }
          break;
        case Tags.EMAIL_SUBJECT:
          msg.mSubject = getValue();
          break;
        case Tags.EMAIL_READ:
          msg.mFlagRead = getValueInt() == 1;
          break;
        case Tags.BASE_BODY:
          bodyParser(msg);
          break;
        case Tags.EMAIL_FLAG:
          msg.mFlagFavorite = flagParser();
          break;
        case Tags.EMAIL_MIME_TRUNCATED:
          truncated = getValueInt() == 1;
          break;
        case Tags.EMAIL_MIME_DATA:
          // We get MIME data for EAS 2.5.  First we parse it, then we take the
          // html and/or plain text data and store it in the message
          if (truncated) {
            // If the MIME data is truncated, don't bother parsing it, because
            // it will take time and throw an exception anyway when EOF is reached
            // In this case, we will load the body separately by tagging the message
            // "partially loaded".
            // Get the data (and ignore it)
            getValue();
            userLog("Partially loaded: ", msg.mServerId);
            msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
            mFetchNeeded = true;
          } else {
            mimeBodyParser(msg, getValue());
          }
          break;
        case Tags.EMAIL_BODY:
          String text = getValue();
          msg.mText = text;
          break;
        case Tags.EMAIL_MESSAGE_CLASS:
          String messageClass = getValue();
          if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
            msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_INVITE;
          } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
            msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_CANCEL;
          }
          break;
        case Tags.EMAIL_MEETING_REQUEST:
          meetingRequestParser(msg);
          break;
        case Tags.EMAIL_THREAD_TOPIC:
          msg.mThreadTopic = getValue();
          break;
        case Tags.RIGHTS_LICENSE:
          skipParser(tag);
          break;
        case Tags.EMAIL2_CONVERSATION_ID:
          msg.mServerConversationId = Base64.encodeToString(getValueBytes(), Base64.URL_SAFE);
          break;
        case Tags.EMAIL2_CONVERSATION_INDEX:
          // Ignore this byte array since we're not constructing a tree.
          getValueBytes();
          break;
        case Tags.EMAIL2_LAST_VERB_EXECUTED:
          int val = getValueInt();
          if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
            // We aren't required to distinguish between reply and reply all here
            msg.mFlags |= EmailContent.Message.FLAG_REPLIED_TO;
          } else if (val == LAST_VERB_FORWARD) {
            msg.mFlags |= EmailContent.Message.FLAG_FORWARDED;
          }
          break;
        default:
          skipTag();
      }
    }

    if (atts.size() > 0) {
      msg.mAttachments = atts;
    }

    if ((msg.mFlags & EmailContent.Message.FLAG_INCOMING_MEETING_MASK) != 0) {
      String text =
          TextUtilities.makeSnippetFromHtmlText(msg.mText != null ? msg.mText : msg.mHtml);
      if (TextUtils.isEmpty(text)) {
        // Create text for this invitation
        String meetingInfo = msg.mMeetingInfo;
        if (!TextUtils.isEmpty(meetingInfo)) {
          PackedString ps = new PackedString(meetingInfo);
          ContentValues values = new ContentValues();
          putFromMeeting(
              ps, MeetingInfo.MEETING_LOCATION, values, CalendarContract.Events.EVENT_LOCATION);
          String dtstart = ps.get(MeetingInfo.MEETING_DTSTART);
          if (!TextUtils.isEmpty(dtstart)) {
            try {
              final long startTime = Utility.parseEmailDateTimeToMillis(dtstart);
              values.put(CalendarContract.Events.DTSTART, startTime);
            } catch (ParseException e) {
              LogUtils.w(TAG, "Parse error for MEETING_DTSTART tag.", e);
            }
          }
          putFromMeeting(ps, MeetingInfo.MEETING_ALL_DAY, values, CalendarContract.Events.ALL_DAY);
          msg.mText = CalendarUtilities.buildMessageTextFromEntityValues(mContext, values, null);
          msg.mHtml = Html.toHtml(new SpannedString(msg.mText));
        }
      }
    }
  }
  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();
    }
  }
  @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);
      }
    }
  }
  /**
   * Copy field-by-field from a "store" message to a "provider" message
   *
   * @param message The message we've just downloaded (must be a MimeMessage)
   * @param localMessage The message we'd like to write into the DB
   * @return true if dirty (changes were made)
   */
  public static boolean updateMessageFields(
      final EmailContent.Message localMessage,
      final Message message,
      final long accountId,
      final long mailboxId)
      throws MessagingException {

    final Address[] from = message.getFrom();
    final Address[] to = message.getRecipients(Message.RecipientType.TO);
    final Address[] cc = message.getRecipients(Message.RecipientType.CC);
    final Address[] bcc = message.getRecipients(Message.RecipientType.BCC);
    final Address[] replyTo = message.getReplyTo();
    final String subject = message.getSubject();
    final Date sentDate = message.getSentDate();
    final Date internalDate = message.getInternalDate();

    if (from != null && from.length > 0) {
      localMessage.mDisplayName = from[0].toFriendly();
    }
    if (sentDate != null) {
      localMessage.mTimeStamp = sentDate.getTime();
    } else if (internalDate != null) {
      LogUtils.w(Logging.LOG_TAG, "No sentDate, falling back to internalDate");
      localMessage.mTimeStamp = internalDate.getTime();
    }
    if (subject != null) {
      localMessage.mSubject = subject;
    }
    localMessage.mFlagRead = message.isSet(Flag.SEEN);
    if (message.isSet(Flag.ANSWERED)) {
      localMessage.mFlags |= EmailContent.Message.FLAG_REPLIED_TO;
    }

    // Keep the message in the "unloaded" state until it has (at least) a display name.
    // This prevents early flickering of empty messages in POP download.
    if (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE) {
      if (localMessage.mDisplayName == null || "".equals(localMessage.mDisplayName)) {
        localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_UNLOADED;
      } else {
        localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
      }
    }
    localMessage.mFlagFavorite = message.isSet(Flag.FLAGGED);
    //        public boolean mFlagAttachment = false;
    //        public int mFlags = 0;

    localMessage.mServerId = message.getUid();
    if (internalDate != null) {
      localMessage.mServerTimeStamp = internalDate.getTime();
    }
    //        public String mClientId;

    // Only replace the local message-id if a new one was found.  This is seen in some ISP's
    // which may deliver messages w/o a message-id header.
    final String messageId = message.getMessageId();
    if (messageId != null) {
      localMessage.mMessageId = messageId;
    }

    //        public long mBodyKey;
    localMessage.mMailboxKey = mailboxId;
    localMessage.mAccountKey = accountId;

    if (from != null && from.length > 0) {
      localMessage.mFrom = Address.toString(from);
    }

    localMessage.mTo = Address.toString(to);
    localMessage.mCc = Address.toString(cc);
    localMessage.mBcc = Address.toString(bcc);
    localMessage.mReplyTo = Address.toString(replyTo);

    //        public String mText;
    //        public String mHtml;
    //        public String mTextReply;
    //        public String mHtmlReply;

    //        // Can be used while building messages, but is NOT saved by the Provider
    //        transient public ArrayList<Attachment> mAttachments = null;

    return true;
  }