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;
21
22import com.android.email.DebugUtils;
23import com.android.email.mail.Store;
24import com.android.email.mail.transport.MailTransport;
25import com.android.emailcommon.Logging;
26import com.android.emailcommon.internet.MimeMessage;
27import com.android.emailcommon.mail.AuthenticationFailedException;
28import com.android.emailcommon.mail.FetchProfile;
29import com.android.emailcommon.mail.Flag;
30import com.android.emailcommon.mail.Folder;
31import com.android.emailcommon.mail.Folder.OpenMode;
32import com.android.emailcommon.mail.Message;
33import com.android.emailcommon.mail.MessagingException;
34import com.android.emailcommon.provider.Account;
35import com.android.emailcommon.provider.HostAuth;
36import com.android.emailcommon.provider.Mailbox;
37import com.android.emailcommon.service.EmailServiceProxy;
38import com.android.emailcommon.service.SearchParams;
39import com.android.emailcommon.utility.LoggingInputStream;
40import com.android.emailcommon.utility.Utility;
41import com.android.mail.utils.LogUtils;
42import com.google.common.annotations.VisibleForTesting;
43
44import org.apache.james.mime4j.EOLConvertingInputStream;
45
46import java.io.IOException;
47import java.io.InputStream;
48import java.util.ArrayList;
49import java.util.HashMap;
50import java.util.Locale;
51
52public class Pop3Store extends Store {
53    // All flags defining debug or development code settings must be FALSE
54    // when code is checked in or released.
55    private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false;
56    private static boolean DEBUG_LOG_RAW_STREAM = false;
57
58    private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
59    /** The name of the only mailbox available to POP3 accounts */
60    private static final String POP3_MAILBOX_NAME = "INBOX";
61    private final HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
62    private final Message[] mOneMessage = new Message[1];
63
64    /**
65     * Static named constructor.
66     */
67    public static Store newInstance(Account account, Context context) throws MessagingException {
68        return new Pop3Store(context, account);
69    }
70
71    /**
72     * Creates a new store for the given account.
73     */
74    private Pop3Store(Context context, Account account) throws MessagingException {
75        mContext = context;
76        mAccount = account;
77
78        HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
79        mTransport = new MailTransport(context, "POP3", recvAuth);
80        String[] userInfoParts = recvAuth.getLogin();
81        mUsername = userInfoParts[0];
82        mPassword = userInfoParts[1];
83    }
84
85    /**
86     * For testing only.  Injects a different transport.  The transport should already be set
87     * up and ready to use.  Do not use for real code.
88     * @param testTransport The Transport to inject and use for all future communication.
89     */
90    /* package */ void setTransport(MailTransport testTransport) {
91        mTransport = testTransport;
92    }
93
94    @Override
95    public Folder getFolder(String name) {
96        Folder folder = mFolders.get(name);
97        if (folder == null) {
98            folder = new Pop3Folder(name);
99            mFolders.put(folder.getName(), folder);
100        }
101        return folder;
102    }
103
104    @Override
105    public Folder[] updateFolders() {
106        Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
107        if (mailbox == null) {
108            mailbox = Mailbox.newSystemMailbox(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
109        }
110        if (mailbox.isSaved()) {
111            mailbox.update(mContext, mailbox.toContentValues());
112        } else {
113            mailbox.save(mContext);
114        }
115        return new Folder[] { getFolder(mailbox.mServerId) };
116    }
117
118    /**
119     * Used by account setup to test if an account's settings are appropriate.  The definition
120     * of "checked" here is simply, can you log into the account and does it meet some minimum set
121     * of feature requirements?
122     *
123     * @throws MessagingException if there was some problem with the account
124     */
125    @Override
126    public Bundle checkSettings() throws MessagingException {
127        Pop3Folder folder = new Pop3Folder(POP3_MAILBOX_NAME);
128        Bundle bundle = null;
129        // Close any open or half-open connections - checkSettings should always be "fresh"
130        if (mTransport.isOpen()) {
131            folder.close(false);
132        }
133        try {
134            folder.open(OpenMode.READ_WRITE);
135            bundle = folder.checkSettings();
136        } finally {
137            folder.close(false);    // false == don't expunge anything
138        }
139        return bundle;
140    }
141
142    public class Pop3Folder extends Folder {
143        private final HashMap<String, Pop3Message> mUidToMsgMap
144                = new HashMap<String, Pop3Message>();
145        private final HashMap<Integer, Pop3Message> mMsgNumToMsgMap
146                = new HashMap<Integer, Pop3Message>();
147        private final HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>();
148        private final String mName;
149        private int mMessageCount;
150        private Pop3Capabilities mCapabilities;
151
152        public Pop3Folder(String name) {
153            if (name.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
154                mName = POP3_MAILBOX_NAME;
155            } else {
156                mName = name;
157            }
158        }
159
160        /**
161         * Used by account setup to test if an account's settings are appropriate.  Here, we run
162         * an additional test to see if UIDL is supported on the server. If it's not we
163         * can't service this account.
164         *
165         * @return Bundle containing validation data (code and, if appropriate, error message)
166         * @throws MessagingException if the account is not going to be useable
167         */
168        public Bundle checkSettings() throws MessagingException {
169            Bundle bundle = new Bundle();
170            int result = MessagingException.NO_ERROR;
171            try {
172                UidlParser parser = new UidlParser();
173                executeSimpleCommand("UIDL");
174                // drain the entire output, so additional communications don't get confused.
175                String response;
176                while ((response = mTransport.readLine(false)) != null) {
177                    parser.parseMultiLine(response);
178                    if (parser.mEndOfMessage) {
179                        break;
180                    }
181                }
182            } catch (IOException ioe) {
183                mTransport.close();
184                result = MessagingException.IOERROR;
185                bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE,
186                        ioe.getMessage());
187            }
188            bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
189            return bundle;
190        }
191
192        @Override
193        public synchronized void open(OpenMode mode) throws MessagingException {
194            if (mTransport.isOpen()) {
195                return;
196            }
197
198            if (!mName.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
199                throw new MessagingException("Folder does not exist");
200            }
201
202            try {
203                mTransport.open();
204
205                // Eat the banner
206                executeSimpleCommand(null);
207
208                mCapabilities = getCapabilities();
209
210                if (mTransport.canTryTlsSecurity()) {
211                    if (mCapabilities.stls) {
212                        executeSimpleCommand("STLS");
213                        mTransport.reopenTls();
214                    } else {
215                        if (DebugUtils.DEBUG) {
216                            LogUtils.d(Logging.LOG_TAG, "TLS not supported but required");
217                        }
218                        throw new MessagingException(MessagingException.TLS_REQUIRED);
219                    }
220                }
221
222                try {
223                    executeSensitiveCommand("USER " + mUsername, "USER /redacted/");
224                    executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/");
225                } catch (MessagingException me) {
226                    if (DebugUtils.DEBUG) {
227                        LogUtils.d(Logging.LOG_TAG, me.toString());
228                    }
229                    throw new AuthenticationFailedException(null, me);
230                }
231            } catch (IOException ioe) {
232                mTransport.close();
233                if (DebugUtils.DEBUG) {
234                    LogUtils.d(Logging.LOG_TAG, ioe.toString());
235                }
236                throw new MessagingException(MessagingException.IOERROR, ioe.toString());
237            }
238
239            Exception statException = null;
240            try {
241                String response = executeSimpleCommand("STAT");
242                String[] parts = response.split(" ");
243                if (parts.length < 2) {
244                    statException = new IOException();
245                } else {
246                    mMessageCount = Integer.parseInt(parts[1]);
247                }
248            } catch (MessagingException me) {
249                statException = me;
250            } catch (IOException ioe) {
251                statException = ioe;
252            } catch (NumberFormatException nfe) {
253                statException = nfe;
254            }
255            if (statException != null) {
256                mTransport.close();
257                if (DebugUtils.DEBUG) {
258                    LogUtils.d(Logging.LOG_TAG, statException.toString());
259                }
260                throw new MessagingException("POP3 STAT", statException);
261            }
262            mUidToMsgMap.clear();
263            mMsgNumToMsgMap.clear();
264            mUidToMsgNumMap.clear();
265        }
266
267        @Override
268        public OpenMode getMode() {
269            return OpenMode.READ_WRITE;
270        }
271
272        /**
273         * Close the folder (and the transport below it).
274         *
275         * MUST NOT return any exceptions.
276         *
277         * @param expunge If true all deleted messages will be expunged (TODO - not implemented)
278         */
279        @Override
280        public void close(boolean expunge) {
281            try {
282                executeSimpleCommand("QUIT");
283            }
284            catch (Exception e) {
285                // ignore any problems here - just continue closing
286            }
287            mTransport.close();
288        }
289
290        @Override
291        public String getName() {
292            return mName;
293        }
294
295        // POP3 does not folder creation
296        @Override
297        public boolean canCreate(FolderType type) {
298            return false;
299        }
300
301        @Override
302        public boolean create(FolderType type) {
303            return false;
304        }
305
306        @Override
307        public boolean exists() {
308            return mName.equalsIgnoreCase(POP3_MAILBOX_NAME);
309        }
310
311        @Override
312        public int getMessageCount() {
313            return mMessageCount;
314        }
315
316        @Override
317        public int getUnreadMessageCount() {
318            return -1;
319        }
320
321        @Override
322        public Message getMessage(String uid) throws MessagingException {
323            if (mUidToMsgNumMap.size() == 0) {
324                try {
325                    indexMsgNums(1, mMessageCount);
326                } catch (IOException ioe) {
327                    mTransport.close();
328                    if (DebugUtils.DEBUG) {
329                        LogUtils.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe);
330                    }
331                    throw new MessagingException("getMessages", ioe);
332                }
333            }
334            Pop3Message message = mUidToMsgMap.get(uid);
335            return message;
336        }
337
338        @Override
339        public Pop3Message[] getMessages(int start, int end, MessageRetrievalListener listener)
340                throws MessagingException {
341            return null;
342        }
343
344        @Override
345        public Pop3Message[] getMessages(long startDate, long endDate,
346                MessageRetrievalListener listener) throws MessagingException {
347            return null;
348        }
349
350        public Pop3Message[] getMessages(int end, final int limit)
351                throws MessagingException {
352            try {
353                indexMsgNums(1, end);
354            } catch (IOException ioe) {
355                mTransport.close();
356                if (DebugUtils.DEBUG) {
357                    LogUtils.d(Logging.LOG_TAG, ioe.toString());
358                }
359                throw new MessagingException("getMessages", ioe);
360            }
361            ArrayList<Message> messages = new ArrayList<Message>();
362            for (int msgNum = end; msgNum > 0 && (messages.size() < limit); msgNum--) {
363                Pop3Message message = mMsgNumToMsgMap.get(msgNum);
364                if (message != null) {
365                    messages.add(message);
366                }
367            }
368            return messages.toArray(new Pop3Message[messages.size()]);
369        }
370
371        /**
372         * Ensures that the given message set (from start to end inclusive)
373         * has been queried so that uids are available in the local cache.
374         * @param start
375         * @param end
376         * @throws MessagingException
377         * @throws IOException
378         */
379        private void indexMsgNums(int start, int end)
380                throws MessagingException, IOException {
381            if (!mMsgNumToMsgMap.isEmpty()) {
382                return;
383            }
384            UidlParser parser = new UidlParser();
385            if (DEBUG_FORCE_SINGLE_LINE_UIDL || (mMessageCount > 5000)) {
386                /*
387                 * In extreme cases we'll do a UIDL command per message instead of a bulk
388                 * download.
389                 */
390                for (int msgNum = start; msgNum <= end; msgNum++) {
391                    Pop3Message message = mMsgNumToMsgMap.get(msgNum);
392                    if (message == null) {
393                        String response = executeSimpleCommand("UIDL " + msgNum);
394                        if (!parser.parseSingleLine(response)) {
395                            throw new IOException();
396                        }
397                        message = new Pop3Message(parser.mUniqueId, this);
398                        indexMessage(msgNum, message);
399                    }
400                }
401            } else {
402                String response = executeSimpleCommand("UIDL");
403                while ((response = mTransport.readLine(false)) != null) {
404                    if (!parser.parseMultiLine(response)) {
405                        throw new IOException();
406                    }
407                    if (parser.mEndOfMessage) {
408                        break;
409                    }
410                    int msgNum = parser.mMessageNumber;
411                    if (msgNum >= start && msgNum <= end) {
412                        Pop3Message message = mMsgNumToMsgMap.get(msgNum);
413                        if (message == null) {
414                            message = new Pop3Message(parser.mUniqueId, this);
415                            indexMessage(msgNum, message);
416                        }
417                    }
418                }
419            }
420        }
421
422        /**
423         * Simple parser class for UIDL messages.
424         *
425         * <p>NOTE:  In variance with RFC 1939, we allow multiple whitespace between the
426         * message-number and unique-id fields.  This provides greater compatibility with some
427         * non-compliant POP3 servers, e.g. mail.comcast.net.
428         */
429        /* package */ class UidlParser {
430
431            /**
432             * Caller can read back message-number from this field
433             */
434            public int mMessageNumber;
435            /**
436             * Caller can read back unique-id from this field
437             */
438            public String mUniqueId;
439            /**
440             * True if the response was "end-of-message"
441             */
442            public boolean mEndOfMessage;
443            /**
444             * True if an error was reported
445             */
446            public boolean mErr;
447
448            /**
449             * Construct & Initialize
450             */
451            public UidlParser() {
452                mErr = true;
453            }
454
455            /**
456             * Parse a single-line response.  This is returned from a command of the form
457             * "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or
458             * "-ERR diagnostic text"
459             *
460             * @param response The string returned from the server
461             * @return true if the string parsed as expected (e.g. no syntax problems)
462             */
463            public boolean parseSingleLine(String response) {
464                mErr = false;
465                if (response == null || response.length() == 0) {
466                    return false;
467                }
468                char first = response.charAt(0);
469                if (first == '+') {
470                    String[] uidParts = response.split(" +");
471                    if (uidParts.length >= 3) {
472                        try {
473                            mMessageNumber = Integer.parseInt(uidParts[1]);
474                        } catch (NumberFormatException nfe) {
475                            return false;
476                        }
477                        mUniqueId = uidParts[2];
478                        mEndOfMessage = true;
479                        return true;
480                    }
481                } else if (first == '-') {
482                    mErr = true;
483                    return true;
484                }
485                return false;
486            }
487
488            /**
489             * Parse a multi-line response.  This is returned from a command of the form
490             * "UIDL" and will be formatted as: "." or "msg-num unique-id".
491             *
492             * @param response The string returned from the server
493             * @return true if the string parsed as expected (e.g. no syntax problems)
494             */
495            public boolean parseMultiLine(String response) {
496                mErr = false;
497                if (response == null || response.length() == 0) {
498                    return false;
499                }
500                char first = response.charAt(0);
501                if (first == '.') {
502                    mEndOfMessage = true;
503                    return true;
504                } else {
505                    String[] uidParts = response.split(" +");
506                    if (uidParts.length >= 2) {
507                        try {
508                            mMessageNumber = Integer.parseInt(uidParts[0]);
509                        } catch (NumberFormatException nfe) {
510                            return false;
511                        }
512                        mUniqueId = uidParts[1];
513                        mEndOfMessage = false;
514                        return true;
515                    }
516                }
517                return false;
518            }
519        }
520
521        private void indexMessage(int msgNum, Pop3Message message) {
522            mMsgNumToMsgMap.put(msgNum, message);
523            mUidToMsgMap.put(message.getUid(), message);
524            mUidToMsgNumMap.put(message.getUid(), msgNum);
525        }
526
527        @Override
528        public Message[] getMessages(String[] uids, MessageRetrievalListener listener) {
529            throw new UnsupportedOperationException(
530                    "Pop3Folder.getMessage(MessageRetrievalListener)");
531        }
532
533        /**
534         * Fetch the items contained in the FetchProfile into the given set of
535         * Messages in as efficient a manner as possible.
536         * @param messages
537         * @param fp
538         * @throws MessagingException
539         */
540        @Override
541        public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
542                throws MessagingException {
543            throw new UnsupportedOperationException(
544                    "Pop3Folder.fetch(Message[], FetchProfile, MessageRetrievalListener)");
545        }
546
547        /**
548         * Fetches the body of the given message, limiting the stored data
549         * to the specified number of lines. If lines is -1 the entire message
550         * is fetched. This is implemented with RETR for lines = -1 or TOP
551         * for any other value. If the server does not support TOP it is
552         * emulated with RETR and extra lines are thrown away.
553         *
554         * @param message
555         * @param lines
556         * @param callback optional callback that reports progress of the fetch
557         */
558        public void fetchBody(Pop3Message message, int lines,
559                EOLConvertingInputStream.Callback callback) throws IOException, MessagingException {
560            String response = null;
561            int messageId = mUidToMsgNumMap.get(message.getUid());
562            if (lines == -1) {
563                // Fetch entire message
564                response = executeSimpleCommand(String.format(Locale.US, "RETR %d", messageId));
565            } else {
566                // Fetch partial message.  Try "TOP", and fall back to slower "RETR" if necessary
567                try {
568                    response = executeSimpleCommand(
569                            String.format(Locale.US, "TOP %d %d", messageId,  lines));
570                } catch (MessagingException me) {
571                    try {
572                        response = executeSimpleCommand(
573                                String.format(Locale.US, "RETR %d", messageId));
574                    } catch (MessagingException e) {
575                        LogUtils.w(Logging.LOG_TAG, "Can't read message " + messageId);
576                    }
577                }
578            }
579            if (response != null)  {
580                try {
581                    int ok = response.indexOf("OK");
582                    if (ok > 0) {
583                        try {
584                            int start = ok + 3;
585                            if (start > response.length()) {
586                                // No length was supplied, this is a protocol error.
587                                LogUtils.e(Logging.LOG_TAG, "No body length supplied");
588                                message.setSize(0);
589                            } else {
590                                int end = response.indexOf(" ", start);
591                                final String intString;
592                                if (end > 0) {
593                                    intString = response.substring(start, end);
594                                } else {
595                                    intString = response.substring(start);
596                                }
597                                message.setSize(Integer.parseInt(intString));
598                            }
599                        } catch (NumberFormatException e) {
600                            // We tried
601                        }
602                    }
603                    InputStream in = mTransport.getInputStream();
604                    if (DEBUG_LOG_RAW_STREAM && DebugUtils.DEBUG) {
605                        in = new LoggingInputStream(in);
606                    }
607                    message.parse(new Pop3ResponseInputStream(in), callback);
608                }
609                catch (MessagingException me) {
610                    /*
611                     * If we're only downloading headers it's possible
612                     * we'll get a broken MIME message which we're not
613                     * real worried about. If we've downloaded the body
614                     * and can't parse it we need to let the user know.
615                     */
616                    if (lines == -1) {
617                        throw me;
618                    }
619                }
620            }
621        }
622
623        @Override
624        public Flag[] getPermanentFlags() {
625            return PERMANENT_FLAGS;
626        }
627
628        @Override
629        public void appendMessage(Context context, Message message, boolean noTimeout) {
630        }
631
632        @Override
633        public void delete(boolean recurse) {
634        }
635
636        @Override
637        public Message[] expunge() {
638            return null;
639        }
640
641        public void deleteMessage(Message message) throws MessagingException {
642            mOneMessage[0] = message;
643            setFlags(mOneMessage, PERMANENT_FLAGS, true);
644        }
645
646        @Override
647        public void setFlags(Message[] messages, Flag[] flags, boolean value)
648                throws MessagingException {
649            if (!value || !Utility.arrayContains(flags, Flag.DELETED)) {
650                /*
651                 * The only flagging we support is setting the Deleted flag.
652                 */
653                return;
654            }
655            try {
656                for (Message message : messages) {
657                    try {
658                        String uid = message.getUid();
659                        int msgNum = mUidToMsgNumMap.get(uid);
660                        executeSimpleCommand(String.format(Locale.US, "DELE %s", msgNum));
661                        // Remove from the maps
662                        mMsgNumToMsgMap.remove(msgNum);
663                        mUidToMsgNumMap.remove(uid);
664                    } catch (MessagingException e) {
665                        // A failed deletion isn't a problem
666                    }
667                }
668            }
669            catch (IOException ioe) {
670                mTransport.close();
671                if (DebugUtils.DEBUG) {
672                    LogUtils.d(Logging.LOG_TAG, ioe.toString());
673                }
674                throw new MessagingException("setFlags()", ioe);
675            }
676        }
677
678        @Override
679        public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) {
680            throw new UnsupportedOperationException("copyMessages is not supported in POP3");
681        }
682
683        private Pop3Capabilities getCapabilities() throws IOException {
684            Pop3Capabilities capabilities = new Pop3Capabilities();
685            try {
686                String response = executeSimpleCommand("CAPA");
687                while ((response = mTransport.readLine(true)) != null) {
688                    if (response.equals(".")) {
689                        break;
690                    } else if (response.equalsIgnoreCase("STLS")){
691                        capabilities.stls = true;
692                    }
693                }
694            }
695            catch (MessagingException me) {
696                /*
697                 * The server may not support the CAPA command, so we just eat this Exception
698                 * and allow the empty capabilities object to be returned.
699                 */
700            }
701            return capabilities;
702        }
703
704        /**
705         * Send a single command and wait for a single line response.  Reopens the connection,
706         * if it is closed.  Leaves the connection open.
707         *
708         * @param command The command string to send to the server.
709         * @return Returns the response string from the server.
710         */
711        private String executeSimpleCommand(String command) throws IOException, MessagingException {
712            return executeSensitiveCommand(command, null);
713        }
714
715        /**
716         * Send a single command and wait for a single line response.  Reopens the connection,
717         * if it is closed.  Leaves the connection open.
718         *
719         * @param command The command string to send to the server.
720         * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
721         * please pass a replacement string here (for logging).
722         * @return Returns the response string from the server.
723         */
724        private String executeSensitiveCommand(String command, String sensitiveReplacement)
725                throws IOException, MessagingException {
726            open(OpenMode.READ_WRITE);
727
728            if (command != null) {
729                mTransport.writeLine(command, sensitiveReplacement);
730            }
731
732            String response = mTransport.readLine(true);
733
734            if (response.length() > 1 && response.charAt(0) == '-') {
735                throw new MessagingException(response);
736            }
737
738            return response;
739        }
740
741        @Override
742        public boolean equals(Object o) {
743            if (o instanceof Pop3Folder) {
744                return ((Pop3Folder) o).mName.equals(mName);
745            }
746            return super.equals(o);
747        }
748
749        @Override
750        @VisibleForTesting
751        public boolean isOpen() {
752            return mTransport.isOpen();
753        }
754
755        @Override
756        public Message createMessage(String uid) {
757            return new Pop3Message(uid, this);
758        }
759
760        @Override
761        public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) {
762            return null;
763        }
764    }
765
766    public static class Pop3Message extends MimeMessage {
767        public Pop3Message(String uid, Pop3Folder folder) {
768            mUid = uid;
769            mFolder = folder;
770            mSize = -1;
771        }
772
773        public void setSize(int size) {
774            mSize = size;
775        }
776
777        @Override
778        public void parse(InputStream in) throws IOException, MessagingException {
779            super.parse(in);
780        }
781
782        @Override
783        public void setFlag(Flag flag, boolean set) throws MessagingException {
784            super.setFlag(flag, set);
785            mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
786        }
787    }
788
789    /**
790     * POP3 Capabilities as defined in RFC 2449.  This is not a complete list of CAPA
791     * responses - just those that we use in this client.
792     */
793    class Pop3Capabilities {
794        /** The STLS (start TLS) command is supported */
795        public boolean stls;
796
797        @Override
798        public String toString() {
799            return String.format("STLS %b", stls);
800        }
801    }
802
803    // TODO figure out what is special about this and merge it into MailTransport
804    class Pop3ResponseInputStream extends InputStream {
805        private final InputStream mIn;
806        private boolean mStartOfLine = true;
807        private boolean mFinished;
808
809        public Pop3ResponseInputStream(InputStream in) {
810            mIn = in;
811        }
812
813        @Override
814        public int read() throws IOException {
815            if (mFinished) {
816                return -1;
817            }
818            int d = mIn.read();
819            if (mStartOfLine && d == '.') {
820                d = mIn.read();
821                if (d == '\r') {
822                    mFinished = true;
823                    mIn.read();
824                    return -1;
825                }
826            }
827
828            mStartOfLine = (d == '\n');
829
830            return d;
831        }
832    }
833}
834