summaryrefslogtreecommitdiffstats
path: root/src/com/android/email/service/Pop3Service.java
blob: 211cf2d7bacb6d66fd502342f98c44493b0d2836 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.email.service;

import android.app.Service;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.TrafficStats;
import android.net.Uri;
import android.os.IBinder;
import android.os.RemoteException;

import com.android.email.NotificationController;
import com.android.email.mail.Store;
import com.android.email.mail.store.Pop3Store;
import com.android.email.mail.store.Pop3Store.Pop3Folder;
import com.android.email.mail.store.Pop3Store.Pop3Message;
import com.android.email.provider.Utilities;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.Folder.OpenMode;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.AttachmentState;
import com.android.mail.utils.LogUtils;

import org.apache.james.mime4j.EOLConvertingInputStream;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import android.util.Log;

public class Pop3Service extends Service {
    private static final String TAG = "Pop3Service";
    private static final int DEFAULT_SYNC_COUNT = 100;
    private static final String ACTION_CHECK_MAIL =
         "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
    private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
    private static final String EXTRA_MSGID = "com.android.email.intent.extra.MSGID";

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG,"Inside onStartCommand");
        final String action = intent.getAction();
        Log.d(TAG,"action is " + action);
        Context context = getApplicationContext();
        if (ACTION_CHECK_MAIL.equals(action)) {
            final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
            Log.d(TAG,"accountId is " + accountId);
            final long inboxId = Mailbox.findMailboxOfType(context, accountId,
                Mailbox.TYPE_INBOX);
            Log.d(TAG,"inboxId is " + inboxId);
            mBinder.init(context);
            mBinder.requestSync(inboxId,true,0);
        }
        return Service.START_STICKY;
    }

    /**
     * Create our EmailService implementation here.
     */
    private final EmailServiceStub mBinder = new EmailServiceStub() {
        @Override
        public void loadAttachment(final IEmailServiceCallback callback, final long accountId,
                final long attachmentId, final boolean background) throws RemoteException {
            Attachment att = Attachment.restoreAttachmentWithId(mContext, attachmentId);
            if (att == null || att.mUiState != AttachmentState.DOWNLOADING) return;
            long inboxId = Mailbox.findMailboxOfType(mContext, att.mAccountKey, Mailbox.TYPE_INBOX);
            if (inboxId == Mailbox.NO_MAILBOX) return;
            // We load attachments during a sync
            requestSync(inboxId, true, 0);
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        mBinder.init(this);
        return mBinder;
    }

    /**
     * Start foreground synchronization of the specified folder. This is called
     * by synchronizeMailbox or checkMail. TODO this should use ID's instead of
     * fully-restored objects
     *
     * @param account
     * @param folder
     * @param deltaMessageCount the requested change in number of messages to sync.
     * @return The status code for whether this operation succeeded.
     * @throws MessagingException
     */
    public static int synchronizeMailboxSynchronous(Context context, final Account account,
            final Mailbox folder, final int deltaMessageCount) throws MessagingException {
        TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
        NotificationController nc = NotificationController.getInstance(context);
        try {
            synchronizePop3Mailbox(context, account, folder, deltaMessageCount);
            // Clear authentication notification for this account
            nc.cancelLoginFailedNotification(account.mId);
        } catch (MessagingException e) {
            if (Logging.LOGD) {
                LogUtils.v(Logging.LOG_TAG, "synchronizeMailbox", e);
            }
            if (e instanceof AuthenticationFailedException) {
                // Generate authentication notification
                nc.showLoginFailedNotificationSynchronous(account.mId, true /* incoming */);
            }
            throw e;
        }
        // TODO: Rather than use exceptions as logic aobve, return the status and handle it
        // correctly in caller.
        return EmailServiceStatus.SUCCESS;
    }

    /**
     * Lightweight record for the first pass of message sync, where I'm just
     * seeing if the local message requires sync. Later (for messages that need
     * syncing) we'll do a full readout from the DB.
     */
    private static class LocalMessageInfo {
        private static final int COLUMN_ID = 0;
        private static final int COLUMN_FLAG_LOADED = 1;
        private static final int COLUMN_SERVER_ID = 2;
        private static final String[] PROJECTION = new String[] {
                EmailContent.RECORD_ID, MessageColumns.FLAG_LOADED, SyncColumns.SERVER_ID
        };

        final long mId;
        final int mFlagLoaded;
        final String mServerId;

        public LocalMessageInfo(Cursor c) {
            mId = c.getLong(COLUMN_ID);
            mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
            mServerId = c.getString(COLUMN_SERVER_ID);
            // Note: mailbox key and account key not needed - they are projected
            // for the SELECT
        }
    }

    /**
     * Load the structure and body of messages not yet synced
     *
     * @param account the account we're syncing
     * @param remoteFolder the (open) Folder we're working on
     * @param unsyncedMessages an array of Message's we've got headers for
     * @param toMailbox the destination mailbox we're syncing
     * @throws MessagingException
     */
    static void loadUnsyncedMessages(final Context context, final Account account,
            Pop3Folder remoteFolder, ArrayList<Pop3Message> unsyncedMessages,
            final Mailbox toMailbox) throws MessagingException {

        if (MailActivityEmail.DEBUG) {
            LogUtils.d(TAG, "Loading " + unsyncedMessages.size() + " unsynced messages");
        }

        try {
            int cnt = unsyncedMessages.size();
            // They are in most recent to least recent order, process them that way.
            for (int i = 0; i < cnt; i++) {
                final Pop3Message message = unsyncedMessages.get(i);
                remoteFolder.fetchBody(message, Pop3Store.FETCH_BODY_SANE_SUGGESTED_SIZE / 76,
                        null);
                int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
                if (!message.isComplete()) {
                    // TODO: when the message is not complete, this should mark the message as
                    // partial.  When that change is made, we need to make sure that:
                    // 1) Partial messages are shown in the conversation list
                    // 2) We are able to download the rest of the message/attachment when the
                    //    user requests it.
                     flag = EmailContent.Message.FLAG_LOADED_PARTIAL;
                }
                if (MailActivityEmail.DEBUG) {
                    LogUtils.d(TAG, "Message is " + (message.isComplete() ? "" : "NOT ")
                            + "complete");
                }
                // If message is incomplete, create a "fake" attachment
                Utilities.copyOneMessageToProvider(context, message, account, toMailbox, flag);
            }
        } catch (IOException e) {
            throw new MessagingException(MessagingException.IOERROR);
        }
    }

    private static class FetchCallback implements EOLConvertingInputStream.Callback {
        private final ContentResolver mResolver;
        private final Uri mAttachmentUri;
        private final ContentValues mContentValues = new ContentValues();

        FetchCallback(ContentResolver resolver, Uri attachmentUri) {
            mResolver = resolver;
            mAttachmentUri = attachmentUri;
        }

        @Override
        public void report(int bytesRead) {
            mContentValues.put(AttachmentColumns.UI_DOWNLOADED_SIZE, bytesRead);
            mResolver.update(mAttachmentUri, mContentValues, null, null);
        }
    }

    /**
     * Synchronizer
     *
     * @param account the account to sync
     * @param mailbox the mailbox to sync
     * @param deltaMessageCount the requested change to number of messages to sync
     * @throws MessagingException
     */
    private synchronized static void synchronizePop3Mailbox(final Context context, final Account account,
            final Mailbox mailbox, final int deltaMessageCount) throws MessagingException {
        // TODO Break this into smaller pieces
        ContentResolver resolver = context.getContentResolver();

        // We only sync Inbox
        if (mailbox.mType != Mailbox.TYPE_INBOX) {
            return;
        }

        // Get the message list from EmailProvider and create an index of the uids

        Cursor localUidCursor = null;
        HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();

        try {
            localUidCursor = resolver.query(
                    EmailContent.Message.CONTENT_URI,
                    LocalMessageInfo.PROJECTION,
                    MessageColumns.MAILBOX_KEY + "=?",
                    new String[] {
                            String.valueOf(mailbox.mId)
                    },
                    null);
            while (localUidCursor.moveToNext()) {
                LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
                localMessageMap.put(info.mServerId, info);
            }
        } finally {
            if (localUidCursor != null) {
                localUidCursor.close();
            }
        }

        // Open the remote folder and create the remote folder if necessary

        Pop3Store remoteStore = (Pop3Store)Store.getInstance(account, context);
        // The account might have been deleted
        if (remoteStore == null)
            return;
        Pop3Folder remoteFolder = (Pop3Folder)remoteStore.getFolder(mailbox.mServerId);

        // Open the remote folder. This pre-loads certain metadata like message
        // count.
        remoteFolder.open(OpenMode.READ_WRITE);

        String[] accountIdArgs = new String[] { Long.toString(account.mId) };
        long trashMailboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_TRASH);
        Cursor updates = resolver.query(
                EmailContent.Message.UPDATED_CONTENT_URI,
                EmailContent.Message.ID_COLUMN_PROJECTION,
                EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
                null);
        try {
            // loop through messages marked as deleted
            while (updates.moveToNext()) {
                long id = updates.getLong(Message.ID_COLUMNS_ID_COLUMN);
                EmailContent.Message currentMsg =
                        EmailContent.Message.restoreMessageWithId(context, id);
                if (currentMsg.mMailboxKey == trashMailboxId) {
                    // Delete this on the server
                    Pop3Message popMessage =
                            (Pop3Message)remoteFolder.getMessage(currentMsg.mServerId);
                    if (popMessage != null) {
                        remoteFolder.deleteMessage(popMessage);
                    }
                }
                // Finally, delete the update
                Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, id);
                context.getContentResolver().delete(uri, null, null);
            }
        } finally {
            updates.close();
        }

        // Get the remote message count.
        final int remoteMessageCount = remoteFolder.getMessageCount();

        // Save the folder message count.
        mailbox.updateMessageCount(context, remoteMessageCount);

        // Create a list of messages to download
        Pop3Message[] remoteMessages = new Pop3Message[0];
        final ArrayList<Pop3Message> unsyncedMessages = new ArrayList<Pop3Message>();
        HashMap<String, Pop3Message> remoteUidMap = new HashMap<String, Pop3Message>();

        if (remoteMessageCount > 0) {
            /*
             * Get all messageIds in the mailbox.
             * We don't necessarily need to sync all of them.
             */
            remoteMessages = remoteFolder.getMessages(remoteMessageCount, remoteMessageCount);
            LogUtils.d(Logging.LOG_TAG, "remoteMessageCount " + remoteMessageCount);

            /*
             * TODO: It would be nicer if the default sync window were time based rather than
             * count based, but POP3 does not support time based queries, and the UIDL command
             * does not report timestamps. To handle this, we would need to load a block of
             * Ids, sync those messages to get the timestamps, and then load more Ids until we
             * have filled out our window.
             */
            int count = 0;
            int countNeeded = DEFAULT_SYNC_COUNT;
            for (final Pop3Message message : remoteMessages) {
                final String uid = message.getUid();
                remoteUidMap.put(uid, message);
            }

            /*
             * Figure out which messages we need to sync. Start at the most recent ones, and keep
             * going until we hit one of four end conditions:
             * 1. We currently have zero local messages. In this case, we will sync the most recent
             * DEFAULT_SYNC_COUNT, then stop.
             * 2. We have some local messages, and after encountering them, we find some older
             * messages that do not yet exist locally. In this case, we will load whichever came
             * before the ones we already had locally, and also deltaMessageCount additional
             * older messages.
             * 3. We have some local messages, but after examining the most recent
             * DEFAULT_SYNC_COUNT remote messages, we still have not encountered any that exist
             * locally. In this case, we'll stop adding new messages to sync, leaving a gap between
             * the ones we've just loaded and the ones we already had.
             * 4. We examine all of the remote messages before running into any of our count
             * limitations.
             */
            for (final Pop3Message message : remoteMessages) {
                final String uid = message.getUid();
                final LocalMessageInfo localMessage = localMessageMap.get(uid);
                if (localMessage == null) {
                    count++;
                } else {
                    // We have found a message that already exists locally. We may or may not
                    // need to keep looking, depending on what deltaMessageCount is.
                    LogUtils.d(Logging.LOG_TAG, "found a local message, need " +
                            deltaMessageCount + " more remote messages");
                    countNeeded = deltaMessageCount;
                    count = 0;
                }

                // localMessage == null -> message has never been created (not even headers)
                // mFlagLoaded != FLAG_LOADED_COMPLETE -> message failed to sync completely
                if (localMessage == null ||
                        (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE &&
                                localMessage.mFlagLoaded != Message.FLAG_LOADED_PARTIAL)) {
                    LogUtils.d(Logging.LOG_TAG, "need to sync " + uid);
                    unsyncedMessages.add(message);
                } else {
                    LogUtils.d(Logging.LOG_TAG, "don't need to sync " + uid);
                }

                if (count >= countNeeded) {
                    LogUtils.d(Logging.LOG_TAG, "loaded " + count + " messages, stopping");
                    break;
                }
            }
        } else {
            if (MailActivityEmail.DEBUG) {
                LogUtils.d(TAG, "*** Message count is zero??");
            }
            remoteFolder.close(false);
            return;
        }

        // Get "attachments" to be loaded
        Cursor c = resolver.query(Attachment.CONTENT_URI, Attachment.CONTENT_PROJECTION,
                AttachmentColumns.ACCOUNT_KEY + "=? AND " +
                        AttachmentColumns.UI_STATE + "=" + AttachmentState.DOWNLOADING,
                new String[] {Long.toString(account.mId)}, null);
        try {
            final ContentValues values = new ContentValues();
            while (c.moveToNext()) {
                values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
                Attachment att = new Attachment();
                att.restore(c);
                Message msg = Message.restoreMessageWithId(context, att.mMessageKey);
                if (msg == null || (msg.mFlagLoaded == Message.FLAG_LOADED_COMPLETE)) {
                    values.put(AttachmentColumns.UI_DOWNLOADED_SIZE, att.mSize);
                    resolver.update(ContentUris.withAppendedId(Attachment.CONTENT_URI, att.mId),
                            values, null, null);
                    continue;
                } else {
                    String uid = msg.mServerId;
                    Pop3Message popMessage = remoteUidMap.get(uid);
                    if (popMessage != null) {
                        Uri attUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, att.mId);
                        try {
                            remoteFolder.fetchBody(popMessage, -1,
                                    new FetchCallback(resolver, attUri));
                        } catch (IOException e) {
                            throw new MessagingException(MessagingException.IOERROR);
                        }

                        // Say we've downloaded the attachment
                        values.put(AttachmentColumns.UI_STATE, AttachmentState.SAVED);
                        resolver.update(attUri, values, null, null);

                        int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
                        if (!popMessage.isComplete()) {
                            LogUtils.e(TAG, "How is this possible?");
                        }
                        Utilities.copyOneMessageToProvider(
                                context, popMessage, account, mailbox, flag);
                        // Get rid of the temporary attachment
                        resolver.delete(attUri, null, null);

                    } else {
                        // TODO: Should we mark this attachment as failed so we don't
                        // keep trying to download?
                        LogUtils.e(TAG, "Could not find message for attachment " + uid);
                    }
                }
            }
        } finally {
            c.close();
        }

        // Remove any messages that are in the local store but no longer on the remote store.
        HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
        localUidsToDelete.removeAll(remoteUidMap.keySet());
        for (String uidToDelete : localUidsToDelete) {
            LogUtils.d(Logging.LOG_TAG, "need to delete " + uidToDelete);
            LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);

            // Delete associated data (attachment files)
            // Attachment & Body records are auto-deleted when we delete the
            // Message record
            AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
                    infoToDelete.mId);

            // Delete the message itself
            Uri uriToDelete = ContentUris.withAppendedId(
                    EmailContent.Message.CONTENT_URI, infoToDelete.mId);
            resolver.delete(uriToDelete, null, null);

            // Delete extra rows (e.g. synced or deleted)
            Uri updateRowToDelete = ContentUris.withAppendedId(
                    EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
            resolver.delete(updateRowToDelete, null, null);
            Uri deleteRowToDelete = ContentUris.withAppendedId(
                    EmailContent.Message.DELETED_CONTENT_URI, infoToDelete.mId);
            resolver.delete(deleteRowToDelete, null, null);
        }

        LogUtils.d(TAG, "loadUnsynchedMessages " + unsyncedMessages.size());
        // Load messages we need to sync
        loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);

        // Clean up and report results
        remoteFolder.close(false);
    }
}