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