/**
  * Returns the mime type for a given attachment. There are three possible results: - If thumbnail
  * Uri, always returns "image/png" (even if there's no attachment) - If the attachment does not
  * exist, returns null - Returns the mime type of the attachment
  */
 @Override
 public String getType(Uri uri) {
   long callingId = Binder.clearCallingIdentity();
   try {
     List<String> segments = uri.getPathSegments();
     String id = segments.get(1);
     int match = sURIMatcher.match(uri);
     String format = (match == ATTACHMENTS_CACHED_FILE_ACCESS) ? null : segments.get(2);
     if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) {
       return "image/png";
     } else {
       uri =
           (match == ATTACHMENTS_CACHED_FILE_ACCESS)
               ? rebuildUri(uri)
               : ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
       Cursor c =
           getContext().getContentResolver().query(uri, MIME_TYPE_PROJECTION, null, null, null);
       try {
         if (c.moveToFirst()) {
           String mimeType = c.getString(MIME_TYPE_COLUMN_MIME_TYPE);
           String fileName = c.getString(MIME_TYPE_COLUMN_FILENAME);
           mimeType = AttachmentUtilities.inferMimeType(fileName, mimeType);
           return mimeType;
         }
       } finally {
         c.close();
       }
       return null;
     }
   } finally {
     Binder.restoreCallingIdentity(callingId);
   }
 }
  /** Save the body part of a single attachment, to a file in the attachments directory. */
  public static void saveAttachmentBody(
      final Context context, final Part part, final Attachment localAttachment, long accountId)
      throws MessagingException, IOException {
    if (part.getBody() != null) {
      final long attachmentId = localAttachment.mId;

      final File saveIn = AttachmentUtilities.getAttachmentDirectory(context, accountId);

      if (!saveIn.isDirectory() && !saveIn.mkdirs()) {
        throw new IOException("Could not create attachment directory");
      }
      final File saveAs =
          AttachmentUtilities.getAttachmentFilename(context, accountId, attachmentId);

      InputStream in = null;
      FileOutputStream out = null;
      final long copySize;
      try {
        in = part.getBody().getInputStream();
        out = new FileOutputStream(saveAs);
        copySize = IOUtils.copyLarge(in, out);
      } finally {
        if (in != null) {
          in.close();
        }
        if (out != null) {
          out.close();
        }
      }

      // update the attachment with the extra information we now know
      final String contentUriString =
          AttachmentUtilities.getAttachmentUri(accountId, attachmentId).toString();

      localAttachment.mSize = copySize;
      localAttachment.setContentUri(contentUriString);

      // update the attachment in the database as well
      final ContentValues cv = new ContentValues(3);
      cv.put(AttachmentColumns.SIZE, copySize);
      cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
      cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
      final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
      context.getContentResolver().update(uri, cv, null, null);
    }
  }
  /**
   * Convert a MIME Part object into an Attachment object. Separated for unit testing.
   *
   * @param part MIME part object to convert
   * @return Populated Account object
   * @throws MessagingException
   */
  @VisibleForTesting
  protected static Attachment mimePartToAttachment(final Part part) throws MessagingException {
    // Transfer fields from mime format to provider format
    final String contentType = MimeUtility.unfoldAndDecode(part.getContentType());

    String name = MimeUtility.getHeaderParameter(contentType, "name");
    if (TextUtils.isEmpty(name)) {
      final String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
      name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
    }

    // Incoming attachment: Try to pull size from disposition (if not downloaded yet)
    long size = 0;
    final String disposition = part.getDisposition();
    if (!TextUtils.isEmpty(disposition)) {
      String s = MimeUtility.getHeaderParameter(disposition, "size");
      if (!TextUtils.isEmpty(s)) {
        try {
          size = Long.parseLong(s);
        } catch (final NumberFormatException e) {
          LogUtils.d(LogUtils.TAG, e, "Could not decode size \"%s\" from attachment part", size);
        }
      }
    }

    // Get partId for unloaded IMAP attachments (if any)
    // This is only provided (and used) when we have structure but not the actual attachment
    final String[] partIds = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
    final String partId = partIds != null ? partIds[0] : null;

    final Attachment localAttachment = new Attachment();

    // Run the mime type through inferMimeType in case we have something generic and can do
    // better using the filename extension
    localAttachment.mMimeType = AttachmentUtilities.inferMimeType(name, part.getMimeType());
    localAttachment.mFileName = name;
    localAttachment.mSize = size;
    localAttachment.mContentId = part.getContentId();
    localAttachment.setContentUri(null); // Will be rewritten by saveAttachmentBody
    localAttachment.mLocation = partId;
    localAttachment.mEncoding = "B"; // TODO - convert other known encodings

    return localAttachment;
  }
  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();
    }
  }
  /// 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);
      }
    }
  }
  @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);
      }
    }
  }
 /**
  * Open an attachment file. There are two "formats" - "raw", which returns an actual file, and
  * "thumbnail", which attempts to generate a thumbnail image.
  *
  * <p>Thumbnails are cached for easy space recovery and cleanup.
  *
  * <p>TODO: The thumbnail format returns null for its failure cases, instead of throwing
  * FileNotFoundException, and should be fixed for consistency.
  *
  * @throws FileNotFoundException
  */
 @Override
 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
   int match = sURIMatcher.match(uri);
   if (match == ATTACHMENTS_CACHED_FILE_ACCESS) {
     long callingId = Binder.clearCallingIdentity();
     try {
       return getContext().getContentResolver().openFileDescriptor(rebuildUri(uri), "r");
     } finally {
       Binder.restoreCallingIdentity(callingId);
     }
   }
   // If this is a write, the caller must have the EmailProvider permission, which is
   // based on signature only
   if (mode.equals("w")) {
     Context context = getContext();
     if (context.checkCallingOrSelfPermission(EmailContent.PROVIDER_PERMISSION)
         != PackageManager.PERMISSION_GRANTED) {
       throw new FileNotFoundException();
     }
     List<String> segments = uri.getPathSegments();
     String accountId = segments.get(0);
     String id = segments.get(1);
     File saveIn = AttachmentUtilities.getAttachmentDirectory(context, Long.parseLong(accountId));
     if (!saveIn.exists()) {
       saveIn.mkdirs();
     }
     File newFile = new File(saveIn, id);
     return ParcelFileDescriptor.open(
         newFile,
         ParcelFileDescriptor.MODE_READ_WRITE
             | ParcelFileDescriptor.MODE_CREATE
             | ParcelFileDescriptor.MODE_TRUNCATE);
   }
   long callingId = Binder.clearCallingIdentity();
   try {
     List<String> segments = uri.getPathSegments();
     String accountId = segments.get(0);
     String id = segments.get(1);
     String format = segments.get(2);
     if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) {
       int width = Integer.parseInt(segments.get(3));
       int height = Integer.parseInt(segments.get(4));
       String filename = "thmb_" + accountId + "_" + id;
       File dir = getContext().getCacheDir();
       File file = new File(dir, filename);
       if (!file.exists()) {
         Uri attachmentUri =
             AttachmentUtilities.getAttachmentUri(Long.parseLong(accountId), Long.parseLong(id));
         Cursor c = query(attachmentUri, new String[] {Columns.DATA}, null, null, null);
         if (c != null) {
           try {
             if (c.moveToFirst()) {
               attachmentUri = Uri.parse(c.getString(0));
             } else {
               return null;
             }
           } finally {
             c.close();
           }
         }
         String type = getContext().getContentResolver().getType(attachmentUri);
         try {
           InputStream in = getContext().getContentResolver().openInputStream(attachmentUri);
           Bitmap thumbnail = createThumbnail(type, in);
           if (thumbnail == null) {
             return null;
           }
           thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
           FileOutputStream out = new FileOutputStream(file);
           thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
           out.close();
           in.close();
         } catch (IOException ioe) {
           LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " + ioe.getMessage());
           return null;
         } catch (OutOfMemoryError oome) {
           LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " + oome.getMessage());
           return null;
         }
       }
       return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
     } else {
       return ParcelFileDescriptor.open(
           new File(getContext().getDatabasePath(accountId + ".db_att"), id),
           ParcelFileDescriptor.MODE_READ_ONLY);
     }
   } finally {
     Binder.restoreCallingIdentity(callingId);
   }
 }