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