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