Pop3Store.java revision a8b683cf3f2efe726220c0235368cf6ea899e3ba
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.mail.store;
18
19import android.content.Context;
20import android.os.Bundle;
21import android.util.Log;
22
23import com.android.email.R;
24import com.android.email.mail.Store;
25import com.android.email.mail.transport.MailTransport;
26import com.android.email2.ui.MailActivityEmail;
27import com.android.emailcommon.Logging;
28import com.android.emailcommon.internet.MimeMessage;
29import com.android.emailcommon.mail.AuthenticationFailedException;
30import com.android.emailcommon.mail.FetchProfile;
31import com.android.emailcommon.mail.Flag;
32import com.android.emailcommon.mail.Folder;
33import com.android.emailcommon.mail.Transport;
34import com.android.emailcommon.mail.Folder.OpenMode;
35import com.android.emailcommon.mail.Message;
36import com.android.emailcommon.mail.MessagingException;
37import com.android.emailcommon.provider.Account;
38import com.android.emailcommon.provider.HostAuth;
39import com.android.emailcommon.provider.Mailbox;
40import com.android.emailcommon.service.EmailServiceProxy;
41import com.android.emailcommon.service.SearchParams;
42import com.android.emailcommon.utility.LoggingInputStream;
43import com.android.emailcommon.utility.Utility;
44import com.google.common.annotations.VisibleForTesting;
45
46import java.io.IOException;
47import java.io.InputStream;
48import java.util.ArrayList;
49import java.util.HashMap;
50import java.util.HashSet;
51
52public class Pop3Store extends Store {
53    // All flags defining debug or development code settings must be FALSE
54    // when code is checked in or released.
55    private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false;
56    private static boolean DEBUG_LOG_RAW_STREAM = false;
57
58    private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
59    /** The name of the only mailbox available to POP3 accounts */
60    private static final String POP3_MAILBOX_NAME = "INBOX";
61    private final HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
62
63//    /**
64//     * Detected latency, used for usage scaling.
65//     * Usage scaling occurs when it is necessary to get information about
66//     * messages that could result in large data loads. This value allows
67//     * the code that loads this data to decide between using large downloads
68//     * (high latency) or multiple round trips (low latency) to accomplish
69//     * the same thing.
70//     * Default is Integer.MAX_VALUE implying massive latency so that the large
71//     * download method is used by default until latency data is collected.
72//     */
73//    private int mLatencyMs = Integer.MAX_VALUE;
74//
75//    /**
76//     * Detected throughput, used for usage scaling.
77//     * Usage scaling occurs when it is necessary to get information about
78//     * messages that could result in large data loads. This value allows
79//     * the code that loads this data to decide between using large downloads
80//     * (high latency) or multiple round trips (low latency) to accomplish
81//     * the same thing.
82//     * Default is Integer.MAX_VALUE implying massive bandwidth so that the
83//     * large download method is used by default until latency data is
84//     * collected.
85//     */
86//    private int mThroughputKbS = Integer.MAX_VALUE;
87
88    /**
89     * Static named constructor.
90     */
91    public static Store newInstance(Account account, Context context) throws MessagingException {
92        return new Pop3Store(context, account);
93    }
94
95    /**
96     * Creates a new store for the given account.
97     */
98    private Pop3Store(Context context, Account account) throws MessagingException {
99        mContext = context;
100        mAccount = account;
101
102        HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
103        if (recvAuth == null || !HostAuth.LEGACY_SCHEME_POP3.equalsIgnoreCase(recvAuth.mProtocol)) {
104            throw new MessagingException("Unsupported protocol");
105        }
106        // defaults, which can be changed by security modifiers
107        int connectionSecurity = Transport.CONNECTION_SECURITY_NONE;
108        int defaultPort = 110;
109
110        // check for security flags and apply changes
111        if ((recvAuth.mFlags & HostAuth.FLAG_SSL) != 0) {
112            connectionSecurity = Transport.CONNECTION_SECURITY_SSL;
113            defaultPort = 995;
114        } else if ((recvAuth.mFlags & HostAuth.FLAG_TLS) != 0) {
115            connectionSecurity = Transport.CONNECTION_SECURITY_TLS;
116        }
117        boolean trustCertificates = ((recvAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0);
118
119        int port = defaultPort;
120        if (recvAuth.mPort != HostAuth.PORT_UNKNOWN) {
121            port = recvAuth.mPort;
122        }
123        mTransport = new MailTransport("POP3");
124        mTransport.setHost(recvAuth.mAddress);
125        mTransport.setPort(port);
126        mTransport.setSecurity(connectionSecurity, trustCertificates);
127
128        String[] userInfoParts = recvAuth.getLogin();
129        if (userInfoParts != null) {
130            mUsername = userInfoParts[0];
131            mPassword = userInfoParts[1];
132        }
133    }
134
135    /**
136     * For testing only.  Injects a different transport.  The transport should already be set
137     * up and ready to use.  Do not use for real code.
138     * @param testTransport The Transport to inject and use for all future communication.
139     */
140    /* package */ void setTransport(Transport testTransport) {
141        mTransport = testTransport;
142    }
143
144    @Override
145    public Folder getFolder(String name) {
146        Folder folder = mFolders.get(name);
147        if (folder == null) {
148            folder = new Pop3Folder(name);
149            mFolders.put(folder.getName(), folder);
150        }
151        return folder;
152    }
153
154    private final int[] DEFAULT_FOLDERS = {
155            Mailbox.TYPE_DRAFTS,
156            Mailbox.TYPE_OUTBOX,
157            Mailbox.TYPE_SENT,
158            Mailbox.TYPE_TRASH
159    };
160
161    @Override
162    public Folder[] updateFolders() {
163        String inboxName = mContext.getString(R.string.mailbox_name_display_inbox);
164        Mailbox mailbox = Mailbox.getMailboxForPath(mContext, mAccount.mId, inboxName);
165        updateMailbox(mailbox, mAccount.mId, inboxName, '\0', true, Mailbox.TYPE_INBOX);
166        // Force the parent key to be "no mailbox" for the mail POP3 mailbox
167        mailbox.mParentKey = Mailbox.NO_MAILBOX;
168        if (mailbox.isSaved()) {
169            mailbox.update(mContext, mailbox.toContentValues());
170        } else {
171            mailbox.save(mContext);
172        }
173
174        // Build default mailboxes as well, in case they're not already made.
175        for (int type : DEFAULT_FOLDERS) {
176            if (Mailbox.findMailboxOfType(mContext, mAccount.mId, type) == Mailbox.NO_MAILBOX) {
177                String name = getMailboxServerName(mContext, type);
178                Mailbox.newSystemMailbox(mAccount.mId, type, name).save(mContext);
179            }
180        }
181
182        return new Folder[] { getFolder(inboxName) };
183    }
184
185
186    /**
187     * Returns the server-side name for a specific mailbox.
188     *
189     * @return the resource string corresponding to the mailbox type, empty if not found.
190     */
191    public String getMailboxServerName(Context context, int mailboxType) {
192        int resId = -1;
193        switch (mailboxType) {
194            case Mailbox.TYPE_INBOX:
195                resId = R.string.mailbox_name_server_inbox;
196                break;
197            case Mailbox.TYPE_OUTBOX:
198                resId = R.string.mailbox_name_server_outbox;
199                break;
200            case Mailbox.TYPE_DRAFTS:
201                resId = R.string.mailbox_name_server_drafts;
202                break;
203            case Mailbox.TYPE_TRASH:
204                resId = R.string.mailbox_name_server_trash;
205                break;
206            case Mailbox.TYPE_SENT:
207                resId = R.string.mailbox_name_server_sent;
208                break;
209            case Mailbox.TYPE_JUNK:
210                resId = R.string.mailbox_name_server_junk;
211                break;
212        }
213        return resId != -1 ? context.getString(resId) : "";
214    }
215
216    /**
217     * Used by account setup to test if an account's settings are appropriate.  The definition
218     * of "checked" here is simply, can you log into the account and does it meet some minimum set
219     * of feature requirements?
220     *
221     * @throws MessagingException if there was some problem with the account
222     */
223    @Override
224    public Bundle checkSettings() throws MessagingException {
225        Pop3Folder folder = new Pop3Folder(POP3_MAILBOX_NAME);
226        Bundle bundle = null;
227        // Close any open or half-open connections - checkSettings should always be "fresh"
228        if (mTransport.isOpen()) {
229            folder.close(false);
230        }
231        try {
232            folder.open(OpenMode.READ_WRITE);
233            bundle = folder.checkSettings();
234        } finally {
235            folder.close(false);    // false == don't expunge anything
236        }
237        return bundle;
238    }
239
240    class Pop3Folder extends Folder {
241        private final HashMap<String, Pop3Message> mUidToMsgMap
242                = new HashMap<String, Pop3Message>();
243        private final HashMap<Integer, Pop3Message> mMsgNumToMsgMap
244                = new HashMap<Integer, Pop3Message>();
245        private final HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>();
246        private final String mName;
247        private int mMessageCount;
248        private Pop3Capabilities mCapabilities;
249
250        public Pop3Folder(String name) {
251            if (name.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
252                mName = POP3_MAILBOX_NAME;
253            } else {
254                mName = name;
255            }
256        }
257
258        /**
259         * Used by account setup to test if an account's settings are appropriate.  Here, we run
260         * an additional test to see if UIDL is supported on the server. If it's not we
261         * can't service this account.
262         *
263         * @return Bundle containing validation data (code and, if appropriate, error message)
264         * @throws MessagingException if the account is not going to be useable
265         */
266        public Bundle checkSettings() throws MessagingException {
267            Bundle bundle = new Bundle();
268            int result = MessagingException.NO_ERROR;
269            if (!mCapabilities.uidl) {
270                try {
271                    UidlParser parser = new UidlParser();
272                    executeSimpleCommand("UIDL");
273                    // drain the entire output, so additional communications don't get confused.
274                    String response;
275                    while ((response = mTransport.readLine()) != null) {
276                        parser.parseMultiLine(response);
277                        if (parser.mEndOfMessage) {
278                            break;
279                        }
280                    }
281                } catch (IOException ioe) {
282                    mTransport.close();
283                    result = MessagingException.IOERROR;
284                    bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE,
285                            ioe.getMessage());
286                }
287            }
288            bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
289            return bundle;
290        }
291
292        @Override
293        public synchronized void open(OpenMode mode) throws MessagingException {
294            if (mTransport.isOpen()) {
295                return;
296            }
297
298            if (!mName.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
299                throw new MessagingException("Folder does not exist");
300            }
301
302            try {
303                mTransport.open();
304
305                // Eat the banner
306                executeSimpleCommand(null);
307
308                mCapabilities = getCapabilities();
309
310                if (mTransport.canTryTlsSecurity()) {
311                    if (mCapabilities.stls) {
312                        executeSimpleCommand("STLS");
313                        mTransport.reopenTls();
314                    } else {
315                        if (MailActivityEmail.DEBUG) {
316                            Log.d(Logging.LOG_TAG, "TLS not supported but required");
317                        }
318                        throw new MessagingException(MessagingException.TLS_REQUIRED);
319                    }
320                }
321
322                try {
323                    executeSensitiveCommand("USER " + mUsername, "USER /redacted/");
324                    executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/");
325                } catch (MessagingException me) {
326                    if (MailActivityEmail.DEBUG) {
327                        Log.d(Logging.LOG_TAG, me.toString());
328                    }
329                    throw new AuthenticationFailedException(null, me);
330                }
331            } catch (IOException ioe) {
332                mTransport.close();
333                if (MailActivityEmail.DEBUG) {
334                    Log.d(Logging.LOG_TAG, ioe.toString());
335                }
336                throw new MessagingException(MessagingException.IOERROR, ioe.toString());
337            }
338
339            Exception statException = null;
340            try {
341                String response = executeSimpleCommand("STAT");
342                String[] parts = response.split(" ");
343                if (parts.length < 2) {
344                    statException = new IOException();
345                } else {
346                    mMessageCount = Integer.parseInt(parts[1]);
347                }
348            } catch (IOException ioe) {
349                statException = ioe;
350            } catch (NumberFormatException nfe) {
351                statException = nfe;
352            }
353            if (statException != null) {
354                mTransport.close();
355                if (MailActivityEmail.DEBUG) {
356                    Log.d(Logging.LOG_TAG, statException.toString());
357                }
358                throw new MessagingException("POP3 STAT", statException);
359            }
360            mUidToMsgMap.clear();
361            mMsgNumToMsgMap.clear();
362            mUidToMsgNumMap.clear();
363        }
364
365        @Override
366        public OpenMode getMode() {
367            return OpenMode.READ_WRITE;
368        }
369
370        /**
371         * Close the folder (and the transport below it).
372         *
373         * MUST NOT return any exceptions.
374         *
375         * @param expunge If true all deleted messages will be expunged (TODO - not implemented)
376         */
377        @Override
378        public void close(boolean expunge) {
379            try {
380                executeSimpleCommand("QUIT");
381            }
382            catch (Exception e) {
383                // ignore any problems here - just continue closing
384            }
385            mTransport.close();
386        }
387
388        @Override
389        public String getName() {
390            return mName;
391        }
392
393        // POP3 does not folder creation
394        @Override
395        public boolean canCreate(FolderType type) {
396            return false;
397        }
398
399        @Override
400        public boolean create(FolderType type) {
401            return false;
402        }
403
404        @Override
405        public boolean exists() {
406            return mName.equalsIgnoreCase(POP3_MAILBOX_NAME);
407        }
408
409        @Override
410        public int getMessageCount() {
411            return mMessageCount;
412        }
413
414        @Override
415        public int getUnreadMessageCount() {
416            return -1;
417        }
418
419        @Override
420        public Message getMessage(String uid) throws MessagingException {
421            if (mUidToMsgNumMap.size() == 0) {
422                try {
423                    indexMsgNums(1, mMessageCount);
424                } catch (IOException ioe) {
425                    mTransport.close();
426                    if (MailActivityEmail.DEBUG) {
427                        Log.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe);
428                    }
429                    throw new MessagingException("getMessages", ioe);
430                }
431            }
432            Pop3Message message = mUidToMsgMap.get(uid);
433            return message;
434        }
435
436        @Override
437        public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
438                throws MessagingException {
439            if (start < 1 || end < 1 || end < start) {
440                throw new MessagingException(String.format("Invalid message set %d %d",
441                        start, end));
442            }
443            try {
444                indexMsgNums(start, end);
445            } catch (IOException ioe) {
446                mTransport.close();
447                if (MailActivityEmail.DEBUG) {
448                    Log.d(Logging.LOG_TAG, ioe.toString());
449                }
450                throw new MessagingException("getMessages", ioe);
451            }
452            ArrayList<Message> messages = new ArrayList<Message>();
453            for (int msgNum = start; msgNum <= end; msgNum++) {
454                Pop3Message message = mMsgNumToMsgMap.get(msgNum);
455                messages.add(message);
456                if (listener != null) {
457                    listener.messageRetrieved(message);
458                }
459            }
460            return messages.toArray(new Message[messages.size()]);
461        }
462
463        /**
464         * Ensures that the given message set (from start to end inclusive)
465         * has been queried so that uids are available in the local cache.
466         * @param start
467         * @param end
468         * @throws MessagingException
469         * @throws IOException
470         */
471        private void indexMsgNums(int start, int end)
472                throws MessagingException, IOException {
473            int unindexedMessageCount = 0;
474            for (int msgNum = start; msgNum <= end; msgNum++) {
475                if (mMsgNumToMsgMap.get(msgNum) == null) {
476                    unindexedMessageCount++;
477                }
478            }
479            if (unindexedMessageCount == 0) {
480                return;
481            }
482            UidlParser parser = new UidlParser();
483            if (DEBUG_FORCE_SINGLE_LINE_UIDL ||
484                    (unindexedMessageCount < 50 && mMessageCount > 5000)) {
485                /*
486                 * In extreme cases we'll do a UIDL command per message instead of a bulk
487                 * download.
488                 */
489                for (int msgNum = start; msgNum <= end; msgNum++) {
490                    Pop3Message message = mMsgNumToMsgMap.get(msgNum);
491                    if (message == null) {
492                        String response = executeSimpleCommand("UIDL " + msgNum);
493                        if (!parser.parseSingleLine(response)) {
494                            throw new IOException();
495                        }
496                        message = new Pop3Message(parser.mUniqueId, this);
497                        indexMessage(msgNum, message);
498                    }
499                }
500            } else {
501                String response = executeSimpleCommand("UIDL");
502                while ((response = mTransport.readLine()) != null) {
503                    if (!parser.parseMultiLine(response)) {
504                        throw new IOException();
505                    }
506                    if (parser.mEndOfMessage) {
507                        break;
508                    }
509                    int msgNum = parser.mMessageNumber;
510                    if (msgNum >= start && msgNum <= end) {
511                        Pop3Message message = mMsgNumToMsgMap.get(msgNum);
512                        if (message == null) {
513                            message = new Pop3Message(parser.mUniqueId, this);
514                            indexMessage(msgNum, message);
515                        }
516                    }
517                }
518            }
519        }
520
521        private void indexUids(ArrayList<String> uids)
522                throws MessagingException, IOException {
523            HashSet<String> unindexedUids = new HashSet<String>();
524            for (String uid : uids) {
525                if (mUidToMsgMap.get(uid) == null) {
526                    unindexedUids.add(uid);
527                }
528            }
529            if (unindexedUids.size() == 0) {
530                return;
531            }
532            /*
533             * If we are missing uids in the cache the only sure way to
534             * get them is to do a full UIDL list. A possible optimization
535             * would be trying UIDL for the latest X messages and praying.
536             */
537            UidlParser parser = new UidlParser();
538            String response = executeSimpleCommand("UIDL");
539            while ((response = mTransport.readLine()) != null) {
540                parser.parseMultiLine(response);
541                if (parser.mEndOfMessage) {
542                    break;
543                }
544                if (unindexedUids.contains(parser.mUniqueId)) {
545                    Pop3Message message = mUidToMsgMap.get(parser.mUniqueId);
546                    if (message == null) {
547                        message = new Pop3Message(parser.mUniqueId, this);
548                    }
549                    indexMessage(parser.mMessageNumber, message);
550                }
551            }
552        }
553
554        /**
555         * Simple parser class for UIDL messages.
556         *
557         * <p>NOTE:  In variance with RFC 1939, we allow multiple whitespace between the
558         * message-number and unique-id fields.  This provides greater compatibility with some
559         * non-compliant POP3 servers, e.g. mail.comcast.net.
560         */
561        /* package */ class UidlParser {
562
563            /**
564             * Caller can read back message-number from this field
565             */
566            public int mMessageNumber;
567            /**
568             * Caller can read back unique-id from this field
569             */
570            public String mUniqueId;
571            /**
572             * True if the response was "end-of-message"
573             */
574            public boolean mEndOfMessage;
575            /**
576             * True if an error was reported
577             */
578            public boolean mErr;
579
580            /**
581             * Construct & Initialize
582             */
583            public UidlParser() {
584                mErr = true;
585            }
586
587            /**
588             * Parse a single-line response.  This is returned from a command of the form
589             * "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or
590             * "-ERR diagnostic text"
591             *
592             * @param response The string returned from the server
593             * @return true if the string parsed as expected (e.g. no syntax problems)
594             */
595            public boolean parseSingleLine(String response) {
596                mErr = false;
597                if (response == null || response.length() == 0) {
598                    return false;
599                }
600                char first = response.charAt(0);
601                if (first == '+') {
602                    String[] uidParts = response.split(" +");
603                    if (uidParts.length >= 3) {
604                        try {
605                            mMessageNumber = Integer.parseInt(uidParts[1]);
606                        } catch (NumberFormatException nfe) {
607                            return false;
608                        }
609                        mUniqueId = uidParts[2];
610                        mEndOfMessage = true;
611                        return true;
612                    }
613                } else if (first == '-') {
614                    mErr = true;
615                    return true;
616                }
617                return false;
618            }
619
620            /**
621             * Parse a multi-line response.  This is returned from a command of the form
622             * "UIDL" and will be formatted as: "." or "msg-num unique-id".
623             *
624             * @param response The string returned from the server
625             * @return true if the string parsed as expected (e.g. no syntax problems)
626             */
627            public boolean parseMultiLine(String response) {
628                mErr = false;
629                if (response == null || response.length() == 0) {
630                    return false;
631                }
632                char first = response.charAt(0);
633                if (first == '.') {
634                    mEndOfMessage = true;
635                    return true;
636                } else {
637                    String[] uidParts = response.split(" +");
638                    if (uidParts.length >= 2) {
639                        try {
640                            mMessageNumber = Integer.parseInt(uidParts[0]);
641                        } catch (NumberFormatException nfe) {
642                            return false;
643                        }
644                        mUniqueId = uidParts[1];
645                        mEndOfMessage = false;
646                        return true;
647                    }
648                }
649                return false;
650            }
651        }
652
653        private void indexMessage(int msgNum, Pop3Message message) {
654            mMsgNumToMsgMap.put(msgNum, message);
655            mUidToMsgMap.put(message.getUid(), message);
656            mUidToMsgNumMap.put(message.getUid(), msgNum);
657        }
658
659        @Override
660        public Message[] getMessages(String[] uids, MessageRetrievalListener listener) {
661            throw new UnsupportedOperationException(
662                    "Pop3Folder.getMessage(MessageRetrievalListener)");
663        }
664
665        /**
666         * Fetch the items contained in the FetchProfile into the given set of
667         * Messages in as efficient a manner as possible.
668         * @param messages
669         * @param fp
670         * @throws MessagingException
671         */
672        @Override
673        public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
674                throws MessagingException {
675            if (messages == null || messages.length == 0) {
676                return;
677            }
678            ArrayList<String> uids = new ArrayList<String>();
679            for (Message message : messages) {
680                uids.add(message.getUid());
681            }
682            try {
683                indexUids(uids);
684                if (fp.contains(FetchProfile.Item.ENVELOPE)) {
685                    // Note: We never pass the listener for the ENVELOPE call, because we're going
686                    // to be calling the listener below in the per-message loop.
687                    fetchEnvelope(messages, null);
688                }
689            } catch (IOException ioe) {
690                mTransport.close();
691                if (MailActivityEmail.DEBUG) {
692                    Log.d(Logging.LOG_TAG, ioe.toString());
693                }
694                throw new MessagingException("fetch", ioe);
695            }
696            for (int i = 0, count = messages.length; i < count; i++) {
697                Message message = messages[i];
698                if (!(message instanceof Pop3Message)) {
699                    throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message");
700                }
701                Pop3Message pop3Message = (Pop3Message)message;
702                try {
703                    if (fp.contains(FetchProfile.Item.BODY)) {
704                        fetchBody(pop3Message, -1);
705                    }
706                    else if (fp.contains(FetchProfile.Item.BODY_SANE)) {
707                        /*
708                         * To convert the suggested download size we take the size
709                         * divided by the maximum line size (76).
710                         */
711                        fetchBody(pop3Message,
712                                FETCH_BODY_SANE_SUGGESTED_SIZE / 76);
713                    }
714                    else if (fp.contains(FetchProfile.Item.STRUCTURE)) {
715                        /*
716                         * If the user is requesting STRUCTURE we are required to set the body
717                         * to null since we do not support the function.
718                         */
719                        pop3Message.setBody(null);
720                    }
721                    if (listener != null) {
722                        listener.messageRetrieved(message);
723                    }
724                } catch (IOException ioe) {
725                    mTransport.close();
726                    if (MailActivityEmail.DEBUG) {
727                        Log.d(Logging.LOG_TAG, ioe.toString());
728                    }
729                    throw new MessagingException("Unable to fetch message", ioe);
730                }
731            }
732        }
733
734        private void fetchEnvelope(Message[] messages,
735                MessageRetrievalListener listener)  throws IOException, MessagingException {
736            int unsizedMessages = 0;
737            for (Message message : messages) {
738                if (message.getSize() == -1) {
739                    unsizedMessages++;
740                }
741            }
742            if (unsizedMessages == 0) {
743                return;
744            }
745            if (unsizedMessages < 50 && mMessageCount > 5000) {
746                /*
747                 * In extreme cases we'll do a command per message instead of a bulk request
748                 * to hopefully save some time and bandwidth.
749                 */
750                for (int i = 0, count = messages.length; i < count; i++) {
751                    Message message = messages[i];
752                    if (!(message instanceof Pop3Message)) {
753                        throw new MessagingException(
754                                "Pop3Store.fetch called with non-Pop3 Message");
755                    }
756                    Pop3Message pop3Message = (Pop3Message)message;
757                    String response = executeSimpleCommand(String.format("LIST %d",
758                            mUidToMsgNumMap.get(pop3Message.getUid())));
759                    try {
760                        String[] listParts = response.split(" ");
761                        int msgNum = Integer.parseInt(listParts[1]);
762                        int msgSize = Integer.parseInt(listParts[2]);
763                        pop3Message.setSize(msgSize);
764                    } catch (NumberFormatException nfe) {
765                        throw new IOException();
766                    }
767                    if (listener != null) {
768                        listener.messageRetrieved(pop3Message);
769                    }
770                }
771            } else {
772                HashSet<String> msgUidIndex = new HashSet<String>();
773                for (Message message : messages) {
774                    msgUidIndex.add(message.getUid());
775                }
776                String response = executeSimpleCommand("LIST");
777                while ((response = mTransport.readLine()) != null) {
778                    if (response.equals(".")) {
779                        break;
780                    }
781                    Pop3Message pop3Message = null;
782                    int msgSize = 0;
783                    try {
784                        String[] listParts = response.split(" ");
785                        int msgNum = Integer.parseInt(listParts[0]);
786                        msgSize = Integer.parseInt(listParts[1]);
787                        pop3Message = mMsgNumToMsgMap.get(msgNum);
788                    } catch (NumberFormatException nfe) {
789                        throw new IOException();
790                    }
791                    if (pop3Message != null && msgUidIndex.contains(pop3Message.getUid())) {
792                        pop3Message.setSize(msgSize);
793                        if (listener != null) {
794                            listener.messageRetrieved(pop3Message);
795                        }
796                    }
797                }
798            }
799        }
800
801        /**
802         * Fetches the body of the given message, limiting the stored data
803         * to the specified number of lines. If lines is -1 the entire message
804         * is fetched. This is implemented with RETR for lines = -1 or TOP
805         * for any other value. If the server does not support TOP it is
806         * emulated with RETR and extra lines are thrown away.
807         *
808         * Note:  Some servers (e.g. live.com) don't support CAPA, but turn out to
809         * support TOP after all.  For better performance on these servers, we'll always
810         * probe TOP, and fall back to RETR when it's truly unsupported.
811         *
812         * @param message
813         * @param lines
814         */
815        private void fetchBody(Pop3Message message, int lines)
816                throws IOException, MessagingException {
817            String response = null;
818            int messageId = mUidToMsgNumMap.get(message.getUid());
819            if (lines == -1) {
820                // Fetch entire message
821                response = executeSimpleCommand(String.format("RETR %d", messageId));
822            } else {
823                // Fetch partial message.  Try "TOP", and fall back to slower "RETR" if necessary
824                try {
825                    response = executeSimpleCommand(String.format("TOP %d %d", messageId,  lines));
826                } catch (MessagingException me) {
827                    response = executeSimpleCommand(String.format("RETR %d", messageId));
828                }
829            }
830            if (response != null)  {
831                try {
832                    InputStream in = mTransport.getInputStream();
833                    if (DEBUG_LOG_RAW_STREAM && MailActivityEmail.DEBUG) {
834                        in = new LoggingInputStream(in);
835                    }
836                    message.parse(new Pop3ResponseInputStream(in));
837                }
838                catch (MessagingException me) {
839                    /*
840                     * If we're only downloading headers it's possible
841                     * we'll get a broken MIME message which we're not
842                     * real worried about. If we've downloaded the body
843                     * and can't parse it we need to let the user know.
844                     */
845                    if (lines == -1) {
846                        throw me;
847                    }
848                }
849            }
850        }
851
852        @Override
853        public Flag[] getPermanentFlags() {
854            return PERMANENT_FLAGS;
855        }
856
857        @Override
858        public void appendMessages(Message[] messages) {
859        }
860
861        @Override
862        public void delete(boolean recurse) {
863        }
864
865        @Override
866        public Message[] expunge() {
867            return null;
868        }
869
870        @Override
871        public void setFlags(Message[] messages, Flag[] flags, boolean value)
872                throws MessagingException {
873            if (!value || !Utility.arrayContains(flags, Flag.DELETED)) {
874                /*
875                 * The only flagging we support is setting the Deleted flag.
876                 */
877                return;
878            }
879            try {
880                for (Message message : messages) {
881                    executeSimpleCommand(String.format("DELE %s",
882                            mUidToMsgNumMap.get(message.getUid())));
883                }
884            }
885            catch (IOException ioe) {
886                mTransport.close();
887                if (MailActivityEmail.DEBUG) {
888                    Log.d(Logging.LOG_TAG, ioe.toString());
889                }
890                throw new MessagingException("setFlags()", ioe);
891            }
892        }
893
894        @Override
895        public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) {
896            throw new UnsupportedOperationException("copyMessages is not supported in POP3");
897        }
898
899//        private boolean isRoundTripModeSuggested() {
900//            long roundTripMethodMs =
901//                (uncachedMessageCount * 2 * mLatencyMs);
902//            long bulkMethodMs =
903//                    (mMessageCount * 58) / (mThroughputKbS * 1024 / 8) * 1000;
904//        }
905
906        private Pop3Capabilities getCapabilities() throws IOException {
907            Pop3Capabilities capabilities = new Pop3Capabilities();
908            try {
909                String response = executeSimpleCommand("CAPA");
910                while ((response = mTransport.readLine()) != null) {
911                    if (response.equals(".")) {
912                        break;
913                    }
914                    if (response.equalsIgnoreCase("STLS")){
915                        capabilities.stls = true;
916                    }
917                    else if (response.equalsIgnoreCase("UIDL")) {
918                        capabilities.uidl = true;
919                    }
920                    else if (response.equalsIgnoreCase("PIPELINING")) {
921                        capabilities.pipelining = true;
922                    }
923                    else if (response.equalsIgnoreCase("USER")) {
924                        capabilities.user = true;
925                    }
926                    else if (response.equalsIgnoreCase("TOP")) {
927                        capabilities.top = true;
928                    }
929                }
930            }
931            catch (MessagingException me) {
932                /*
933                 * The server may not support the CAPA command, so we just eat this Exception
934                 * and allow the empty capabilities object to be returned.
935                 */
936            }
937            return capabilities;
938        }
939
940        /**
941         * Send a single command and wait for a single line response.  Reopens the connection,
942         * if it is closed.  Leaves the connection open.
943         *
944         * @param command The command string to send to the server.
945         * @return Returns the response string from the server.
946         */
947        private String executeSimpleCommand(String command) throws IOException, MessagingException {
948            return executeSensitiveCommand(command, null);
949        }
950
951        /**
952         * Send a single command and wait for a single line response.  Reopens the connection,
953         * if it is closed.  Leaves the connection open.
954         *
955         * @param command The command string to send to the server.
956         * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
957         * please pass a replacement string here (for logging).
958         * @return Returns the response string from the server.
959         */
960        private String executeSensitiveCommand(String command, String sensitiveReplacement)
961                throws IOException, MessagingException {
962            open(OpenMode.READ_WRITE);
963
964            if (command != null) {
965                mTransport.writeLine(command, sensitiveReplacement);
966            }
967
968            String response = mTransport.readLine();
969
970            if (response.length() > 1 && response.charAt(0) == '-') {
971                throw new MessagingException(response);
972            }
973
974            return response;
975        }
976
977        @Override
978        public boolean equals(Object o) {
979            if (o instanceof Pop3Folder) {
980                return ((Pop3Folder) o).mName.equals(mName);
981            }
982            return super.equals(o);
983        }
984
985        @Override
986        @VisibleForTesting
987        public boolean isOpen() {
988            return mTransport.isOpen();
989        }
990
991        @Override
992        public Message createMessage(String uid) {
993            return new Pop3Message(uid, this);
994        }
995
996        @Override
997        public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) {
998            return null;
999        }
1000    }
1001
1002    public static class Pop3Message extends MimeMessage {
1003        public Pop3Message(String uid, Pop3Folder folder) {
1004            mUid = uid;
1005            mFolder = folder;
1006            mSize = -1;
1007        }
1008
1009        public void setSize(int size) {
1010            mSize = size;
1011        }
1012
1013        @Override
1014        public void parse(InputStream in) throws IOException, MessagingException {
1015            super.parse(in);
1016        }
1017
1018        @Override
1019        public void setFlag(Flag flag, boolean set) throws MessagingException {
1020            super.setFlag(flag, set);
1021            mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
1022        }
1023    }
1024
1025    /**
1026     * POP3 Capabilities as defined in RFC 2449.  This is not a complete list of CAPA
1027     * responses - just those that we use in this client.
1028     */
1029    class Pop3Capabilities {
1030        /** The STLS (start TLS) command is supported */
1031        public boolean stls;
1032        /** the TOP command (retrieve a partial message) is supported */
1033        public boolean top;
1034        /** USER and PASS login/auth commands are supported */
1035        public boolean user;
1036        /** the optional UIDL command is supported (unused) */
1037        public boolean uidl;
1038        /** the server is capable of accepting multiple commands at a time (unused) */
1039        public boolean pipelining;
1040
1041        @Override
1042        public String toString() {
1043            return String.format("STLS %b, TOP %b, USER %b, UIDL %b, PIPELINING %b",
1044                    stls,
1045                    top,
1046                    user,
1047                    uidl,
1048                    pipelining);
1049        }
1050    }
1051
1052    // TODO figure out what is special about this and merge it into MailTransport
1053    class Pop3ResponseInputStream extends InputStream {
1054        private final InputStream mIn;
1055        private boolean mStartOfLine = true;
1056        private boolean mFinished;
1057
1058        public Pop3ResponseInputStream(InputStream in) {
1059            mIn = in;
1060        }
1061
1062        @Override
1063        public int read() throws IOException {
1064            if (mFinished) {
1065                return -1;
1066            }
1067            int d = mIn.read();
1068            if (mStartOfLine && d == '.') {
1069                d = mIn.read();
1070                if (d == '\r') {
1071                    mFinished = true;
1072                    mIn.read();
1073                    return -1;
1074                }
1075            }
1076
1077            mStartOfLine = (d == '\n');
1078
1079            return d;
1080        }
1081    }
1082}
1083