Home | History | Annotate | Download | only in service
      1 /* Copyright (C) 2012 The Android Open Source Project
      2  *
      3  * Licensed under the Apache License, Version 2.0 (the "License");
      4  * you may not use this file except in compliance with the License.
      5  * You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software
     10  * distributed under the License is distributed on an "AS IS" BASIS,
     11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12  * See the License for the specific language governing permissions and
     13  * limitations under the License.
     14  */
     15 
     16 package com.android.email.service;
     17 
     18 import android.content.ContentResolver;
     19 import android.content.ContentUris;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.net.TrafficStats;
     24 import android.net.Uri;
     25 import android.os.Bundle;
     26 import android.os.RemoteException;
     27 
     28 import com.android.email.DebugUtils;
     29 import com.android.email.NotificationController;
     30 import com.android.email.NotificationControllerCreatorHolder;
     31 import com.android.email.mail.Sender;
     32 import com.android.email.mail.Store;
     33 import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
     34 import com.android.emailcommon.Logging;
     35 import com.android.emailcommon.TrafficFlags;
     36 import com.android.emailcommon.internet.MimeBodyPart;
     37 import com.android.emailcommon.internet.MimeHeader;
     38 import com.android.emailcommon.internet.MimeMultipart;
     39 import com.android.emailcommon.mail.AuthenticationFailedException;
     40 import com.android.emailcommon.mail.FetchProfile;
     41 import com.android.emailcommon.mail.Folder;
     42 import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
     43 import com.android.emailcommon.mail.Folder.OpenMode;
     44 import com.android.emailcommon.mail.Message;
     45 import com.android.emailcommon.mail.MessagingException;
     46 import com.android.emailcommon.provider.Account;
     47 import com.android.emailcommon.provider.EmailContent;
     48 import com.android.emailcommon.provider.EmailContent.Attachment;
     49 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     50 import com.android.emailcommon.provider.EmailContent.Body;
     51 import com.android.emailcommon.provider.EmailContent.BodyColumns;
     52 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     53 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     54 import com.android.emailcommon.provider.Mailbox;
     55 import com.android.emailcommon.service.EmailServiceStatus;
     56 import com.android.emailcommon.service.EmailServiceVersion;
     57 import com.android.emailcommon.service.HostAuthCompat;
     58 import com.android.emailcommon.service.IEmailService;
     59 import com.android.emailcommon.service.IEmailServiceCallback;
     60 import com.android.emailcommon.service.SearchParams;
     61 import com.android.emailcommon.utility.AttachmentUtilities;
     62 import com.android.emailcommon.utility.Utility;
     63 import com.android.mail.providers.UIProvider;
     64 import com.android.mail.utils.LogUtils;
     65 
     66 import java.util.HashSet;
     67 
     68 /**
     69  * EmailServiceStub is an abstract class representing an EmailService
     70  *
     71  * This class provides legacy support for a few methods that are common to both
     72  * IMAP and POP3, including startSync, loadMore, loadAttachment, and sendMail
     73  */
     74 public abstract class EmailServiceStub extends IEmailService.Stub implements IEmailService {
     75 
     76     private static final int MAILBOX_COLUMN_ID = 0;
     77     private static final int MAILBOX_COLUMN_SERVER_ID = 1;
     78     private static final int MAILBOX_COLUMN_TYPE = 2;
     79 
     80     /** Small projection for just the columns required for a sync. */
     81     private static final String[] MAILBOX_PROJECTION = {
     82         MailboxColumns._ID,
     83         MailboxColumns.SERVER_ID,
     84         MailboxColumns.TYPE,
     85     };
     86 
     87     protected Context mContext;
     88 
     89     protected void init(Context context) {
     90         mContext = context;
     91     }
     92 
     93     @Override
     94     public Bundle validate(HostAuthCompat hostAuthCom) throws RemoteException {
     95         // TODO Auto-generated method stub
     96         return null;
     97     }
     98 
     99     protected void requestSync(long mailboxId, boolean userRequest, int deltaMessageCount) {
    100         final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
    101         if (mailbox == null) return;
    102         final Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
    103         if (account == null) return;
    104         final EmailServiceInfo info =
    105                 EmailServiceUtils.getServiceInfoForAccount(mContext, account.mId);
    106         final android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress,
    107                 info.accountType);
    108         final Bundle extras = Mailbox.createSyncBundle(mailboxId);
    109         if (userRequest) {
    110             extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
    111             extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
    112             extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
    113         }
    114         if (deltaMessageCount != 0) {
    115             extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount);
    116         }
    117         ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras);
    118         LogUtils.i(Logging.LOG_TAG, "requestSync EmailServiceStub startSync %s, %s",
    119                 account.toString(), extras.toString());
    120     }
    121 
    122     @Override
    123     public void loadAttachment(final IEmailServiceCallback cb, final long accountId,
    124             final long attachmentId, final boolean background) throws RemoteException {
    125         Folder remoteFolder = null;
    126         try {
    127             //1. Check if the attachment is already here and return early in that case
    128             Attachment attachment =
    129                 Attachment.restoreAttachmentWithId(mContext, attachmentId);
    130             if (attachment == null) {
    131                 cb.loadAttachmentStatus(0, attachmentId,
    132                         EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0);
    133                 return;
    134             }
    135             final long messageId = attachment.mMessageKey;
    136 
    137             final EmailContent.Message message =
    138                     EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey);
    139             if (message == null) {
    140                 cb.loadAttachmentStatus(messageId, attachmentId,
    141                         EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
    142                 return;
    143             }
    144 
    145             // If the message is loaded, just report that we're finished
    146             if (Utility.attachmentExists(mContext, attachment)
    147                     && attachment.mUiState == UIProvider.AttachmentState.SAVED) {
    148                 cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS,
    149                         0);
    150                 return;
    151             }
    152 
    153             // Say we're starting...
    154             cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.IN_PROGRESS, 0);
    155 
    156             // 2. Open the remote folder.
    157             final Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
    158             Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
    159             if (mailbox == null) {
    160                 // This could be null if the account is deleted at just the wrong time.
    161                 return;
    162             }
    163             if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
    164                 long sourceId = Utility.getFirstRowLong(mContext, Body.CONTENT_URI,
    165                         new String[] {BodyColumns.SOURCE_MESSAGE_KEY},
    166                         BodyColumns.MESSAGE_KEY + "=?",
    167                         new String[] {Long.toString(messageId)}, null, 0, -1L);
    168                 if (sourceId != -1) {
    169                     EmailContent.Message sourceMsg =
    170                             EmailContent.Message.restoreMessageWithId(mContext, sourceId);
    171                     if (sourceMsg != null) {
    172                         mailbox = Mailbox.restoreMailboxWithId(mContext, sourceMsg.mMailboxKey);
    173                         message.mServerId = sourceMsg.mServerId;
    174                     }
    175                 }
    176             } else if (mailbox.mType == Mailbox.TYPE_SEARCH && message.mMainMailboxKey != 0) {
    177                 mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMainMailboxKey);
    178             }
    179 
    180             if (account == null || mailbox == null) {
    181                 // If the account/mailbox are gone, just report success; the UI handles this
    182                 cb.loadAttachmentStatus(messageId, attachmentId,
    183                         EmailServiceStatus.SUCCESS, 0);
    184                 return;
    185             }
    186             TrafficStats.setThreadStatsTag(
    187                     TrafficFlags.getAttachmentFlags(mContext, account));
    188 
    189             final Store remoteStore = Store.getInstance(account, mContext);
    190             remoteFolder = remoteStore.getFolder(mailbox.mServerId);
    191             remoteFolder.open(OpenMode.READ_WRITE);
    192 
    193             // 3. Generate a shell message in which to retrieve the attachment,
    194             // and a shell BodyPart for the attachment.  Then glue them together.
    195             final Message storeMessage = remoteFolder.createMessage(message.mServerId);
    196             final MimeBodyPart storePart = new MimeBodyPart();
    197             storePart.setSize((int)attachment.mSize);
    198             storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA,
    199                     attachment.mLocation);
    200             storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
    201                     String.format("%s;\n name=\"%s\"",
    202                     attachment.mMimeType,
    203                     attachment.mFileName));
    204 
    205             // TODO is this always true for attachments?  I think we dropped the
    206             // true encoding along the way
    207             storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
    208 
    209             final MimeMultipart multipart = new MimeMultipart();
    210             multipart.setSubType("mixed");
    211             multipart.addBodyPart(storePart);
    212 
    213             storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
    214             storeMessage.setBody(multipart);
    215 
    216             // 4. Now ask for the attachment to be fetched
    217             final FetchProfile fp = new FetchProfile();
    218             fp.add(storePart);
    219             remoteFolder.fetch(new Message[] { storeMessage }, fp,
    220                     new MessageRetrievalListenerBridge(messageId, attachmentId, cb));
    221 
    222             // If we failed to load the attachment, throw an Exception here, so that
    223             // AttachmentService knows that we failed
    224             if (storePart.getBody() == null) {
    225                 throw new MessagingException("Attachment not loaded.");
    226             }
    227 
    228             // Save the attachment to wherever it's going
    229             AttachmentUtilities.saveAttachment(mContext, storePart.getBody().getInputStream(),
    230                     attachment);
    231 
    232             // 6. Report success
    233             cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0);
    234 
    235         } catch (MessagingException me) {
    236             LogUtils.i(Logging.LOG_TAG, me, "Error loading attachment");
    237 
    238             final ContentValues cv = new ContentValues(1);
    239             cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED);
    240             final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
    241             mContext.getContentResolver().update(uri, cv, null, null);
    242 
    243             cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.CONNECTION_ERROR, 0);
    244         } finally {
    245             if (remoteFolder != null) {
    246                 remoteFolder.close(false);
    247             }
    248         }
    249 
    250     }
    251 
    252     /**
    253      * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and
    254      * pass down to {@link IEmailServiceCallback}.
    255      */
    256     public class MessageRetrievalListenerBridge implements MessageRetrievalListener {
    257         private final long mMessageId;
    258         private final long mAttachmentId;
    259         private final IEmailServiceCallback mCallback;
    260 
    261 
    262         public MessageRetrievalListenerBridge(final long messageId, final long attachmentId,
    263                 final IEmailServiceCallback callback) {
    264             mMessageId = messageId;
    265             mAttachmentId = attachmentId;
    266             mCallback = callback;
    267         }
    268 
    269         @Override
    270         public void loadAttachmentProgress(int progress) {
    271             try {
    272                 mCallback.loadAttachmentStatus(mMessageId, mAttachmentId,
    273                         EmailServiceStatus.IN_PROGRESS, progress);
    274             } catch (final RemoteException e) {
    275                 // No danger if the client is no longer around
    276             }
    277         }
    278 
    279         @Override
    280         public void messageRetrieved(com.android.emailcommon.mail.Message message) {
    281         }
    282     }
    283 
    284     @Override
    285     public void updateFolderList(final long accountId) throws RemoteException {
    286         final Account account = Account.restoreAccountWithId(mContext, accountId);
    287         if (account == null) {
    288             LogUtils.e(LogUtils.TAG, "Account %d not found in updateFolderList", accountId);
    289             return;
    290         };
    291         long inboxId = -1;
    292         TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
    293         Cursor localFolderCursor = null;
    294         Store store = null;
    295         try {
    296             store = Store.getInstance(account, mContext);
    297 
    298             // Step 0: Make sure the default system mailboxes exist.
    299             for (final int type : Mailbox.REQUIRED_FOLDER_TYPES) {
    300                 if (Mailbox.findMailboxOfType(mContext, accountId, type) == Mailbox.NO_MAILBOX) {
    301                     final Mailbox mailbox = Mailbox.newSystemMailbox(mContext, accountId, type);
    302                     if (store.canSyncFolderType(type)) {
    303                         // If this folder is syncable, then we should set its UISyncStatus.
    304                         // Otherwise the UI could show the empty state until the sync
    305                         // actually occurs.
    306                         mailbox.mUiSyncStatus = Mailbox.SYNC_STATUS_INITIAL_SYNC_NEEDED;
    307                     }
    308                     mailbox.save(mContext);
    309                     if (type == Mailbox.TYPE_INBOX) {
    310                         inboxId = mailbox.mId;
    311                     }
    312                 }
    313             }
    314 
    315             // Step 1: Get remote mailboxes
    316             final Folder[] remoteFolders = store.updateFolders();
    317             final HashSet<String> remoteFolderNames = new HashSet<String>();
    318             for (final Folder remoteFolder : remoteFolders) {
    319                 remoteFolderNames.add(remoteFolder.getName());
    320             }
    321 
    322             // Step 2: Get local mailboxes
    323             localFolderCursor = mContext.getContentResolver().query(
    324                     Mailbox.CONTENT_URI,
    325                     MAILBOX_PROJECTION,
    326                     EmailContent.MailboxColumns.ACCOUNT_KEY + "=?",
    327                     new String[] { String.valueOf(account.mId) },
    328                     null);
    329 
    330             // Step 3: Remove any local mailbox not on the remote list
    331             while (localFolderCursor.moveToNext()) {
    332                 final String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID);
    333                 // Short circuit if we have a remote mailbox with the same name
    334                 if (remoteFolderNames.contains(mailboxPath)) {
    335                     continue;
    336                 }
    337 
    338                 final int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE);
    339                 final long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID);
    340                 switch (mailboxType) {
    341                     case Mailbox.TYPE_INBOX:
    342                     case Mailbox.TYPE_DRAFTS:
    343                     case Mailbox.TYPE_OUTBOX:
    344                     case Mailbox.TYPE_SENT:
    345                     case Mailbox.TYPE_TRASH:
    346                     case Mailbox.TYPE_SEARCH:
    347                         // Never, ever delete special mailboxes
    348                         break;
    349                     default:
    350                         // Drop all attachment files related to this mailbox
    351                         AttachmentUtilities.deleteAllMailboxAttachmentFiles(
    352                                 mContext, accountId, mailboxId);
    353                         // Delete the mailbox; database triggers take care of related
    354                         // Message, Body and Attachment records
    355                         Uri uri = ContentUris.withAppendedId(
    356                                 Mailbox.CONTENT_URI, mailboxId);
    357                         mContext.getContentResolver().delete(uri, null, null);
    358                         break;
    359                 }
    360             }
    361         } catch (MessagingException me) {
    362             LogUtils.i(Logging.LOG_TAG, me, "Error in updateFolderList");
    363             // We'll hope this is temporary
    364             // TODO: Figure out what type of messaging exception it was and return an appropriate
    365             // result. If we start doing this from sync, it's important to let the sync manager
    366             // know if the failure was due to IO error or authentication errors.
    367         } finally {
    368             if (localFolderCursor != null) {
    369                 localFolderCursor.close();
    370             }
    371             if (store != null) {
    372                 store.closeConnections();
    373             }
    374             // If we just created the inbox, sync it
    375             if (inboxId != -1) {
    376                 requestSync(inboxId, true, 0);
    377             }
    378         }
    379     }
    380 
    381     @Override
    382     public void setLogging(final int flags) throws RemoteException {
    383         // Not required
    384     }
    385 
    386     @Override
    387     public Bundle autoDiscover(final String userName, final String password)
    388             throws RemoteException {
    389         // Not required
    390        return null;
    391     }
    392 
    393     @Override
    394     public void sendMeetingResponse(final long messageId, final int response)
    395             throws RemoteException {
    396         // Not required
    397     }
    398 
    399     @Override
    400     public void deleteExternalAccountPIMData(final String emailAddress) throws RemoteException {
    401         // No need to do anything here, for IMAP and POP accounts none of our data is external.
    402     }
    403 
    404     @Override
    405     public int searchMessages(final long accountId, final SearchParams params,
    406                               final long destMailboxId)
    407             throws RemoteException {
    408         // Not required
    409         return EmailServiceStatus.SUCCESS;
    410     }
    411 
    412     @Override
    413     public void pushModify(final long accountId) throws RemoteException {
    414         LogUtils.e(Logging.LOG_TAG, "pushModify invalid for account type for %d", accountId);
    415     }
    416 
    417     @Override
    418     public int sync(final long accountId, final Bundle syncExtras) {
    419         return EmailServiceStatus.SUCCESS;
    420 
    421     }
    422 
    423     @Override
    424     public void sendMail(final long accountId) throws RemoteException {
    425         sendMailImpl(mContext, accountId);
    426     }
    427 
    428     public static void sendMailImpl(final Context context, final long accountId) {
    429         final Account account = Account.restoreAccountWithId(context, accountId);
    430         if (account == null) {
    431             LogUtils.e(LogUtils.TAG, "account %d not found in sendMailImpl", accountId);
    432             return;
    433         }
    434         TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(context, account));
    435         final NotificationController nc =
    436                 NotificationControllerCreatorHolder.getInstance(context);
    437         // 1.  Loop through all messages in the account's outbox
    438         final long outboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX);
    439         if (outboxId == Mailbox.NO_MAILBOX) {
    440             return;
    441         }
    442         final ContentResolver resolver = context.getContentResolver();
    443         final Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
    444                 EmailContent.Message.ID_COLUMN_PROJECTION,
    445                 MessageColumns.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId)},
    446                 null);
    447         try {
    448             // 2.  exit early
    449             if (c.getCount() <= 0) {
    450                 return;
    451             }
    452             final Sender sender = Sender.getInstance(context, account);
    453             final Store remoteStore = Store.getInstance(account, context);
    454             final ContentValues moveToSentValues;
    455             if (remoteStore.requireCopyMessageToSentFolder()) {
    456                 Mailbox sentFolder =
    457                     Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SENT);
    458                 moveToSentValues = new ContentValues();
    459                 moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId);
    460             } else {
    461                 moveToSentValues = null;
    462             }
    463 
    464             // 3.  loop through the available messages and send them
    465             while (c.moveToNext()) {
    466                 final long messageId;
    467                 if (moveToSentValues != null) {
    468                     moveToSentValues.remove(EmailContent.MessageColumns.FLAGS);
    469                 }
    470                 try {
    471                     messageId = c.getLong(0);
    472                     // Don't send messages with unloaded attachments
    473                     if (Utility.hasUnloadedAttachments(context, messageId)) {
    474                         if (DebugUtils.DEBUG) {
    475                             LogUtils.d(Logging.LOG_TAG, "Can't send #" + messageId +
    476                                     "; unloaded attachments");
    477                         }
    478                         continue;
    479                     }
    480                     sender.sendMessage(messageId);
    481                 } catch (MessagingException me) {
    482                     // report error for this message, but keep trying others
    483                     if (me instanceof AuthenticationFailedException && nc != null) {
    484                         nc.showLoginFailedNotificationSynchronous(account.mId,
    485                                 false /* incoming */);
    486                     }
    487                     continue;
    488                 }
    489                 // 4. move to sent, or delete
    490                 final Uri syncedUri =
    491                     ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
    492                 // Delete all cached files
    493                 AttachmentUtilities.deleteAllCachedAttachmentFiles(context, account.mId, messageId);
    494                 if (moveToSentValues != null) {
    495                     // If this is a forwarded message and it has attachments, delete them, as they
    496                     // duplicate information found elsewhere (on the server).  This saves storage.
    497                     final EmailContent.Message msg =
    498                         EmailContent.Message.restoreMessageWithId(context, messageId);
    499                     if ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0) {
    500                         AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
    501                                 messageId);
    502                     }
    503                     final int flags = msg.mFlags & ~(EmailContent.Message.FLAG_TYPE_REPLY |
    504                             EmailContent.Message.FLAG_TYPE_FORWARD |
    505                             EmailContent.Message.FLAG_TYPE_REPLY_ALL |
    506                             EmailContent.Message.FLAG_TYPE_ORIGINAL);
    507 
    508                     moveToSentValues.put(EmailContent.MessageColumns.FLAGS, flags);
    509                     resolver.update(syncedUri, moveToSentValues, null, null);
    510                 } else {
    511                     AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
    512                             messageId);
    513                     final Uri uri =
    514                         ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
    515                     resolver.delete(uri, null, null);
    516                     resolver.delete(syncedUri, null, null);
    517                 }
    518             }
    519             if (nc != null) {
    520                 nc.cancelLoginFailedNotification(account.mId);
    521             }
    522         } catch (MessagingException me) {
    523             if (me instanceof AuthenticationFailedException && nc != null) {
    524                 nc.showLoginFailedNotificationSynchronous(account.mId, false /* incoming */);
    525             }
    526         } finally {
    527             c.close();
    528         }
    529     }
    530 
    531     public int getApiVersion() {
    532         return EmailServiceVersion.CURRENT;
    533     }
    534 }
    535