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