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