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