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