Home | History | Annotate | Download | only in store
      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 
     17 package com.android.email.mail.store;
     18 
     19 import android.content.Context;
     20 import android.os.Bundle;
     21 
     22 import com.android.email.mail.Store;
     23 import com.android.email.mail.transport.MailTransport;
     24 import com.android.email2.ui.MailActivityEmail;
     25 import com.android.emailcommon.Logging;
     26 import com.android.emailcommon.internet.MimeMessage;
     27 import com.android.emailcommon.mail.AuthenticationFailedException;
     28 import com.android.emailcommon.mail.FetchProfile;
     29 import com.android.emailcommon.mail.Flag;
     30 import com.android.emailcommon.mail.Folder;
     31 import com.android.emailcommon.mail.Folder.OpenMode;
     32 import com.android.emailcommon.mail.Message;
     33 import com.android.emailcommon.mail.MessagingException;
     34 import com.android.emailcommon.provider.Account;
     35 import com.android.emailcommon.provider.HostAuth;
     36 import com.android.emailcommon.provider.Mailbox;
     37 import com.android.emailcommon.service.EmailServiceProxy;
     38 import com.android.emailcommon.service.SearchParams;
     39 import com.android.emailcommon.utility.LoggingInputStream;
     40 import com.android.emailcommon.utility.Utility;
     41 import com.android.mail.utils.LogUtils;
     42 import com.google.common.annotations.VisibleForTesting;
     43 
     44 import org.apache.james.mime4j.EOLConvertingInputStream;
     45 
     46 import java.io.IOException;
     47 import java.io.InputStream;
     48 import java.util.ArrayList;
     49 import java.util.HashMap;
     50 import java.util.Locale;
     51 
     52 public 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