Pop3Store.java revision 7f7f7e6402eec1baab6bedcb58da61369cae4097
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.Transport;
34import com.android.emailcommon.mail.Folder.OpenMode;
35import com.android.emailcommon.mail.Message;
36import com.android.emailcommon.mail.MessagingException;
37import com.android.emailcommon.provider.Account;
38import com.android.emailcommon.provider.HostAuth;
39import com.android.emailcommon.provider.Mailbox;
40import com.android.emailcommon.service.EmailServiceProxy;
41import com.android.emailcommon.service.SearchParams;
42import com.android.emailcommon.utility.LoggingInputStream;
43import com.android.emailcommon.utility.Utility;
44import com.google.common.annotations.VisibleForTesting;
45
46import java.io.IOException;
47import java.io.InputStream;
48import java.util.ArrayList;
49import java.util.HashMap;
50
51public class Pop3Store extends Store {
52    // All flags defining debug or development code settings must be FALSE
53    // when code is checked in or released.
54    private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false;
55    private static boolean DEBUG_LOG_RAW_STREAM = false;
56
57    private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
58    /** The name of the only mailbox available to POP3 accounts */
59    private static final String POP3_MAILBOX_NAME = "INBOX";
60    private final HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
61    private final Message[] mOneMessage = new Message[1];
62
63    /**
64     * Static named constructor.
65     */
66    public static Store newInstance(Account account, Context context) throws MessagingException {
67        return new Pop3Store(context, account);
68    }
69
70    /**
71     * Creates a new store for the given account.
72     */
73    private Pop3Store(Context context, Account account) throws MessagingException {
74        mContext = context;
75        mAccount = account;
76
77        HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
78        if (recvAuth == null || !HostAuth.LEGACY_SCHEME_POP3.equalsIgnoreCase(recvAuth.mProtocol)) {
79            throw new MessagingException("Unsupported protocol");
80        }
81        mTransport = new MailTransport(context, "POP3", recvAuth);
82        String[] userInfoParts = recvAuth.getLogin();
83        if (userInfoParts != null) {
84            mUsername = userInfoParts[0];
85            mPassword = userInfoParts[1];
86        }
87    }
88
89    /**
90     * For testing only.  Injects a different transport.  The transport should already be set
91     * up and ready to use.  Do not use for real code.
92     * @param testTransport The Transport to inject and use for all future communication.
93     */
94    /* package */ void setTransport(Transport testTransport) {
95        mTransport = testTransport;
96    }
97
98    @Override
99    public Folder getFolder(String name) {
100        Folder folder = mFolders.get(name);
101        if (folder == null) {
102            folder = new Pop3Folder(name);
103            mFolders.put(folder.getName(), folder);
104        }
105        return folder;
106    }
107
108    private final int[] DEFAULT_FOLDERS = {
109            Mailbox.TYPE_DRAFTS,
110            Mailbox.TYPE_OUTBOX,
111            Mailbox.TYPE_SENT,
112            Mailbox.TYPE_TRASH
113    };
114
115    @Override
116    public Folder[] updateFolders() {
117        String inboxName = mContext.getString(R.string.mailbox_name_display_inbox);
118        Mailbox mailbox = Mailbox.getMailboxForPath(mContext, mAccount.mId, inboxName);
119        updateMailbox(mailbox, mAccount.mId, inboxName, '\0', true, Mailbox.TYPE_INBOX);
120        // Force the parent key to be "no mailbox" for the mail POP3 mailbox
121        mailbox.mParentKey = Mailbox.NO_MAILBOX;
122        if (mailbox.isSaved()) {
123            mailbox.update(mContext, mailbox.toContentValues());
124        } else {
125            mailbox.save(mContext);
126        }
127
128        // Build default mailboxes as well, in case they're not already made.
129        for (int type : DEFAULT_FOLDERS) {
130            if (Mailbox.findMailboxOfType(mContext, mAccount.mId, type) == Mailbox.NO_MAILBOX) {
131                String name = getMailboxServerName(mContext, type);
132                mailbox = Mailbox.newSystemMailbox(mAccount.mId, type, name);
133                mailbox.save(mContext);
134            }
135        }
136
137        return new Folder[] { getFolder(inboxName) };
138    }
139
140
141    /**
142     * Returns the server-side name for a specific mailbox.
143     *
144     * @return the resource string corresponding to the mailbox type, empty if not found.
145     */
146    public String getMailboxServerName(Context context, int mailboxType) {
147        int resId = -1;
148        switch (mailboxType) {
149            case Mailbox.TYPE_INBOX:
150                resId = R.string.mailbox_name_server_inbox;
151                break;
152            case Mailbox.TYPE_OUTBOX:
153                resId = R.string.mailbox_name_server_outbox;
154                break;
155            case Mailbox.TYPE_DRAFTS:
156                resId = R.string.mailbox_name_server_drafts;
157                break;
158            case Mailbox.TYPE_TRASH:
159                resId = R.string.mailbox_name_server_trash;
160                break;
161            case Mailbox.TYPE_SENT:
162                resId = R.string.mailbox_name_server_sent;
163                break;
164            case Mailbox.TYPE_JUNK:
165                resId = R.string.mailbox_name_server_junk;
166                break;
167        }
168        return resId != -1 ? context.getString(resId) : "";
169    }
170
171    /**
172     * Used by account setup to test if an account's settings are appropriate.  The definition
173     * of "checked" here is simply, can you log into the account and does it meet some minimum set
174     * of feature requirements?
175     *
176     * @throws MessagingException if there was some problem with the account
177     */
178    @Override
179    public Bundle checkSettings() throws MessagingException {
180        Pop3Folder folder = new Pop3Folder(POP3_MAILBOX_NAME);
181        Bundle bundle = null;
182        // Close any open or half-open connections - checkSettings should always be "fresh"
183        if (mTransport.isOpen()) {
184            folder.close(false);
185        }
186        try {
187            folder.open(OpenMode.READ_WRITE);
188            bundle = folder.checkSettings();
189        } finally {
190            folder.close(false);    // false == don't expunge anything
191        }
192        return bundle;
193    }
194
195    public class Pop3Folder extends Folder {
196        private final HashMap<String, Pop3Message> mUidToMsgMap
197                = new HashMap<String, Pop3Message>();
198        private final HashMap<Integer, Pop3Message> mMsgNumToMsgMap
199                = new HashMap<Integer, Pop3Message>();
200        private final HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>();
201        private final String mName;
202        private int mMessageCount;
203        private Pop3Capabilities mCapabilities;
204
205        public Pop3Folder(String name) {
206            if (name.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
207                mName = POP3_MAILBOX_NAME;
208            } else {
209                mName = name;
210            }
211        }
212
213        /**
214         * Used by account setup to test if an account's settings are appropriate.  Here, we run
215         * an additional test to see if UIDL is supported on the server. If it's not we
216         * can't service this account.
217         *
218         * @return Bundle containing validation data (code and, if appropriate, error message)
219         * @throws MessagingException if the account is not going to be useable
220         */
221        public Bundle checkSettings() throws MessagingException {
222            Bundle bundle = new Bundle();
223            int result = MessagingException.NO_ERROR;
224            try {
225                UidlParser parser = new UidlParser();
226                executeSimpleCommand("UIDL");
227                // drain the entire output, so additional communications don't get confused.
228                String response;
229                while ((response = mTransport.readLine()) != null) {
230                    parser.parseMultiLine(response);
231                    if (parser.mEndOfMessage) {
232                        break;
233                    }
234                }
235            } catch (IOException ioe) {
236                mTransport.close();
237                result = MessagingException.IOERROR;
238                bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE,
239                        ioe.getMessage());
240            }
241            bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
242            return bundle;
243        }
244
245        @Override
246        public synchronized void open(OpenMode mode) throws MessagingException {
247            if (mTransport.isOpen()) {
248                return;
249            }
250
251            if (!mName.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
252                throw new MessagingException("Folder does not exist");
253            }
254
255            try {
256                mTransport.open();
257
258                // Eat the banner
259                executeSimpleCommand(null);
260
261                mCapabilities = getCapabilities();
262
263                if (mTransport.canTryTlsSecurity()) {
264                    if (mCapabilities.stls) {
265                        executeSimpleCommand("STLS");
266                        mTransport.reopenTls();
267                    } else {
268                        if (MailActivityEmail.DEBUG) {
269                            Log.d(Logging.LOG_TAG, "TLS not supported but required");
270                        }
271                        throw new MessagingException(MessagingException.TLS_REQUIRED);
272                    }
273                }
274
275                try {
276                    executeSensitiveCommand("USER " + mUsername, "USER /redacted/");
277                    executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/");
278                } catch (MessagingException me) {
279                    if (MailActivityEmail.DEBUG) {
280                        Log.d(Logging.LOG_TAG, me.toString());
281                    }
282                    throw new AuthenticationFailedException(null, me);
283                }
284            } catch (IOException ioe) {
285                mTransport.close();
286                if (MailActivityEmail.DEBUG) {
287                    Log.d(Logging.LOG_TAG, ioe.toString());
288                }
289                throw new MessagingException(MessagingException.IOERROR, ioe.toString());
290            }
291
292            Exception statException = null;
293            try {
294                String response = executeSimpleCommand("STAT");
295                String[] parts = response.split(" ");
296                if (parts.length < 2) {
297                    statException = new IOException();
298                } else {
299                    mMessageCount = Integer.parseInt(parts[1]);
300                }
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()) != 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         * Note:  Some servers (e.g. live.com) don't support CAPA, but turn out to
600         * support TOP after all.  For better performance on these servers, we'll always
601         * probe TOP, and fall back to RETR when it's truly unsupported.
602         *
603         * @param message
604         * @param lines
605         */
606        public void fetchBody(Pop3Message message, int lines)
607                throws IOException, MessagingException {
608            String response = null;
609            int messageId = mUidToMsgNumMap.get(message.getUid());
610            if (lines == -1) {
611                // Fetch entire message
612                response = executeSimpleCommand(String.format("RETR %d", messageId));
613            } else {
614                // Fetch partial message.  Try "TOP", and fall back to slower "RETR" if necessary
615                try {
616                    response = executeSimpleCommand(String.format("TOP %d %d", messageId,  lines));
617                } catch (MessagingException me) {
618                    try {
619                        response = executeSimpleCommand(String.format("RETR %d", messageId));
620                    } catch (MessagingException e) {
621                        Log.w(Logging.LOG_TAG, "Can't read message " + messageId);
622                    }
623                }
624            }
625            if (response != null)  {
626                try {
627                    int ok = response.indexOf("OK");
628                    if (ok > 0) {
629                        try {
630                            int start = ok + 3;
631                            int end = response.indexOf(" ", start);
632                            String intString;
633                            if (end > 0) {
634                                intString = response.substring(start, end);
635                            } else {
636                                intString = response.substring(start);
637                            }
638                            message.setSize(Integer.parseInt(intString));
639                        } catch (NumberFormatException e) {
640                            // We tried
641                        }
642                    }
643                    InputStream in = mTransport.getInputStream();
644                    if (DEBUG_LOG_RAW_STREAM && MailActivityEmail.DEBUG) {
645                        in = new LoggingInputStream(in);
646                    }
647                    message.parse(new Pop3ResponseInputStream(in));
648                }
649                catch (MessagingException me) {
650                    /*
651                     * If we're only downloading headers it's possible
652                     * we'll get a broken MIME message which we're not
653                     * real worried about. If we've downloaded the body
654                     * and can't parse it we need to let the user know.
655                     */
656                    if (lines == -1) {
657                        throw me;
658                    }
659                }
660            }
661        }
662
663        @Override
664        public Flag[] getPermanentFlags() {
665            return PERMANENT_FLAGS;
666        }
667
668        @Override
669        public void appendMessages(Message[] messages) {
670        }
671
672        @Override
673        public void delete(boolean recurse) {
674        }
675
676        @Override
677        public Message[] expunge() {
678            return null;
679        }
680
681        public void deleteMessage(Message message) throws MessagingException {
682            mOneMessage[0] = message;
683            setFlags(mOneMessage, PERMANENT_FLAGS, true);
684        }
685
686        @Override
687        public void setFlags(Message[] messages, Flag[] flags, boolean value)
688                throws MessagingException {
689            if (!value || !Utility.arrayContains(flags, Flag.DELETED)) {
690                /*
691                 * The only flagging we support is setting the Deleted flag.
692                 */
693                return;
694            }
695            try {
696                for (Message message : messages) {
697                    try {
698                        String uid = message.getUid();
699                        int msgNum = mUidToMsgNumMap.get(uid);
700                        executeSimpleCommand(String.format("DELE %s", msgNum));
701                        // Remove from the maps
702                        mMsgNumToMsgMap.remove(msgNum);
703                        mUidToMsgNumMap.remove(uid);
704                    } catch (MessagingException e) {
705                        // A failed deletion isn't a problem
706                    }
707                }
708            }
709            catch (IOException ioe) {
710                mTransport.close();
711                if (MailActivityEmail.DEBUG) {
712                    Log.d(Logging.LOG_TAG, ioe.toString());
713                }
714                throw new MessagingException("setFlags()", ioe);
715            }
716        }
717
718        @Override
719        public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) {
720            throw new UnsupportedOperationException("copyMessages is not supported in POP3");
721        }
722
723        private Pop3Capabilities getCapabilities() throws IOException {
724            Pop3Capabilities capabilities = new Pop3Capabilities();
725            try {
726                String response = executeSimpleCommand("CAPA");
727                while ((response = mTransport.readLine()) != null) {
728                    if (response.equals(".")) {
729                        break;
730                    } else if (response.equalsIgnoreCase("STLS")){
731                        capabilities.stls = true;
732                    }
733                }
734            }
735            catch (MessagingException me) {
736                /*
737                 * The server may not support the CAPA command, so we just eat this Exception
738                 * and allow the empty capabilities object to be returned.
739                 */
740            }
741            return capabilities;
742        }
743
744        /**
745         * Send a single command and wait for a single line response.  Reopens the connection,
746         * if it is closed.  Leaves the connection open.
747         *
748         * @param command The command string to send to the server.
749         * @return Returns the response string from the server.
750         */
751        private String executeSimpleCommand(String command) throws IOException, MessagingException {
752            return executeSensitiveCommand(command, null);
753        }
754
755        /**
756         * Send a single command and wait for a single line response.  Reopens the connection,
757         * if it is closed.  Leaves the connection open.
758         *
759         * @param command The command string to send to the server.
760         * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
761         * please pass a replacement string here (for logging).
762         * @return Returns the response string from the server.
763         */
764        private String executeSensitiveCommand(String command, String sensitiveReplacement)
765                throws IOException, MessagingException {
766            open(OpenMode.READ_WRITE);
767
768            if (command != null) {
769                mTransport.writeLine(command, sensitiveReplacement);
770            }
771
772            String response = mTransport.readLine();
773
774            if (response.length() > 1 && response.charAt(0) == '-') {
775                throw new MessagingException(response);
776            }
777
778            return response;
779        }
780
781        @Override
782        public boolean equals(Object o) {
783            if (o instanceof Pop3Folder) {
784                return ((Pop3Folder) o).mName.equals(mName);
785            }
786            return super.equals(o);
787        }
788
789        @Override
790        @VisibleForTesting
791        public boolean isOpen() {
792            return mTransport.isOpen();
793        }
794
795        @Override
796        public Message createMessage(String uid) {
797            return new Pop3Message(uid, this);
798        }
799
800        @Override
801        public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) {
802            return null;
803        }
804    }
805
806    public static class Pop3Message extends MimeMessage {
807        public Pop3Message(String uid, Pop3Folder folder) {
808            mUid = uid;
809            mFolder = folder;
810            mSize = -1;
811        }
812
813        public void setSize(int size) {
814            mSize = size;
815        }
816
817        @Override
818        public void parse(InputStream in) throws IOException, MessagingException {
819            super.parse(in);
820        }
821
822        @Override
823        public void setFlag(Flag flag, boolean set) throws MessagingException {
824            super.setFlag(flag, set);
825            mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
826        }
827    }
828
829    /**
830     * POP3 Capabilities as defined in RFC 2449.  This is not a complete list of CAPA
831     * responses - just those that we use in this client.
832     */
833    class Pop3Capabilities {
834        /** The STLS (start TLS) command is supported */
835        public boolean stls;
836
837        @Override
838        public String toString() {
839            return String.format("STLS %b", stls);
840        }
841    }
842
843    // TODO figure out what is special about this and merge it into MailTransport
844    class Pop3ResponseInputStream extends InputStream {
845        private final InputStream mIn;
846        private boolean mStartOfLine = true;
847        private boolean mFinished;
848
849        public Pop3ResponseInputStream(InputStream in) {
850            mIn = in;
851        }
852
853        @Override
854        public int read() throws IOException {
855            if (mFinished) {
856                return -1;
857            }
858            int d = mIn.read();
859            if (mStartOfLine && d == '.') {
860                d = mIn.read();
861                if (d == '\r') {
862                    mFinished = true;
863                    mIn.read();
864                    return -1;
865                }
866            }
867
868            mStartOfLine = (d == '\n');
869
870            return d;
871        }
872    }
873}
874