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