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