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