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