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