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