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