/* 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; import android.text.TextUtils; import android.util.Log; import com.android.email.NotificationController; import com.android.email.R; import com.android.email.mail.Sender; import com.android.email.mail.Store; import com.android.email.provider.Utilities; import com.android.email2.ui.MailActivityEmail; import com.android.emailcommon.Api; 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.HostAuth; import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.service.EmailServiceStatus; 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; 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; public static final String SYNC_EXTRA_MAILBOX_ID = "__mailboxId__"; /** Small projection for just the columns required for a sync. */ private static final String[] MAILBOX_PROJECTION = new String[] { MailboxColumns.ID, MailboxColumns.SERVER_ID, MailboxColumns.TYPE, }; protected Context mContext; private IEmailServiceCallback.Stub mCallback; protected void init(Context context, IEmailServiceCallback.Stub callbackProxy) { mContext = context; mCallback = callbackProxy; } @Override public Bundle validate(HostAuth hostauth) throws RemoteException { // TODO Auto-generated method stub return null; } @Override public void startSync(long mailboxId, boolean userRequest) throws RemoteException { Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); if (mailbox == null) return; Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); if (account == null) return; android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress, mContext.getString(R.string.account_manager_type_pop3)); Bundle extras = new Bundle(); extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); extras.putLong(SYNC_EXTRA_MAILBOX_ID, mailboxId); ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras); } @Override public void stopSync(long mailboxId) throws RemoteException { // Not required } @Override public void loadMore(long messageId) throws RemoteException { // Load a message for view... try { // 1. Resample the message, in case it disappeared or synced while // this command was in queue EmailContent.Message message = EmailContent.Message.restoreMessageWithId(mContext, messageId); if (message == null) { mCallback.loadMessageStatus(messageId, EmailServiceStatus.MESSAGE_NOT_FOUND, 0); return; } if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) { // We should NEVER get here mCallback.loadMessageStatus(messageId, 0, 100); return; } // 2. Open the remote folder. // TODO combine with common code in loadAttachment Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); 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)); Store remoteStore = Store.getInstance(account, mContext); String remoteServerId = mailbox.mServerId; // If this is a search result, use the protocolSearchInfo field to get the // correct remote location if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) { remoteServerId = message.mProtocolSearchInfo; } Folder remoteFolder = remoteStore.getFolder(remoteServerId); remoteFolder.open(OpenMode.READ_WRITE); // 3. Set up to download the entire message Message remoteMessage = remoteFolder.getMessage(message.mServerId); 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); // 5. Notify UI mCallback.loadMessageStatus(messageId, 0, 100); } catch (MessagingException me) { if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0); } catch (RuntimeException rte) { mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0); } } private void doProgressCallback(long messageId, long attachmentId, int progress) { try { mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.IN_PROGRESS, progress); } catch (RemoteException e) { // No danger if the client is no longer around } } @Override public void loadAttachment(long attachmentId, boolean background) throws RemoteException { try { //1. Check if the attachment is already here and return early in that case Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); if (attachment == null) { mCallback.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0); return; } long messageId = attachment.mMessageKey; EmailContent.Message message = EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey); if (message == null) { mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.MESSAGE_NOT_FOUND, 0); } // If the message is loaded, just report that we're finished if (Utility.attachmentExists(mContext, attachment)) { mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); return; } // Say we're starting... doProgressCallback(messageId, attachmentId, 0); // 2. Open the remote folder. Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 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; } } } if (account == null || mailbox == null) { // If the account/mailbox are gone, just report success; the UI handles this mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); return; } TrafficStats.setThreadStatsTag( TrafficFlags.getAttachmentFlags(mContext, account)); Store remoteStore = Store.getInstance(account, mContext); Folder 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. Message storeMessage = remoteFolder.createMessage(message.mServerId); 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 storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); 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 FetchProfile fp = new FetchProfile(); fp.add(storePart); remoteFolder.fetch(new Message[] { storeMessage }, fp, new MessageRetrievalListenerBridge(messageId, attachmentId)); // 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 mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); // Close the connection remoteFolder.close(false); } catch (MessagingException me) { if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); // TODO: Fix this up; consider the best approach ContentValues cv = new ContentValues(); cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED); Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); mContext.getContentResolver().update(uri, cv, null, null); mCallback.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.CONNECTION_ERROR, 0); } } /** * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and * pass down to {@link Result}. */ public class MessageRetrievalListenerBridge implements MessageRetrievalListener { private final long mMessageId; private final long mAttachmentId; public MessageRetrievalListenerBridge(long messageId, long attachmentId) { mMessageId = messageId; mAttachmentId = attachmentId; } @Override public void loadAttachmentProgress(int progress) { doProgressCallback(mMessageId, mAttachmentId, progress); } @Override public void messageRetrieved(com.android.emailcommon.mail.Message message) { } } @Override public void updateFolderList(long accountId) throws RemoteException { Account account = Account.restoreAccountWithId(mContext, accountId); if (account == null) return; Mailbox inbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX); TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); Cursor localFolderCursor = null; try { // Step 1: Get remote mailboxes Store store = Store.getInstance(account, mContext); Folder[] remoteFolders = store.updateFolders(); HashSet remoteFolderNames = new HashSet(); for (int i = 0, count = remoteFolders.length; i < count; i++) { remoteFolderNames.add(remoteFolders[i].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()) { 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; } int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE); 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 e) { // We'll hope this is temporary } finally { if (localFolderCursor != null) { localFolderCursor.close(); } // If this is a first sync, find the inbox and sync it if (inbox == null) { inbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX); if (inbox != null) { startSync(inbox.mId, true); } } } } @Override public boolean createFolder(long accountId, String name) throws RemoteException { // Not required return false; } @Override public boolean deleteFolder(long accountId, String name) throws RemoteException { // Not required return false; } @Override public boolean renameFolder(long accountId, String oldName, String newName) throws RemoteException { // Not required return false; } @Override public void setCallback(IEmailServiceCallback cb) throws RemoteException { // Not required } @Override public void setLogging(int on) throws RemoteException { // Not required } @Override public void hostChanged(long accountId) throws RemoteException { // Not required } @Override public Bundle autoDiscover(String userName, String password) throws RemoteException { // Not required return null; } @Override public void sendMeetingResponse(long messageId, int response) throws RemoteException { // Not required } @Override public void deleteAccountPIMData(long accountId) throws RemoteException { MailService.reconcilePopAccountsSync(mContext); } @Override public int getApiLevel() throws RemoteException { return Api.LEVEL; } @Override public int searchMessages(long accountId, SearchParams params, long destMailboxId) throws RemoteException { // Not required return 0; } @Override public void sendMail(long accountId) throws RemoteException { sendMailImpl(mContext, accountId); } public static void sendMailImpl(Context context, long accountId) { Account account = Account.restoreAccountWithId(context, accountId); TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(context, account)); NotificationController nc = NotificationController.getInstance(context); // 1. Loop through all messages in the account's outbox long outboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX); if (outboxId == Mailbox.NO_MAILBOX) { return; } ContentResolver resolver = context.getContentResolver(); 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; } Sender sender = Sender.getInstance(context, account); Store remoteStore = Store.getInstance(account, context); boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder(); ContentValues moveToSentValues = null; if (requireMoveMessageToSentFolder) { Mailbox sentFolder = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SENT); moveToSentValues = new ContentValues(); moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId); } // 3. loop through the available messages and send them while (c.moveToNext()) { long messageId = -1; moveToSentValues.remove(EmailContent.MessageColumns.FLAGS); try { messageId = c.getLong(0); // Don't send messages with unloaded attachments if (Utility.hasUnloadedAttachments(context, messageId)) { if (MailActivityEmail.DEBUG) { Log.d(Logging.LOG_TAG, "Can't send #" + messageId + "; unloaded attachments"); } continue; } sender.sendMessage(messageId); } catch (MessagingException me) { // report error for this message, but keep trying others if (me instanceof AuthenticationFailedException) { nc.showLoginFailedNotification(account.mId); } continue; } // 4. move to sent, or delete Uri syncedUri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); if (requireMoveMessageToSentFolder) { // If this is a forwarded message and it has attachments, delete them, as they // duplicate information found elsewhere (on the server). This saves storage. EmailContent.Message msg = EmailContent.Message.restoreMessageWithId(context, messageId); if (msg != null && ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) { AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, messageId); } int flags = msg.mFlags & ~(EmailContent.Message.FLAG_TYPE_REPLY | EmailContent.Message.FLAG_TYPE_FORWARD); moveToSentValues.put(EmailContent.MessageColumns.FLAGS, flags); resolver.update(syncedUri, moveToSentValues, null, null); } else { AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, messageId); Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); resolver.delete(uri, null, null); resolver.delete(syncedUri, null, null); } } nc.cancelLoginFailedNotification(account.mId); } catch (MessagingException me) { if (me instanceof AuthenticationFailedException) { nc.showLoginFailedNotification(account.mId); } } finally { c.close(); } } }