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