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