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