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;
27
28import com.android.email.DebugUtils;
29import com.android.email.NotificationController;
30import com.android.email.NotificationControllerCreatorHolder;
31import com.android.email.mail.Sender;
32import com.android.email.mail.Store;
33import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
34import com.android.emailcommon.Logging;
35import com.android.emailcommon.TrafficFlags;
36import com.android.emailcommon.internet.MimeBodyPart;
37import com.android.emailcommon.internet.MimeHeader;
38import com.android.emailcommon.internet.MimeMultipart;
39import com.android.emailcommon.mail.AuthenticationFailedException;
40import com.android.emailcommon.mail.FetchProfile;
41import com.android.emailcommon.mail.Folder;
42import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
43import com.android.emailcommon.mail.Folder.OpenMode;
44import com.android.emailcommon.mail.Message;
45import com.android.emailcommon.mail.MessagingException;
46import com.android.emailcommon.provider.Account;
47import com.android.emailcommon.provider.EmailContent;
48import com.android.emailcommon.provider.EmailContent.Attachment;
49import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
50import com.android.emailcommon.provider.EmailContent.Body;
51import com.android.emailcommon.provider.EmailContent.BodyColumns;
52import com.android.emailcommon.provider.EmailContent.MailboxColumns;
53import com.android.emailcommon.provider.EmailContent.MessageColumns;
54import com.android.emailcommon.provider.Mailbox;
55import com.android.emailcommon.service.EmailServiceStatus;
56import com.android.emailcommon.service.EmailServiceVersion;
57import com.android.emailcommon.service.HostAuthCompat;
58import com.android.emailcommon.service.IEmailService;
59import com.android.emailcommon.service.IEmailServiceCallback;
60import com.android.emailcommon.service.SearchParams;
61import com.android.emailcommon.utility.AttachmentUtilities;
62import com.android.emailcommon.utility.Utility;
63import com.android.mail.providers.UIProvider;
64import com.android.mail.utils.LogUtils;
65
66import 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 */
74public 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