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