2012-06-28 19:16:59 +00:00
|
|
|
/* Copyright (C) 2012 The Android Open Source Project
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package com.android.email.service;
|
|
|
|
|
|
|
|
import android.content.ContentResolver;
|
|
|
|
import android.content.ContentUris;
|
|
|
|
import android.content.ContentValues;
|
|
|
|
import android.content.Context;
|
|
|
|
import android.database.Cursor;
|
|
|
|
import android.net.TrafficStats;
|
|
|
|
import android.net.Uri;
|
|
|
|
import android.os.Bundle;
|
|
|
|
import android.os.RemoteException;
|
2014-08-05 09:43:02 +00:00
|
|
|
import android.text.TextUtils;
|
2012-06-28 19:16:59 +00:00
|
|
|
|
2014-08-28 18:00:08 +00:00
|
|
|
import com.android.email.DebugUtils;
|
2012-06-28 19:16:59 +00:00
|
|
|
import com.android.email.NotificationController;
|
2014-09-07 20:36:33 +00:00
|
|
|
import com.android.email.NotificationControllerCreatorHolder;
|
2012-06-28 19:16:59 +00:00
|
|
|
import com.android.email.mail.Sender;
|
|
|
|
import com.android.email.mail.Store;
|
2014-08-05 09:43:02 +00:00
|
|
|
import com.android.email.provider.AccountReconciler;
|
|
|
|
import com.android.email.provider.Utilities;
|
2012-09-22 00:49:26 +00:00
|
|
|
import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
|
2012-06-28 19:16:59 +00:00
|
|
|
import com.android.emailcommon.Logging;
|
|
|
|
import com.android.emailcommon.TrafficFlags;
|
|
|
|
import com.android.emailcommon.internet.MimeBodyPart;
|
|
|
|
import com.android.emailcommon.internet.MimeHeader;
|
|
|
|
import com.android.emailcommon.internet.MimeMultipart;
|
|
|
|
import com.android.emailcommon.mail.AuthenticationFailedException;
|
|
|
|
import com.android.emailcommon.mail.FetchProfile;
|
|
|
|
import com.android.emailcommon.mail.Folder;
|
|
|
|
import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
|
|
|
|
import com.android.emailcommon.mail.Folder.OpenMode;
|
|
|
|
import com.android.emailcommon.mail.Message;
|
|
|
|
import com.android.emailcommon.mail.MessagingException;
|
|
|
|
import com.android.emailcommon.provider.Account;
|
|
|
|
import com.android.emailcommon.provider.EmailContent;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.Attachment;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.Body;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.BodyColumns;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.MessageColumns;
|
|
|
|
import com.android.emailcommon.provider.Mailbox;
|
|
|
|
import com.android.emailcommon.service.EmailServiceStatus;
|
2014-07-10 22:08:29 +00:00
|
|
|
import com.android.emailcommon.service.EmailServiceVersion;
|
2014-07-02 20:29:58 +00:00
|
|
|
import com.android.emailcommon.service.HostAuthCompat;
|
2012-06-28 19:16:59 +00:00
|
|
|
import com.android.emailcommon.service.IEmailService;
|
|
|
|
import com.android.emailcommon.service.IEmailServiceCallback;
|
|
|
|
import com.android.emailcommon.service.SearchParams;
|
|
|
|
import com.android.emailcommon.utility.AttachmentUtilities;
|
|
|
|
import com.android.emailcommon.utility.Utility;
|
|
|
|
import com.android.mail.providers.UIProvider;
|
2013-05-26 04:32:32 +00:00
|
|
|
import com.android.mail.utils.LogUtils;
|
2012-06-28 19:16:59 +00:00
|
|
|
|
|
|
|
import java.util.HashSet;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* EmailServiceStub is an abstract class representing an EmailService
|
|
|
|
*
|
|
|
|
* This class provides legacy support for a few methods that are common to both
|
|
|
|
* IMAP and POP3, including startSync, loadMore, loadAttachment, and sendMail
|
|
|
|
*/
|
|
|
|
public abstract class EmailServiceStub extends IEmailService.Stub implements IEmailService {
|
|
|
|
|
|
|
|
private static final int MAILBOX_COLUMN_ID = 0;
|
|
|
|
private static final int MAILBOX_COLUMN_SERVER_ID = 1;
|
|
|
|
private static final int MAILBOX_COLUMN_TYPE = 2;
|
|
|
|
|
|
|
|
/** Small projection for just the columns required for a sync. */
|
2014-04-11 21:42:28 +00:00
|
|
|
private static final String[] MAILBOX_PROJECTION = {
|
|
|
|
MailboxColumns._ID,
|
2012-06-28 19:16:59 +00:00
|
|
|
MailboxColumns.SERVER_ID,
|
|
|
|
MailboxColumns.TYPE,
|
|
|
|
};
|
|
|
|
|
2012-08-02 17:53:40 +00:00
|
|
|
protected Context mContext;
|
2012-06-28 19:16:59 +00:00
|
|
|
|
2013-07-30 02:11:41 +00:00
|
|
|
protected void init(Context context) {
|
2012-06-28 19:16:59 +00:00
|
|
|
mContext = context;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2014-07-02 20:29:58 +00:00
|
|
|
public Bundle validate(HostAuthCompat hostAuthCom) throws RemoteException {
|
2012-06-28 19:16:59 +00:00
|
|
|
// TODO Auto-generated method stub
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2014-01-29 23:24:06 +00:00
|
|
|
protected void requestSync(long mailboxId, boolean userRequest, int deltaMessageCount) {
|
2013-09-12 18:04:49 +00:00
|
|
|
final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
|
2012-06-28 19:16:59 +00:00
|
|
|
if (mailbox == null) return;
|
2013-09-12 18:04:49 +00:00
|
|
|
final Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
|
2012-06-28 19:16:59 +00:00
|
|
|
if (account == null) return;
|
2013-09-12 18:04:49 +00:00
|
|
|
final EmailServiceInfo info =
|
|
|
|
EmailServiceUtils.getServiceInfoForAccount(mContext, account.mId);
|
|
|
|
final android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress,
|
2012-09-22 00:49:26 +00:00
|
|
|
info.accountType);
|
2013-10-09 18:03:40 +00:00
|
|
|
final Bundle extras = Mailbox.createSyncBundle(mailboxId);
|
2013-03-12 19:38:01 +00:00
|
|
|
if (userRequest) {
|
|
|
|
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
|
|
|
|
extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
|
|
|
|
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
|
|
|
|
}
|
2013-04-05 01:30:39 +00:00
|
|
|
if (deltaMessageCount != 0) {
|
2013-04-25 20:52:49 +00:00
|
|
|
extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount);
|
2013-04-05 01:30:39 +00:00
|
|
|
}
|
2012-06-28 19:16:59 +00:00
|
|
|
ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras);
|
2014-08-05 09:43:02 +00:00
|
|
|
LogUtils.i(Logging.LOG_TAG, "requestSync EmailServiceStub requestSync %s, %s",
|
2013-10-01 23:11:54 +00:00
|
|
|
account.toString(), extras.toString());
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
2014-08-05 09:43:02 +00:00
|
|
|
/**
|
|
|
|
* Delete a single message by moving it to the trash, or really delete it if it's already in
|
|
|
|
* trash or a draft message.
|
|
|
|
*
|
|
|
|
* This function has no callback, no result reporting, because the desired outcome
|
|
|
|
* is reflected entirely by changes to one or more cursors.
|
|
|
|
*
|
|
|
|
* @param messageId The id of the message to "delete".
|
|
|
|
*/
|
|
|
|
public void deleteMessage(long messageId) {
|
|
|
|
|
|
|
|
final EmailContent.Message message =
|
|
|
|
EmailContent.Message.restoreMessageWithId(mContext, messageId);
|
|
|
|
if (message == null) {
|
|
|
|
if (Logging.LOGD) LogUtils.v(Logging.LOG_TAG, "dletMsg message NULL");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// 1. Get the message's account
|
|
|
|
final Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
|
|
|
|
// 2. Get the message's original mailbox
|
|
|
|
final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
|
|
|
|
if (account == null || mailbox == null) {
|
|
|
|
if (Logging.LOGD) LogUtils.v(Logging.LOG_TAG, "dletMsg account or mailbox NULL");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if(Logging.LOGD)
|
|
|
|
LogUtils.d(Logging.LOG_TAG, "AccountKey " + account.mId + "oirigMailbix: "
|
|
|
|
+ mailbox.mId);
|
|
|
|
// 3. Confirm that there is a trash mailbox available. If not, create one
|
|
|
|
Mailbox trashFolder = Mailbox.restoreMailboxOfType(mContext, account.mId,
|
|
|
|
Mailbox.TYPE_TRASH);
|
|
|
|
if (trashFolder == null) {
|
|
|
|
if (Logging.LOGD) LogUtils.v(Logging.LOG_TAG, "dletMsg Trash mailbox NULL");
|
|
|
|
} else {
|
|
|
|
LogUtils.d(Logging.LOG_TAG, "TrasMailbix: " + trashFolder.mId);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 4. Drop non-essential data for the message (e.g. attachment files)
|
|
|
|
AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId,
|
|
|
|
messageId);
|
|
|
|
|
|
|
|
Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI,
|
|
|
|
messageId);
|
|
|
|
|
|
|
|
// 5. Perform "delete" as appropriate
|
|
|
|
if ((mailbox.mId == trashFolder.mId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) {
|
|
|
|
// 5a. Really delete it
|
|
|
|
mContext.getContentResolver().delete(uri, null, null);
|
|
|
|
} else {
|
|
|
|
// 5b. Move to trash
|
|
|
|
ContentValues cv = new ContentValues();
|
|
|
|
cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashFolder.mId);
|
|
|
|
mContext.getContentResolver().update(uri, cv, null, null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Moves messages to a new mailbox.
|
|
|
|
* This function has no callback, no result reporting, because the desired outcome
|
|
|
|
* is reflected entirely by changes to one or more cursors.
|
|
|
|
* Note this method assumes all of the given message and mailbox IDs belong to the same
|
|
|
|
* account.
|
|
|
|
*
|
|
|
|
* @param messageIds IDs of the messages that are to be moved
|
|
|
|
* @param newMailboxId ID of the new mailbox that the messages will be moved to
|
|
|
|
* @return an asynchronous task that executes the move (for testing only)
|
|
|
|
*/
|
|
|
|
public void MoveMessages(long messageId, long newMailboxId) {
|
|
|
|
Account account = Account.getAccountForMessageId(mContext, messageId);
|
|
|
|
if (account != null) {
|
|
|
|
if (Logging.LOGD) {
|
|
|
|
LogUtils.d(Logging.LOG_TAG, "moveMessage Acct " + account.mId);
|
|
|
|
LogUtils.d(Logging.LOG_TAG, "moveMessage messageId:" + messageId);
|
|
|
|
}
|
|
|
|
ContentValues cv = new ContentValues();
|
|
|
|
cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId);
|
|
|
|
ContentResolver resolver = mContext.getContentResolver();
|
|
|
|
Uri uri = ContentUris.withAppendedId(
|
|
|
|
EmailContent.Message.SYNCED_CONTENT_URI, messageId);
|
|
|
|
resolver.update(uri, cv, null, null);
|
|
|
|
} else {
|
|
|
|
LogUtils.d(Logging.LOG_TAG, "moveMessage Cannot find account");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set/clear boolean columns of a message
|
|
|
|
* @param messageId the message to update
|
|
|
|
* @param columnName the column to update
|
|
|
|
* @param columnValue the new value for the column
|
|
|
|
*/
|
|
|
|
private void setMessageBoolean(long messageId, String columnName, boolean columnValue) {
|
|
|
|
ContentValues cv = new ContentValues();
|
|
|
|
cv.put(columnName, columnValue);
|
|
|
|
Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
|
|
|
|
mContext.getContentResolver().update(uri, cv, null, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set/clear the unread status of a message
|
|
|
|
*
|
|
|
|
* @param messageId the message to update
|
|
|
|
* @param isRead the new value for the isRead flag
|
|
|
|
*/
|
|
|
|
public void setMessageRead(long messageId, boolean isRead) {
|
|
|
|
setMessageBoolean(messageId, EmailContent.MessageColumns.FLAG_READ, isRead);
|
|
|
|
}
|
|
|
|
|
2012-06-28 19:16:59 +00:00
|
|
|
@Override
|
2014-02-26 16:57:53 +00:00
|
|
|
public void loadAttachment(final IEmailServiceCallback cb, final long accountId,
|
|
|
|
final long attachmentId, final boolean background) throws RemoteException {
|
2013-10-08 22:40:05 +00:00
|
|
|
Folder remoteFolder = null;
|
2012-06-28 19:16:59 +00:00
|
|
|
try {
|
|
|
|
//1. Check if the attachment is already here and return early in that case
|
|
|
|
Attachment attachment =
|
|
|
|
Attachment.restoreAttachmentWithId(mContext, attachmentId);
|
|
|
|
if (attachment == null) {
|
2013-07-30 02:11:41 +00:00
|
|
|
cb.loadAttachmentStatus(0, attachmentId,
|
2012-06-28 19:16:59 +00:00
|
|
|
EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0);
|
|
|
|
return;
|
|
|
|
}
|
2013-09-12 18:04:49 +00:00
|
|
|
final long messageId = attachment.mMessageKey;
|
2012-06-28 19:16:59 +00:00
|
|
|
|
2013-09-12 18:04:49 +00:00
|
|
|
final EmailContent.Message message =
|
2012-06-28 19:16:59 +00:00
|
|
|
EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey);
|
|
|
|
if (message == null) {
|
2013-07-30 02:11:41 +00:00
|
|
|
cb.loadAttachmentStatus(messageId, attachmentId,
|
2012-06-28 19:16:59 +00:00
|
|
|
EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
|
2013-09-12 18:04:49 +00:00
|
|
|
return;
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the message is loaded, just report that we're finished
|
2013-09-12 18:04:49 +00:00
|
|
|
if (Utility.attachmentExists(mContext, attachment)
|
|
|
|
&& attachment.mUiState == UIProvider.AttachmentState.SAVED) {
|
2013-07-30 02:11:41 +00:00
|
|
|
cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS,
|
2012-06-28 19:16:59 +00:00
|
|
|
0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Say we're starting...
|
2013-07-30 02:11:41 +00:00
|
|
|
cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.IN_PROGRESS, 0);
|
2012-06-28 19:16:59 +00:00
|
|
|
|
|
|
|
// 2. Open the remote folder.
|
2013-09-12 18:04:49 +00:00
|
|
|
final Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
|
2012-06-28 19:16:59 +00:00
|
|
|
Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
|
2014-03-26 17:38:51 +00:00
|
|
|
if (mailbox == null) {
|
|
|
|
// This could be null if the account is deleted at just the wrong time.
|
|
|
|
return;
|
2014-03-26 17:47:54 +00:00
|
|
|
}
|
|
|
|
if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
|
2012-06-28 19:16:59 +00:00
|
|
|
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);
|
Add an additional mailbox key column to message table
b/11294681
The problem is that when we try to open an attachment for a
message in search results, it fails. The reason is that part of
loading the attachment, we need to open the remote folder the
message is in. For search results, the message's mailboxKey is
the special fake "search_results" folder, which doesn't actually
exist on the server.
For this change, I've added a new column called "mainMailboxKey".
For search results, this column will be populated with the real
mailbox the message is in. It will be blank for other messages.
This is a quick and low risk fix for this bug, but it's kind
of awkward. We would prefer to do one or both of the following
some time after MR1.
1. Make the "search_results" folder be a virtual folder, the same
way that unread, starred, and other virtual folders are. For these,
there is actually no mailbox row in the database, just some
queries that check various flags in the messages and behave
like folders in the UI. The messages actually still reside in the
real folders.
2. Remove the requirement to open the folder at all to load the
attachment.
Change-Id: I825ab846f78bf8b041a5d1d579260dc5d7b4c522
2013-10-23 18:18:54 +00:00
|
|
|
if (sourceId != -1) {
|
2012-06-28 19:16:59 +00:00
|
|
|
EmailContent.Message sourceMsg =
|
|
|
|
EmailContent.Message.restoreMessageWithId(mContext, sourceId);
|
|
|
|
if (sourceMsg != null) {
|
|
|
|
mailbox = Mailbox.restoreMailboxWithId(mContext, sourceMsg.mMailboxKey);
|
|
|
|
message.mServerId = sourceMsg.mServerId;
|
|
|
|
}
|
|
|
|
}
|
Add an additional mailbox key column to message table
b/11294681
The problem is that when we try to open an attachment for a
message in search results, it fails. The reason is that part of
loading the attachment, we need to open the remote folder the
message is in. For search results, the message's mailboxKey is
the special fake "search_results" folder, which doesn't actually
exist on the server.
For this change, I've added a new column called "mainMailboxKey".
For search results, this column will be populated with the real
mailbox the message is in. It will be blank for other messages.
This is a quick and low risk fix for this bug, but it's kind
of awkward. We would prefer to do one or both of the following
some time after MR1.
1. Make the "search_results" folder be a virtual folder, the same
way that unread, starred, and other virtual folders are. For these,
there is actually no mailbox row in the database, just some
queries that check various flags in the messages and behave
like folders in the UI. The messages actually still reside in the
real folders.
2. Remove the requirement to open the folder at all to load the
attachment.
Change-Id: I825ab846f78bf8b041a5d1d579260dc5d7b4c522
2013-10-23 18:18:54 +00:00
|
|
|
} else if (mailbox.mType == Mailbox.TYPE_SEARCH && message.mMainMailboxKey != 0) {
|
|
|
|
mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMainMailboxKey);
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (account == null || mailbox == null) {
|
|
|
|
// If the account/mailbox are gone, just report success; the UI handles this
|
2013-07-30 02:11:41 +00:00
|
|
|
cb.loadAttachmentStatus(messageId, attachmentId,
|
2012-06-28 19:16:59 +00:00
|
|
|
EmailServiceStatus.SUCCESS, 0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
TrafficStats.setThreadStatsTag(
|
|
|
|
TrafficFlags.getAttachmentFlags(mContext, account));
|
|
|
|
|
2013-09-12 18:04:49 +00:00
|
|
|
final Store remoteStore = Store.getInstance(account, mContext);
|
2013-10-08 22:40:05 +00:00
|
|
|
remoteFolder = remoteStore.getFolder(mailbox.mServerId);
|
2012-06-28 19:16:59 +00:00
|
|
|
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.
|
2013-09-12 18:04:49 +00:00
|
|
|
final Message storeMessage = remoteFolder.createMessage(message.mServerId);
|
|
|
|
final MimeBodyPart storePart = new MimeBodyPart();
|
2012-06-28 19:16:59 +00:00
|
|
|
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));
|
Add an additional mailbox key column to message table
b/11294681
The problem is that when we try to open an attachment for a
message in search results, it fails. The reason is that part of
loading the attachment, we need to open the remote folder the
message is in. For search results, the message's mailboxKey is
the special fake "search_results" folder, which doesn't actually
exist on the server.
For this change, I've added a new column called "mainMailboxKey".
For search results, this column will be populated with the real
mailbox the message is in. It will be blank for other messages.
This is a quick and low risk fix for this bug, but it's kind
of awkward. We would prefer to do one or both of the following
some time after MR1.
1. Make the "search_results" folder be a virtual folder, the same
way that unread, starred, and other virtual folders are. For these,
there is actually no mailbox row in the database, just some
queries that check various flags in the messages and behave
like folders in the UI. The messages actually still reside in the
real folders.
2. Remove the requirement to open the folder at all to load the
attachment.
Change-Id: I825ab846f78bf8b041a5d1d579260dc5d7b4c522
2013-10-23 18:18:54 +00:00
|
|
|
|
2012-06-28 19:16:59 +00:00
|
|
|
// TODO is this always true for attachments? I think we dropped the
|
|
|
|
// true encoding along the way
|
|
|
|
storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
|
|
|
|
|
2013-09-12 18:04:49 +00:00
|
|
|
final MimeMultipart multipart = new MimeMultipart();
|
2012-06-28 19:16:59 +00:00
|
|
|
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
|
2013-09-12 18:04:49 +00:00
|
|
|
final FetchProfile fp = new FetchProfile();
|
2012-06-28 19:16:59 +00:00
|
|
|
fp.add(storePart);
|
|
|
|
remoteFolder.fetch(new Message[] { storeMessage }, fp,
|
2013-07-30 02:11:41 +00:00
|
|
|
new MessageRetrievalListenerBridge(messageId, attachmentId, cb));
|
2012-06-28 19:16:59 +00:00
|
|
|
|
|
|
|
// If we failed to load the attachment, throw an Exception here, so that
|
2014-06-25 16:56:29 +00:00
|
|
|
// AttachmentService knows that we failed
|
2012-06-28 19:16:59 +00:00
|
|
|
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
|
2013-07-30 02:11:41 +00:00
|
|
|
cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0);
|
2012-06-28 19:16:59 +00:00
|
|
|
|
2013-10-08 22:40:05 +00:00
|
|
|
} catch (MessagingException me) {
|
|
|
|
LogUtils.i(Logging.LOG_TAG, me, "Error loading attachment");
|
2012-06-28 19:16:59 +00:00
|
|
|
|
2013-09-12 18:04:49 +00:00
|
|
|
final ContentValues cv = new ContentValues(1);
|
2012-06-28 19:16:59 +00:00
|
|
|
cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED);
|
2013-09-12 18:04:49 +00:00
|
|
|
final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
|
2012-06-28 19:16:59 +00:00
|
|
|
mContext.getContentResolver().update(uri, cv, null, null);
|
|
|
|
|
2013-07-30 02:11:41 +00:00
|
|
|
cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.CONNECTION_ERROR, 0);
|
2013-10-08 22:40:05 +00:00
|
|
|
} finally {
|
|
|
|
if (remoteFolder != null) {
|
|
|
|
remoteFolder.close(false);
|
|
|
|
}
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and
|
2013-09-12 18:04:49 +00:00
|
|
|
* pass down to {@link IEmailServiceCallback}.
|
2012-06-28 19:16:59 +00:00
|
|
|
*/
|
|
|
|
public class MessageRetrievalListenerBridge implements MessageRetrievalListener {
|
|
|
|
private final long mMessageId;
|
|
|
|
private final long mAttachmentId;
|
2013-07-30 02:11:41 +00:00
|
|
|
private final IEmailServiceCallback mCallback;
|
2012-06-28 19:16:59 +00:00
|
|
|
|
2013-07-30 02:11:41 +00:00
|
|
|
|
|
|
|
public MessageRetrievalListenerBridge(final long messageId, final long attachmentId,
|
|
|
|
final IEmailServiceCallback callback) {
|
2012-06-28 19:16:59 +00:00
|
|
|
mMessageId = messageId;
|
|
|
|
mAttachmentId = attachmentId;
|
2013-07-30 02:11:41 +00:00
|
|
|
mCallback = callback;
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void loadAttachmentProgress(int progress) {
|
2013-07-30 02:11:41 +00:00
|
|
|
try {
|
|
|
|
mCallback.loadAttachmentStatus(mMessageId, mAttachmentId,
|
|
|
|
EmailServiceStatus.IN_PROGRESS, progress);
|
|
|
|
} catch (final RemoteException e) {
|
|
|
|
// No danger if the client is no longer around
|
|
|
|
}
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void messageRetrieved(com.android.emailcommon.mail.Message message) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2014-06-12 17:59:57 +00:00
|
|
|
public void updateFolderList(final long accountId) throws RemoteException {
|
2013-09-12 18:04:49 +00:00
|
|
|
final Account account = Account.restoreAccountWithId(mContext, accountId);
|
2014-06-12 17:59:57 +00:00
|
|
|
if (account == null) {
|
|
|
|
LogUtils.e(LogUtils.TAG, "Account %d not found in updateFolderList", accountId);
|
|
|
|
return;
|
|
|
|
};
|
2013-03-20 22:23:51 +00:00
|
|
|
long inboxId = -1;
|
2012-06-28 19:16:59 +00:00
|
|
|
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
|
|
|
|
Cursor localFolderCursor = null;
|
2014-01-03 21:45:08 +00:00
|
|
|
Store store = null;
|
2012-06-28 19:16:59 +00:00
|
|
|
try {
|
2014-10-14 20:03:34 +00:00
|
|
|
store = Store.getInstance(account, mContext);
|
|
|
|
|
2013-03-20 22:23:51 +00:00
|
|
|
// Step 0: Make sure the default system mailboxes exist.
|
2013-08-01 23:19:28 +00:00
|
|
|
for (final int type : Mailbox.REQUIRED_FOLDER_TYPES) {
|
2013-03-20 22:23:51 +00:00
|
|
|
if (Mailbox.findMailboxOfType(mContext, accountId, type) == Mailbox.NO_MAILBOX) {
|
2013-09-12 18:04:49 +00:00
|
|
|
final Mailbox mailbox = Mailbox.newSystemMailbox(mContext, accountId, type);
|
2014-10-14 20:03:34 +00:00
|
|
|
if (store.canSyncFolderType(type)) {
|
|
|
|
// If this folder is syncable, then we should set its UISyncStatus.
|
|
|
|
// Otherwise the UI could show the empty state until the sync
|
|
|
|
// actually occurs.
|
|
|
|
mailbox.mUiSyncStatus = Mailbox.SYNC_STATUS_INITIAL_SYNC_NEEDED;
|
|
|
|
}
|
2013-03-20 22:23:51 +00:00
|
|
|
mailbox.save(mContext);
|
|
|
|
if (type == Mailbox.TYPE_INBOX) {
|
|
|
|
inboxId = mailbox.mId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-06-28 19:16:59 +00:00
|
|
|
// Step 1: Get remote mailboxes
|
2013-09-12 18:04:49 +00:00
|
|
|
final Folder[] remoteFolders = store.updateFolders();
|
|
|
|
final HashSet<String> remoteFolderNames = new HashSet<String>();
|
|
|
|
for (final Folder remoteFolder : remoteFolders) {
|
|
|
|
remoteFolderNames.add(remoteFolder.getName());
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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()) {
|
2013-09-12 18:04:49 +00:00
|
|
|
final String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID);
|
2012-06-28 19:16:59 +00:00
|
|
|
// Short circuit if we have a remote mailbox with the same name
|
|
|
|
if (remoteFolderNames.contains(mailboxPath)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2013-09-12 18:04:49 +00:00
|
|
|
final int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE);
|
|
|
|
final long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID);
|
2012-06-28 19:16:59 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
} catch (MessagingException me) {
|
|
|
|
LogUtils.i(Logging.LOG_TAG, me, "Error in updateFolderList");
|
2012-06-28 19:16:59 +00:00
|
|
|
// We'll hope this is temporary
|
2014-06-12 17:59:57 +00:00
|
|
|
// TODO: Figure out what type of messaging exception it was and return an appropriate
|
|
|
|
// result. If we start doing this from sync, it's important to let the sync manager
|
|
|
|
// know if the failure was due to IO error or authentication errors.
|
2012-06-28 19:16:59 +00:00
|
|
|
} finally {
|
|
|
|
if (localFolderCursor != null) {
|
|
|
|
localFolderCursor.close();
|
|
|
|
}
|
2014-01-03 22:31:33 +00:00
|
|
|
if (store != null) {
|
|
|
|
store.closeConnections();
|
|
|
|
}
|
2013-03-20 22:23:51 +00:00
|
|
|
// If we just created the inbox, sync it
|
|
|
|
if (inboxId != -1) {
|
2014-01-29 23:24:06 +00:00
|
|
|
requestSync(inboxId, true, 0);
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2014-06-12 17:59:57 +00:00
|
|
|
public void setLogging(final int flags) throws RemoteException {
|
2012-06-28 19:16:59 +00:00
|
|
|
// Not required
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2014-06-12 17:59:57 +00:00
|
|
|
public Bundle autoDiscover(final String userName, final String password)
|
|
|
|
throws RemoteException {
|
2012-06-28 19:16:59 +00:00
|
|
|
// Not required
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2014-06-12 17:59:57 +00:00
|
|
|
public void sendMeetingResponse(final long messageId, final int response)
|
|
|
|
throws RemoteException {
|
2012-06-28 19:16:59 +00:00
|
|
|
// Not required
|
|
|
|
}
|
|
|
|
|
2014-05-21 20:41:37 +00:00
|
|
|
@Override
|
2014-07-10 22:08:29 +00:00
|
|
|
public void deleteExternalAccountPIMData(final String emailAddress) throws RemoteException {
|
|
|
|
// No need to do anything here, for IMAP and POP accounts none of our data is external.
|
2014-05-21 20:41:37 +00:00
|
|
|
}
|
|
|
|
|
2012-06-28 19:16:59 +00:00
|
|
|
@Override
|
2014-06-12 17:59:57 +00:00
|
|
|
public int searchMessages(final long accountId, final SearchParams params,
|
|
|
|
final long destMailboxId)
|
2012-06-28 19:16:59 +00:00
|
|
|
throws RemoteException {
|
|
|
|
// Not required
|
2014-06-12 17:59:57 +00:00
|
|
|
return EmailServiceStatus.SUCCESS;
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
2014-02-25 01:34:37 +00:00
|
|
|
@Override
|
2014-06-12 17:59:57 +00:00
|
|
|
public void pushModify(final long accountId) throws RemoteException {
|
2014-02-25 01:34:37 +00:00
|
|
|
LogUtils.e(Logging.LOG_TAG, "pushModify invalid for account type for %d", accountId);
|
|
|
|
}
|
|
|
|
|
2014-02-26 02:07:07 +00:00
|
|
|
@Override
|
2014-06-12 17:59:57 +00:00
|
|
|
public int sync(final long accountId, final Bundle syncExtras) {
|
|
|
|
return EmailServiceStatus.SUCCESS;
|
2014-05-22 20:21:55 +00:00
|
|
|
|
2014-06-12 17:59:57 +00:00
|
|
|
}
|
2014-02-26 02:07:07 +00:00
|
|
|
|
2012-06-28 19:16:59 +00:00
|
|
|
@Override
|
2014-06-12 17:59:57 +00:00
|
|
|
public void sendMail(final long accountId) throws RemoteException {
|
2012-06-28 19:16:59 +00:00
|
|
|
sendMailImpl(mContext, accountId);
|
|
|
|
}
|
|
|
|
|
2014-06-12 17:59:57 +00:00
|
|
|
public static void sendMailImpl(final Context context, final long accountId) {
|
2013-09-12 18:04:49 +00:00
|
|
|
final Account account = Account.restoreAccountWithId(context, accountId);
|
2014-06-12 17:59:57 +00:00
|
|
|
if (account == null) {
|
|
|
|
LogUtils.e(LogUtils.TAG, "account %d not found in sendMailImpl", accountId);
|
|
|
|
return;
|
|
|
|
}
|
2012-06-28 19:16:59 +00:00
|
|
|
TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(context, account));
|
2014-09-07 20:36:33 +00:00
|
|
|
final NotificationController nc =
|
|
|
|
NotificationControllerCreatorHolder.getInstance(context);
|
2012-06-28 19:16:59 +00:00
|
|
|
// 1. Loop through all messages in the account's outbox
|
2013-09-12 18:04:49 +00:00
|
|
|
final long outboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX);
|
2012-06-28 19:16:59 +00:00
|
|
|
if (outboxId == Mailbox.NO_MAILBOX) {
|
|
|
|
return;
|
|
|
|
}
|
2013-09-12 18:04:49 +00:00
|
|
|
final ContentResolver resolver = context.getContentResolver();
|
|
|
|
final Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
|
2012-06-28 19:16:59 +00:00
|
|
|
EmailContent.Message.ID_COLUMN_PROJECTION,
|
2014-07-10 22:08:29 +00:00
|
|
|
MessageColumns.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId)},
|
2012-06-28 19:16:59 +00:00
|
|
|
null);
|
|
|
|
try {
|
|
|
|
// 2. exit early
|
|
|
|
if (c.getCount() <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
2013-09-12 18:04:49 +00:00
|
|
|
final Sender sender = Sender.getInstance(context, account);
|
|
|
|
final Store remoteStore = Store.getInstance(account, context);
|
|
|
|
final ContentValues moveToSentValues;
|
|
|
|
if (remoteStore.requireCopyMessageToSentFolder()) {
|
2012-06-28 19:16:59 +00:00
|
|
|
Mailbox sentFolder =
|
|
|
|
Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SENT);
|
|
|
|
moveToSentValues = new ContentValues();
|
|
|
|
moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId);
|
2013-09-12 18:04:49 +00:00
|
|
|
} else {
|
|
|
|
moveToSentValues = null;
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 3. loop through the available messages and send them
|
|
|
|
while (c.moveToNext()) {
|
2013-09-12 18:04:49 +00:00
|
|
|
final long messageId;
|
2013-03-22 01:49:55 +00:00
|
|
|
if (moveToSentValues != null) {
|
|
|
|
moveToSentValues.remove(EmailContent.MessageColumns.FLAGS);
|
|
|
|
}
|
2012-06-28 19:16:59 +00:00
|
|
|
try {
|
|
|
|
messageId = c.getLong(0);
|
|
|
|
// Don't send messages with unloaded attachments
|
|
|
|
if (Utility.hasUnloadedAttachments(context, messageId)) {
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(Logging.LOG_TAG, "Can't send #" + messageId +
|
2012-06-28 19:16:59 +00:00
|
|
|
"; unloaded attachments");
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
sender.sendMessage(messageId);
|
|
|
|
} catch (MessagingException me) {
|
|
|
|
// report error for this message, but keep trying others
|
2014-09-07 20:36:33 +00:00
|
|
|
if (me instanceof AuthenticationFailedException && nc != null) {
|
2014-07-11 17:08:20 +00:00
|
|
|
nc.showLoginFailedNotificationSynchronous(account.mId,
|
|
|
|
false /* incoming */);
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// 4. move to sent, or delete
|
2013-02-23 03:42:40 +00:00
|
|
|
final Uri syncedUri =
|
2012-06-28 19:16:59 +00:00
|
|
|
ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
|
2013-02-23 03:42:40 +00:00
|
|
|
// Delete all cached files
|
|
|
|
AttachmentUtilities.deleteAllCachedAttachmentFiles(context, account.mId, messageId);
|
2013-09-12 18:04:49 +00:00
|
|
|
if (moveToSentValues != null) {
|
2012-06-28 19:16:59 +00:00
|
|
|
// If this is a forwarded message and it has attachments, delete them, as they
|
|
|
|
// duplicate information found elsewhere (on the server). This saves storage.
|
2013-02-23 03:42:40 +00:00
|
|
|
final EmailContent.Message msg =
|
2012-06-28 19:16:59 +00:00
|
|
|
EmailContent.Message.restoreMessageWithId(context, messageId);
|
2013-09-12 18:04:49 +00:00
|
|
|
if ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0) {
|
2012-06-28 19:16:59 +00:00
|
|
|
AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
|
|
|
|
messageId);
|
|
|
|
}
|
2013-02-23 03:42:40 +00:00
|
|
|
final int flags = msg.mFlags & ~(EmailContent.Message.FLAG_TYPE_REPLY |
|
2013-10-15 00:40:44 +00:00
|
|
|
EmailContent.Message.FLAG_TYPE_FORWARD |
|
|
|
|
EmailContent.Message.FLAG_TYPE_REPLY_ALL |
|
|
|
|
EmailContent.Message.FLAG_TYPE_ORIGINAL);
|
|
|
|
|
2012-06-28 19:16:59 +00:00
|
|
|
moveToSentValues.put(EmailContent.MessageColumns.FLAGS, flags);
|
|
|
|
resolver.update(syncedUri, moveToSentValues, null, null);
|
|
|
|
} else {
|
|
|
|
AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
|
|
|
|
messageId);
|
2013-02-23 03:42:40 +00:00
|
|
|
final Uri uri =
|
2012-06-28 19:16:59 +00:00
|
|
|
ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
|
|
|
|
resolver.delete(uri, null, null);
|
|
|
|
resolver.delete(syncedUri, null, null);
|
|
|
|
}
|
|
|
|
}
|
2014-09-07 20:36:33 +00:00
|
|
|
if (nc != null) {
|
|
|
|
nc.cancelLoginFailedNotification(account.mId);
|
|
|
|
}
|
2012-06-28 19:16:59 +00:00
|
|
|
} catch (MessagingException me) {
|
2014-09-07 20:36:33 +00:00
|
|
|
if (me instanceof AuthenticationFailedException && nc != null) {
|
2014-07-11 17:08:20 +00:00
|
|
|
nc.showLoginFailedNotificationSynchronous(account.mId, false /* incoming */);
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
c.close();
|
|
|
|
}
|
|
|
|
}
|
2014-07-10 22:08:29 +00:00
|
|
|
|
|
|
|
public int getApiVersion() {
|
|
|
|
return EmailServiceVersion.CURRENT;
|
|
|
|
}
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|