1 /* Copyright (C) 2012 The Android Open Source Project
2  *
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 package com.android.email.service;
17 
18 import android.content.ContentResolver;
19 import android.content.ContentUris;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.database.Cursor;
23 import android.net.TrafficStats;
24 import android.net.Uri;
25 import android.os.Bundle;
26 import android.os.RemoteException;
27 
28 import com.android.email.DebugUtils;
29 import com.android.email.NotificationController;
30 import com.android.email.NotificationControllerCreatorHolder;
31 import com.android.email.mail.Sender;
32 import com.android.email.mail.Store;
33 import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
34 import com.android.emailcommon.Logging;
35 import com.android.emailcommon.TrafficFlags;
36 import com.android.emailcommon.internet.MimeBodyPart;
37 import com.android.emailcommon.internet.MimeHeader;
38 import com.android.emailcommon.internet.MimeMultipart;
39 import com.android.emailcommon.mail.AuthenticationFailedException;
40 import com.android.emailcommon.mail.FetchProfile;
41 import com.android.emailcommon.mail.Folder;
42 import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
43 import com.android.emailcommon.mail.Folder.OpenMode;
44 import com.android.emailcommon.mail.Message;
45 import com.android.emailcommon.mail.MessagingException;
46 import com.android.emailcommon.provider.Account;
47 import com.android.emailcommon.provider.EmailContent;
48 import com.android.emailcommon.provider.EmailContent.Attachment;
49 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
50 import com.android.emailcommon.provider.EmailContent.Body;
51 import com.android.emailcommon.provider.EmailContent.BodyColumns;
52 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
53 import com.android.emailcommon.provider.EmailContent.MessageColumns;
54 import com.android.emailcommon.provider.Mailbox;
55 import com.android.emailcommon.service.EmailServiceStatus;
56 import com.android.emailcommon.service.EmailServiceVersion;
57 import com.android.emailcommon.service.HostAuthCompat;
58 import com.android.emailcommon.service.IEmailService;
59 import com.android.emailcommon.service.IEmailServiceCallback;
60 import com.android.emailcommon.service.SearchParams;
61 import com.android.emailcommon.utility.AttachmentUtilities;
62 import com.android.emailcommon.utility.Utility;
63 import com.android.mail.providers.UIProvider;
64 import com.android.mail.utils.LogUtils;
65 
66 import java.util.HashSet;
67 
68 /**
69  * EmailServiceStub is an abstract class representing an EmailService
70  *
71  * This class provides legacy support for a few methods that are common to both
72  * IMAP and POP3, including startSync, loadMore, loadAttachment, and sendMail
73  */
74 public abstract class EmailServiceStub extends IEmailService.Stub implements IEmailService {
75 
76     private static final int MAILBOX_COLUMN_ID = 0;
77     private static final int MAILBOX_COLUMN_SERVER_ID = 1;
78     private static final int MAILBOX_COLUMN_TYPE = 2;
79 
80     /** Small projection for just the columns required for a sync. */
81     private static final String[] MAILBOX_PROJECTION = {
82         MailboxColumns._ID,
83         MailboxColumns.SERVER_ID,
84         MailboxColumns.TYPE,
85     };
86 
87     protected Context mContext;
88 
init(Context context)89     protected void init(Context context) {
90         mContext = context;
91     }
92 
93     @Override
validate(HostAuthCompat hostAuthCom)94     public Bundle validate(HostAuthCompat hostAuthCom) throws RemoteException {
95         // TODO Auto-generated method stub
96         return null;
97     }
98 
requestSync(long mailboxId, boolean userRequest, int deltaMessageCount)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
loadAttachment(final IEmailServiceCallback cb, final long accountId, final long attachmentId, final boolean background)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 
MessageRetrievalListenerBridge(final long messageId, final long attachmentId, final IEmailServiceCallback callback)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
loadAttachmentProgress(int progress)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
messageRetrieved(com.android.emailcommon.mail.Message message)280         public void messageRetrieved(com.android.emailcommon.mail.Message message) {
281         }
282     }
283 
284     @Override
updateFolderList(final long accountId)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
setLogging(final int flags)382     public void setLogging(final int flags) throws RemoteException {
383         // Not required
384     }
385 
386     @Override
autoDiscover(final String userName, final String password)387     public Bundle autoDiscover(final String userName, final String password)
388             throws RemoteException {
389         // Not required
390        return null;
391     }
392 
393     @Override
sendMeetingResponse(final long messageId, final int response)394     public void sendMeetingResponse(final long messageId, final int response)
395             throws RemoteException {
396         // Not required
397     }
398 
399     @Override
deleteExternalAccountPIMData(final String emailAddress)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
searchMessages(final long accountId, final SearchParams params, final long destMailboxId)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
pushModify(final long accountId)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
sync(final long accountId, final Bundle syncExtras)418     public int sync(final long accountId, final Bundle syncExtras) {
419         return EmailServiceStatus.SUCCESS;
420 
421     }
422 
423     @Override
sendMail(final long accountId)424     public void sendMail(final long accountId) throws RemoteException {
425         sendMailImpl(mContext, accountId);
426     }
427 
sendMailImpl(final Context context, final long accountId)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 
getApiVersion()531     public int getApiVersion() {
532         return EmailServiceVersion.CURRENT;
533     }
534 }
535