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