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 com.android.email.Email;
     20 import com.android.email.Utility;
     21 import com.android.email.mail.AuthenticationFailedException;
     22 import com.android.email.mail.FetchProfile;
     23 import com.android.email.mail.Flag;
     24 import com.android.email.mail.Folder;
     25 import com.android.email.mail.Message;
     26 import com.android.email.mail.MessageRetrievalListener;
     27 import com.android.email.mail.MessagingException;
     28 import com.android.email.mail.Store;
     29 import com.android.email.mail.Transport;
     30 import com.android.email.mail.Folder.OpenMode;
     31 import com.android.email.mail.internet.MimeMessage;
     32 import com.android.email.mail.transport.LoggingInputStream;
     33 import com.android.email.mail.transport.MailTransport;
     34 
     35 import android.content.Context;
     36 import android.util.Config;
     37 import android.util.Log;
     38 
     39 import java.io.IOException;
     40 import java.io.InputStream;
     41 import java.net.URI;
     42 import java.net.URISyntaxException;
     43 import java.util.ArrayList;
     44 import java.util.HashMap;
     45 import java.util.HashSet;
     46 
     47 public class Pop3Store extends Store {
     48     // All flags defining debug or development code settings must be FALSE
     49     // when code is checked in or released.
     50     private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false;
     51     private static boolean DEBUG_LOG_RAW_STREAM = false;
     52 
     53     private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
     54 
     55     private Transport mTransport;
     56     private String mUsername;
     57     private String mPassword;
     58     private HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
     59 
     60 //    /**
     61 //     * Detected latency, used for usage scaling.
     62 //     * Usage scaling occurs when it is neccesary to get information about
     63 //     * messages that could result in large data loads. This value allows
     64 //     * the code that loads this data to decide between using large downloads
     65 //     * (high latency) or multiple round trips (low latency) to accomplish
     66 //     * the same thing.
     67 //     * Default is Integer.MAX_VALUE implying massive latency so that the large
     68 //     * download method is used by default until latency data is collected.
     69 //     */
     70 //    private int mLatencyMs = Integer.MAX_VALUE;
     71 //
     72 //    /**
     73 //     * Detected throughput, used for usage scaling.
     74 //     * Usage scaling occurs when it is neccesary to get information about
     75 //     * messages that could result in large data loads. This value allows
     76 //     * the code that loads this data to decide between using large downloads
     77 //     * (high latency) or multiple round trips (low latency) to accomplish
     78 //     * the same thing.
     79 //     * Default is Integer.MAX_VALUE implying massive bandwidth so that the
     80 //     * large download method is used by default until latency data is
     81 //     * collected.
     82 //     */
     83 //    private int mThroughputKbS = Integer.MAX_VALUE;
     84 
     85     /**
     86      * Static named constructor.
     87      */
     88     public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks)
     89             throws MessagingException {
     90         return new Pop3Store(uri);
     91     }
     92 
     93     /**
     94      * pop3://user:password@server:port
     95      * pop3+tls+://user:password@server:port
     96      * pop3+tls+trustallcerts://user:password@server:port
     97      * pop3+ssl+://user:password@server:port
     98      * pop3+ssl+trustallcerts://user:password@server:port
     99      *
    100      * @param _uri
    101      */
    102     private Pop3Store(String _uri) throws MessagingException {
    103         URI uri;
    104         try {
    105             uri = new URI(_uri);
    106         } catch (URISyntaxException use) {
    107             throw new MessagingException("Invalid Pop3Store URI", use);
    108         }
    109 
    110         String scheme = uri.getScheme();
    111         if (scheme == null || !scheme.startsWith(STORE_SCHEME_POP3)) {
    112             throw new MessagingException("Unsupported protocol");
    113         }
    114         // defaults, which can be changed by security modifiers
    115         int connectionSecurity = Transport.CONNECTION_SECURITY_NONE;
    116         int defaultPort = 110;
    117         // check for security modifiers and apply changes
    118         if (scheme.contains("+ssl")) {
    119             connectionSecurity = Transport.CONNECTION_SECURITY_SSL;
    120             defaultPort = 995;
    121         } else if (scheme.contains("+tls")) {
    122             connectionSecurity = Transport.CONNECTION_SECURITY_TLS;
    123         }
    124         boolean trustCertificates = scheme.contains(STORE_SECURITY_TRUST_CERTIFICATES);
    125 
    126         mTransport = new MailTransport("POP3");
    127         mTransport.setUri(uri, defaultPort);
    128         mTransport.setSecurity(connectionSecurity, trustCertificates);
    129 
    130         String[] userInfoParts = mTransport.getUserInfoParts();
    131         if (userInfoParts != null) {
    132             mUsername = userInfoParts[0];
    133             if (userInfoParts.length > 1) {
    134                 mPassword = userInfoParts[1];
    135             }
    136         }
    137     }
    138 
    139     /**
    140      * For testing only.  Injects a different transport.  The transport should already be set
    141      * up and ready to use.  Do not use for real code.
    142      * @param testTransport The Transport to inject and use for all future communication.
    143      */
    144     /* package */ void setTransport(Transport testTransport) {
    145         mTransport = testTransport;
    146     }
    147 
    148     @Override
    149     public Folder getFolder(String name) throws MessagingException {
    150         Folder folder = mFolders.get(name);
    151         if (folder == null) {
    152             folder = new Pop3Folder(name);
    153             mFolders.put(folder.getName(), folder);
    154         }
    155         return folder;
    156     }
    157 
    158     @Override
    159     public Folder[] getPersonalNamespaces() throws MessagingException {
    160         return new Folder[] {
    161             getFolder("INBOX"),
    162         };
    163     }
    164 
    165     /**
    166      * Used by account setup to test if an account's settings are appropriate.  The definition
    167      * of "checked" here is simply, can you log into the account and does it meet some minimum set
    168      * of feature requirements?
    169      *
    170      * @throws MessagingException if there was some problem with the account
    171      */
    172     @Override
    173     public void checkSettings() throws MessagingException {
    174         Pop3Folder folder = new Pop3Folder("INBOX");
    175         try {
    176             folder.open(OpenMode.READ_WRITE, null);
    177             folder.checkSettings();
    178         } finally {
    179             folder.close(false);    // false == don't expunge anything
    180         }
    181     }
    182 
    183     class Pop3Folder extends Folder {
    184         private HashMap<String, Pop3Message> mUidToMsgMap = new HashMap<String, Pop3Message>();
    185         private HashMap<Integer, Pop3Message> mMsgNumToMsgMap = new HashMap<Integer, Pop3Message>();
    186         private HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>();
    187         private String mName;
    188         private int mMessageCount;
    189         private Pop3Capabilities mCapabilities;
    190 
    191         public Pop3Folder(String name) {
    192             this.mName = name;
    193             if (mName.equalsIgnoreCase("INBOX")) {
    194                 mName = "INBOX";
    195             }
    196         }
    197 
    198         /**
    199          * Used by account setup to test if an account's settings are appropriate.  Here, we run
    200          * an additional test to see if UIDL is supported on the server. If it's not we
    201          * can't service this account.
    202          *
    203          * @throws MessagingException if the account is not going to be useable
    204          */
    205         public void checkSettings() throws MessagingException {
    206             if (!mCapabilities.uidl) {
    207                 try {
    208                     UidlParser parser = new UidlParser();
    209                     executeSimpleCommand("UIDL");
    210                     // drain the entire output, so additional communications don't get confused.
    211                     String response;
    212                     while ((response = mTransport.readLine()) != null) {
    213                         parser.parseMultiLine(response);
    214                         if (parser.mEndOfMessage) {
    215                             break;
    216                         }
    217                     }
    218                 } catch (IOException ioe) {
    219                     mTransport.close();
    220                     throw new MessagingException(null, ioe);
    221                 }
    222             }
    223         }
    224 
    225         @Override
    226         public synchronized void open(OpenMode mode, PersistentDataCallbacks callbacks)
    227                 throws MessagingException {
    228             if (mTransport.isOpen()) {
    229                 return;
    230             }
    231 
    232             if (!mName.equalsIgnoreCase("INBOX")) {
    233                 throw new MessagingException("Folder does not exist");
    234             }
    235 
    236             try {
    237                 mTransport.open();
    238 
    239                 // Eat the banner
    240                 executeSimpleCommand(null);
    241 
    242                 mCapabilities = getCapabilities();
    243 
    244                 if (mTransport.canTryTlsSecurity()) {
    245                     if (mCapabilities.stls) {
    246                         executeSimpleCommand("STLS");
    247                         mTransport.reopenTls();
    248                     } else {
    249                         if (Config.LOGD && Email.DEBUG) {
    250                             Log.d(Email.LOG_TAG, "TLS not supported but required");
    251                         }
    252                         throw new MessagingException(MessagingException.TLS_REQUIRED);
    253                     }
    254                 }
    255 
    256                 try {
    257                     executeSensitiveCommand("USER " + mUsername, "USER /redacted/");
    258                     executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/");
    259                 } catch (MessagingException me) {
    260                     if (Config.LOGD && Email.DEBUG) {
    261                         Log.d(Email.LOG_TAG, me.toString());
    262                     }
    263                     throw new AuthenticationFailedException(null, me);
    264                 }
    265             } catch (IOException ioe) {
    266                 mTransport.close();
    267                 if (Config.LOGD && Email.DEBUG) {
    268                     Log.d(Email.LOG_TAG, ioe.toString());
    269                 }
    270                 throw new MessagingException(MessagingException.IOERROR, ioe.toString());
    271             }
    272 
    273             Exception statException = null;
    274             try {
    275                 String response = executeSimpleCommand("STAT");
    276                 String[] parts = response.split(" ");
    277                 if (parts.length < 2) {
    278                     statException = new IOException();
    279                 } else {
    280                     mMessageCount = Integer.parseInt(parts[1]);
    281                 }
    282             } catch (IOException ioe) {
    283                 statException = ioe;
    284             } catch (NumberFormatException nfe) {
    285                 statException = nfe;
    286             }
    287             if (statException != null) {
    288                 mTransport.close();
    289                 if (Config.LOGD && Email.DEBUG) {
    290                     Log.d(Email.LOG_TAG, statException.toString());
    291                 }
    292                 throw new MessagingException("POP3 STAT", statException);
    293             }
    294             mUidToMsgMap.clear();
    295             mMsgNumToMsgMap.clear();
    296             mUidToMsgNumMap.clear();
    297         }
    298 
    299         @Override
    300         public OpenMode getMode() throws MessagingException {
    301             return OpenMode.READ_WRITE;
    302         }
    303 
    304         /**
    305          * Close the folder (and the transport below it).
    306          *
    307          * MUST NOT return any exceptions.
    308          *
    309          * @param expunge If true all deleted messages will be expunged (TODO - not implemented)
    310          */
    311         @Override
    312         public void close(boolean expunge) {
    313             try {
    314                 executeSimpleCommand("QUIT");
    315             }
    316             catch (Exception e) {
    317                 // ignore any problems here - just continue closing
    318             }
    319             mTransport.close();
    320         }
    321 
    322         @Override
    323         public String getName() {
    324             return mName;
    325         }
    326 
    327         // POP3 does not folder creation
    328         public boolean canCreate(FolderType type) {
    329             return false;
    330         }
    331 
    332         @Override
    333         public boolean create(FolderType type) throws MessagingException {
    334             return false;
    335         }
    336 
    337         @Override
    338         public boolean exists() throws MessagingException {
    339             return mName.equalsIgnoreCase("INBOX");
    340         }
    341 
    342         @Override
    343         public int getMessageCount() {
    344             return mMessageCount;
    345         }
    346 
    347         @Override
    348         public int getUnreadMessageCount() throws MessagingException {
    349             return -1;
    350         }
    351 
    352         @Override
    353         public Message getMessage(String uid) throws MessagingException {
    354             if (mUidToMsgNumMap.size() == 0) {
    355                 try {
    356                     indexMsgNums(1, mMessageCount);
    357                 } catch (IOException ioe) {
    358                     mTransport.close();
    359                     if (Email.DEBUG) {
    360                         Log.d(Email.LOG_TAG, "Unable to index during getMessage " + ioe);
    361                     }
    362                     throw new MessagingException("getMessages", ioe);
    363                 }
    364             }
    365             Pop3Message message = mUidToMsgMap.get(uid);
    366             return message;
    367         }
    368 
    369         @Override
    370         public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
    371                 throws MessagingException {
    372             if (start < 1 || end < 1 || end < start) {
    373                 throw new MessagingException(String.format("Invalid message set %d %d",
    374                         start, end));
    375             }
    376             try {
    377                 indexMsgNums(start, end);
    378             } catch (IOException ioe) {
    379                 mTransport.close();
    380                 if (Config.LOGD && Email.DEBUG) {
    381                     Log.d(Email.LOG_TAG, ioe.toString());
    382                 }
    383                 throw new MessagingException("getMessages", ioe);
    384             }
    385             ArrayList<Message> messages = new ArrayList<Message>();
    386             int i = 0;
    387             for (int msgNum = start; msgNum <= end; msgNum++) {
    388                 Pop3Message message = mMsgNumToMsgMap.get(msgNum);
    389                 if (listener != null) {
    390                     listener.messageStarted(message.getUid(), i++, (end - start) + 1);
    391                 }
    392                 messages.add(message);
    393                 if (listener != null) {
    394                     listener.messageFinished(message, i++, (end - start) + 1);
    395                 }
    396             }
    397             return messages.toArray(new Message[messages.size()]);
    398         }
    399 
    400         /**
    401          * Ensures that the given message set (from start to end inclusive)
    402          * has been queried so that uids are available in the local cache.
    403          * @param start
    404          * @param end
    405          * @throws MessagingException
    406          * @throws IOException
    407          */
    408         private void indexMsgNums(int start, int end)
    409                 throws MessagingException, IOException {
    410             int unindexedMessageCount = 0;
    411             for (int msgNum = start; msgNum <= end; msgNum++) {
    412                 if (mMsgNumToMsgMap.get(msgNum) == null) {
    413                     unindexedMessageCount++;
    414                 }
    415             }
    416             if (unindexedMessageCount == 0) {
    417                 return;
    418             }
    419             UidlParser parser = new UidlParser();
    420             if (DEBUG_FORCE_SINGLE_LINE_UIDL ||
    421                     (unindexedMessageCount < 50 && mMessageCount > 5000)) {
    422                 /*
    423                  * In extreme cases we'll do a UIDL command per message instead of a bulk
    424                  * download.
    425                  */
    426                 for (int msgNum = start; msgNum <= end; msgNum++) {
    427                     Pop3Message message = mMsgNumToMsgMap.get(msgNum);
    428                     if (message == null) {
    429                         String response = executeSimpleCommand("UIDL " + msgNum);
    430                         if (!parser.parseSingleLine(response)) {
    431                             throw new IOException();
    432                         }
    433                         message = new Pop3Message(parser.mUniqueId, this);
    434                         indexMessage(msgNum, message);
    435                     }
    436                 }
    437             } else {
    438                 String response = executeSimpleCommand("UIDL");
    439                 while ((response = mTransport.readLine()) != null) {
    440                     if (!parser.parseMultiLine(response)) {
    441                         throw new IOException();
    442                     }
    443                     if (parser.mEndOfMessage) {
    444                         break;
    445                     }
    446                     int msgNum = parser.mMessageNumber;
    447                     if (msgNum >= start && msgNum <= end) {
    448                         Pop3Message message = mMsgNumToMsgMap.get(msgNum);
    449                         if (message == null) {
    450                             message = new Pop3Message(parser.mUniqueId, this);
    451                             indexMessage(msgNum, message);
    452                         }
    453                     }
    454                 }
    455             }
    456         }
    457 
    458         private void indexUids(ArrayList<String> uids)
    459                 throws MessagingException, IOException {
    460             HashSet<String> unindexedUids = new HashSet<String>();
    461             for (String uid : uids) {
    462                 if (mUidToMsgMap.get(uid) == null) {
    463                     unindexedUids.add(uid);
    464                 }
    465             }
    466             if (unindexedUids.size() == 0) {
    467                 return;
    468             }
    469             /*
    470              * If we are missing uids in the cache the only sure way to
    471              * get them is to do a full UIDL list. A possible optimization
    472              * would be trying UIDL for the latest X messages and praying.
    473              */
    474             UidlParser parser = new UidlParser();
    475             String response = executeSimpleCommand("UIDL");
    476             while ((response = mTransport.readLine()) != null) {
    477                 parser.parseMultiLine(response);
    478                 if (parser.mEndOfMessage) {
    479                     break;
    480                 }
    481                 if (unindexedUids.contains(parser.mUniqueId)) {
    482                     Pop3Message message = mUidToMsgMap.get(parser.mUniqueId);
    483                     if (message == null) {
    484                         message = new Pop3Message(parser.mUniqueId, this);
    485                     }
    486                     indexMessage(parser.mMessageNumber, message);
    487                 }
    488             }
    489         }
    490 
    491         /**
    492          * Simple parser class for UIDL messages.
    493          *
    494          * <p>NOTE:  In variance with RFC 1939, we allow multiple whitespace between the
    495          * message-number and unique-id fields.  This provides greater compatibility with some
    496          * non-compliant POP3 servers, e.g. mail.comcast.net.
    497          */
    498         /* package */ class UidlParser {
    499 
    500             /**
    501              * Caller can read back message-number from this field
    502              */
    503             public int mMessageNumber;
    504             /**
    505              * Caller can read back unique-id from this field
    506              */
    507             public String mUniqueId;
    508             /**
    509              * True if the response was "end-of-message"
    510              */
    511             public boolean mEndOfMessage;
    512             /**
    513              * True if an error was reported
    514              */
    515             public boolean mErr;
    516 
    517             /**
    518              * Construct & Initialize
    519              */
    520             public UidlParser() {
    521                 mErr = true;
    522             }
    523 
    524             /**
    525              * Parse a single-line response.  This is returned from a command of the form
    526              * "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or
    527              * "-ERR diagnostic text"
    528              *
    529              * @param response The string returned from the server
    530              * @return true if the string parsed as expected (e.g. no syntax problems)
    531              */
    532             public boolean parseSingleLine(String response) {
    533                 mErr = false;
    534                 if (response == null || response.length() == 0) {
    535                     return false;
    536                 }
    537                 char first = response.charAt(0);
    538                 if (first == '+') {
    539                     String[] uidParts = response.split(" +");
    540                     if (uidParts.length >= 3) {
    541                         try {
    542                             mMessageNumber = Integer.parseInt(uidParts[1]);
    543                         } catch (NumberFormatException nfe) {
    544                             return false;
    545                         }
    546                         mUniqueId = uidParts[2];
    547                         mEndOfMessage = true;
    548                         return true;
    549                     }
    550                 } else if (first == '-') {
    551                     mErr = true;
    552                     return true;
    553                 }
    554                 return false;
    555             }
    556 
    557             /**
    558              * Parse a multi-line response.  This is returned from a command of the form
    559              * "UIDL" and will be formatted as: "." or "msg-num unique-id".
    560              *
    561              * @param response The string returned from the server
    562              * @return true if the string parsed as expected (e.g. no syntax problems)
    563              */
    564             public boolean parseMultiLine(String response) {
    565                 mErr = false;
    566                 if (response == null || response.length() == 0) {
    567                     return false;
    568                 }
    569                 char first = response.charAt(0);
    570                 if (first == '.') {
    571                     mEndOfMessage = true;
    572                     return true;
    573                 } else {
    574                     String[] uidParts = response.split(" +");
    575                     if (uidParts.length >= 2) {
    576                         try {
    577                             mMessageNumber = Integer.parseInt(uidParts[0]);
    578                         } catch (NumberFormatException nfe) {
    579                             return false;
    580                         }
    581                         mUniqueId = uidParts[1];
    582                         mEndOfMessage = false;
    583                         return true;
    584                     }
    585                 }
    586                 return false;
    587             }
    588         }
    589 
    590         private void indexMessage(int msgNum, Pop3Message message) {
    591             mMsgNumToMsgMap.put(msgNum, message);
    592             mUidToMsgMap.put(message.getUid(), message);
    593             mUidToMsgNumMap.put(message.getUid(), msgNum);
    594         }
    595 
    596         @Override
    597         public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException {
    598             throw new UnsupportedOperationException("Pop3Folder.getMessage(MessageRetrievalListener)");
    599         }
    600 
    601         @Override
    602         public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
    603                 throws MessagingException {
    604             throw new UnsupportedOperationException("Pop3Folder.getMessage(MessageRetrievalListener)");
    605         }
    606 
    607         /**
    608          * Fetch the items contained in the FetchProfile into the given set of
    609          * Messages in as efficient a manner as possible.
    610          * @param messages
    611          * @param fp
    612          * @throws MessagingException
    613          */
    614         public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
    615                 throws MessagingException {
    616             if (messages == null || messages.length == 0) {
    617                 return;
    618             }
    619             ArrayList<String> uids = new ArrayList<String>();
    620             for (Message message : messages) {
    621                 uids.add(message.getUid());
    622             }
    623             try {
    624                 indexUids(uids);
    625                 if (fp.contains(FetchProfile.Item.ENVELOPE)) {
    626                     // Note: We never pass the listener for the ENVELOPE call, because we're going
    627                     // to be calling the listener below in the per-message loop.
    628                     fetchEnvelope(messages, null);
    629                 }
    630             } catch (IOException ioe) {
    631                 mTransport.close();
    632                 if (Config.LOGD && Email.DEBUG) {
    633                     Log.d(Email.LOG_TAG, ioe.toString());
    634                 }
    635                 throw new MessagingException("fetch", ioe);
    636             }
    637             for (int i = 0, count = messages.length; i < count; i++) {
    638                 Message message = messages[i];
    639                 if (!(message instanceof Pop3Message)) {
    640                     throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message");
    641                 }
    642                 Pop3Message pop3Message = (Pop3Message)message;
    643                 try {
    644                     if (listener != null) {
    645                         listener.messageStarted(pop3Message.getUid(), i, count);
    646                     }
    647                     if (fp.contains(FetchProfile.Item.BODY)) {
    648                         fetchBody(pop3Message, -1);
    649                     }
    650                     else if (fp.contains(FetchProfile.Item.BODY_SANE)) {
    651                         /*
    652                          * To convert the suggested download size we take the size
    653                          * divided by the maximum line size (76).
    654                          */
    655                         fetchBody(pop3Message,
    656                                 FETCH_BODY_SANE_SUGGESTED_SIZE / 76);
    657                     }
    658                     else if (fp.contains(FetchProfile.Item.STRUCTURE)) {
    659                         /*
    660                          * If the user is requesting STRUCTURE we are required to set the body
    661                          * to null since we do not support the function.
    662                          */
    663                         pop3Message.setBody(null);
    664                     }
    665                     if (listener != null) {
    666                         listener.messageFinished(message, i, count);
    667                     }
    668                 } catch (IOException ioe) {
    669                     mTransport.close();
    670                     if (Config.LOGD && Email.DEBUG) {
    671                         Log.d(Email.LOG_TAG, ioe.toString());
    672                     }
    673                     throw new MessagingException("Unable to fetch message", ioe);
    674                 }
    675             }
    676         }
    677 
    678         private void fetchEnvelope(Message[] messages,
    679                 MessageRetrievalListener listener)  throws IOException, MessagingException {
    680             int unsizedMessages = 0;
    681             for (Message message : messages) {
    682                 if (message.getSize() == -1) {
    683                     unsizedMessages++;
    684                 }
    685             }
    686             if (unsizedMessages == 0) {
    687                 return;
    688             }
    689             if (unsizedMessages < 50 && mMessageCount > 5000) {
    690                 /*
    691                  * In extreme cases we'll do a command per message instead of a bulk request
    692                  * to hopefully save some time and bandwidth.
    693                  */
    694                 for (int i = 0, count = messages.length; i < count; i++) {
    695                     Message message = messages[i];
    696                     if (!(message instanceof Pop3Message)) {
    697                         throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message");
    698                     }
    699                     Pop3Message pop3Message = (Pop3Message)message;
    700                     if (listener != null) {
    701                         listener.messageStarted(pop3Message.getUid(), i, count);
    702                     }
    703                     String response = executeSimpleCommand(String.format("LIST %d",
    704                             mUidToMsgNumMap.get(pop3Message.getUid())));
    705                     try {
    706                         String[] listParts = response.split(" ");
    707                         int msgNum = Integer.parseInt(listParts[1]);
    708                         int msgSize = Integer.parseInt(listParts[2]);
    709                         pop3Message.setSize(msgSize);
    710                     } catch (NumberFormatException nfe) {
    711                         throw new IOException();
    712                     }
    713                     if (listener != null) {
    714                         listener.messageFinished(pop3Message, i, count);
    715                     }
    716                 }
    717             } else {
    718                 HashSet<String> msgUidIndex = new HashSet<String>();
    719                 for (Message message : messages) {
    720                     msgUidIndex.add(message.getUid());
    721                 }
    722                 int i = 0, count = messages.length;
    723                 String response = executeSimpleCommand("LIST");
    724                 while ((response = mTransport.readLine()) != null) {
    725                     if (response.equals(".")) {
    726                         break;
    727                     }
    728                     Pop3Message pop3Message = null;
    729                     int msgSize = 0;
    730                     try {
    731                         String[] listParts = response.split(" ");
    732                         int msgNum = Integer.parseInt(listParts[0]);
    733                         msgSize = Integer.parseInt(listParts[1]);
    734                         pop3Message = mMsgNumToMsgMap.get(msgNum);
    735                     } catch (NumberFormatException nfe) {
    736                         throw new IOException();
    737                     }
    738                     if (pop3Message != null && msgUidIndex.contains(pop3Message.getUid())) {
    739                         if (listener != null) {
    740                             listener.messageStarted(pop3Message.getUid(), i, count);
    741                         }
    742                         pop3Message.setSize(msgSize);
    743                         if (listener != null) {
    744                             listener.messageFinished(pop3Message, i, count);
    745                         }
    746                         i++;
    747                     }
    748                 }
    749             }
    750         }
    751 
    752         /**
    753          * Fetches the body of the given message, limiting the stored data
    754          * to the specified number of lines. If lines is -1 the entire message
    755          * is fetched. This is implemented with RETR for lines = -1 or TOP
    756          * for any other value. If the server does not support TOP it is
    757          * emulated with RETR and extra lines are thrown away.
    758          *
    759          * Note:  Some servers (e.g. live.com) don't support CAPA, but turn out to
    760          * support TOP after all.  For better performance on these servers, we'll always
    761          * probe TOP, and fall back to RETR when it's truly unsupported.
    762          *
    763          * @param message
    764          * @param lines
    765          */
    766         private void fetchBody(Pop3Message message, int lines)
    767                 throws IOException, MessagingException {
    768             String response = null;
    769             int messageId = mUidToMsgNumMap.get(message.getUid());
    770             if (lines == -1) {
    771                 // Fetch entire message
    772                 response = executeSimpleCommand(String.format("RETR %d", messageId));
    773             } else {
    774                 // Fetch partial message.  Try "TOP", and fall back to slower "RETR" if necessary
    775                 try {
    776                     response = executeSimpleCommand(String.format("TOP %d %d", messageId,  lines));
    777                 } catch (MessagingException me) {
    778                     response = executeSimpleCommand(String.format("RETR %d", messageId));
    779                 }
    780             }
    781             if (response != null)  {
    782                 try {
    783                     InputStream in = mTransport.getInputStream();
    784                     if (DEBUG_LOG_RAW_STREAM && Config.LOGD && Email.DEBUG) {
    785                         in = new LoggingInputStream(in);
    786                     }
    787                     message.parse(new Pop3ResponseInputStream(in));
    788                 }
    789                 catch (MessagingException me) {
    790                     /*
    791                      * If we're only downloading headers it's possible
    792                      * we'll get a broken MIME message which we're not
    793                      * real worried about. If we've downloaded the body
    794                      * and can't parse it we need to let the user know.
    795                      */
    796                     if (lines == -1) {
    797                         throw me;
    798                     }
    799                 }
    800             }
    801         }
    802 
    803         @Override
    804         public Flag[] getPermanentFlags() throws MessagingException {
    805             return PERMANENT_FLAGS;
    806         }
    807 
    808         public void appendMessages(Message[] messages) throws MessagingException {
    809         }
    810 
    811         public void delete(boolean recurse) throws MessagingException {
    812         }
    813 
    814         public Message[] expunge() throws MessagingException {
    815             return null;
    816         }
    817 
    818         public void setFlags(Message[] messages, Flag[] flags, boolean value)
    819                 throws MessagingException {
    820             if (!value || !Utility.arrayContains(flags, Flag.DELETED)) {
    821                 /*
    822                  * The only flagging we support is setting the Deleted flag.
    823                  */
    824                 return;
    825             }
    826             try {
    827                 for (Message message : messages) {
    828                     executeSimpleCommand(String.format("DELE %s",
    829                             mUidToMsgNumMap.get(message.getUid())));
    830                 }
    831             }
    832             catch (IOException ioe) {
    833                 mTransport.close();
    834                 if (Config.LOGD && Email.DEBUG) {
    835                     Log.d(Email.LOG_TAG, ioe.toString());
    836                 }
    837                 throw new MessagingException("setFlags()", ioe);
    838             }
    839         }
    840 
    841         @Override
    842         public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks)
    843                 throws MessagingException {
    844             throw new UnsupportedOperationException("copyMessages is not supported in POP3");
    845         }
    846 
    847 //        private boolean isRoundTripModeSuggested() {
    848 //            long roundTripMethodMs =
    849 //                (uncachedMessageCount * 2 * mLatencyMs);
    850 //            long bulkMethodMs =
    851 //                    (mMessageCount * 58) / (mThroughputKbS * 1024 / 8) * 1000;
    852 //        }
    853 
    854         private Pop3Capabilities getCapabilities() throws IOException, MessagingException {
    855             Pop3Capabilities capabilities = new Pop3Capabilities();
    856             try {
    857                 String response = executeSimpleCommand("CAPA");
    858                 while ((response = mTransport.readLine()) != null) {
    859                     if (response.equals(".")) {
    860                         break;
    861                     }
    862                     if (response.equalsIgnoreCase("STLS")){
    863                         capabilities.stls = true;
    864                     }
    865                     else if (response.equalsIgnoreCase("UIDL")) {
    866                         capabilities.uidl = true;
    867                     }
    868                     else if (response.equalsIgnoreCase("PIPELINING")) {
    869                         capabilities.pipelining = true;
    870                     }
    871                     else if (response.equalsIgnoreCase("USER")) {
    872                         capabilities.user = true;
    873                     }
    874                     else if (response.equalsIgnoreCase("TOP")) {
    875                         capabilities.top = true;
    876                     }
    877                 }
    878             }
    879             catch (MessagingException me) {
    880                 /*
    881                  * The server may not support the CAPA command, so we just eat this Exception
    882                  * and allow the empty capabilities object to be returned.
    883                  */
    884             }
    885             return capabilities;
    886         }
    887 
    888         /**
    889          * Send a single command and wait for a single line response.  Reopens the connection,
    890          * if it is closed.  Leaves the connection open.
    891          *
    892          * @param command The command string to send to the server.
    893          * @return Returns the response string from the server.
    894          */
    895         private String executeSimpleCommand(String command) throws IOException, MessagingException {
    896             return executeSensitiveCommand(command, null);
    897         }
    898 
    899         /**
    900          * Send a single command and wait for a single line response.  Reopens the connection,
    901          * if it is closed.  Leaves the connection open.
    902          *
    903          * @param command The command string to send to the server.
    904          * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
    905          * please pass a replacement string here (for logging).
    906          * @return Returns the response string from the server.
    907          */
    908         private String executeSensitiveCommand(String command, String sensitiveReplacement)
    909                 throws IOException, MessagingException {
    910             open(OpenMode.READ_WRITE, null);
    911 
    912             if (command != null) {
    913                 mTransport.writeLine(command, sensitiveReplacement);
    914             }
    915 
    916             String response = mTransport.readLine();
    917 
    918             if (response.length() > 1 && response.charAt(0) == '-') {
    919                 throw new MessagingException(response);
    920             }
    921 
    922             return response;
    923         }
    924 
    925         @Override
    926         public boolean equals(Object o) {
    927             if (o instanceof Pop3Folder) {
    928                 return ((Pop3Folder) o).mName.equals(mName);
    929             }
    930             return super.equals(o);
    931         }
    932 
    933         @Override
    934         // TODO this is deprecated, eventually discard
    935         public boolean isOpen() {
    936             return mTransport.isOpen();
    937         }
    938 
    939         @Override
    940         public Message createMessage(String uid) throws MessagingException {
    941             return new Pop3Message(uid, this);
    942         }
    943     }
    944 
    945     class Pop3Message extends MimeMessage {
    946         public Pop3Message(String uid, Pop3Folder folder) throws MessagingException {
    947             mUid = uid;
    948             mFolder = folder;
    949             mSize = -1;
    950         }
    951 
    952         public void setSize(int size) {
    953             mSize = size;
    954         }
    955 
    956         protected void parse(InputStream in) throws IOException, MessagingException {
    957             super.parse(in);
    958         }
    959 
    960         @Override
    961         public void setFlag(Flag flag, boolean set) throws MessagingException {
    962             super.setFlag(flag, set);
    963             mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
    964         }
    965     }
    966 
    967     /**
    968      * POP3 Capabilities as defined in RFC 2449.  This is not a complete list of CAPA
    969      * responses - just those that we use in this client.
    970      */
    971     class Pop3Capabilities {
    972         /** The STLS (start TLS) command is supported */
    973         public boolean stls;
    974         /** the TOP command (retrieve a partial message) is supported */
    975         public boolean top;
    976         /** USER and PASS login/auth commands are supported */
    977         public boolean user;
    978         /** the optional UIDL command is supported (unused) */
    979         public boolean uidl;
    980         /** the server is capable of accepting multiple commands at a time (unused) */
    981         public boolean pipelining;
    982 
    983         public String toString() {
    984             return String.format("STLS %b, TOP %b, USER %b, UIDL %b, PIPELINING %b",
    985                     stls,
    986                     top,
    987                     user,
    988                     uidl,
    989                     pipelining);
    990         }
    991     }
    992 
    993     // TODO figure out what is special about this and merge it into MailTransport
    994     class Pop3ResponseInputStream extends InputStream {
    995         InputStream mIn;
    996         boolean mStartOfLine = true;
    997         boolean mFinished;
    998 
    999         public Pop3ResponseInputStream(InputStream in) {
   1000             mIn = in;
   1001         }
   1002 
   1003         @Override
   1004         public int read() throws IOException {
   1005             if (mFinished) {
   1006                 return -1;
   1007             }
   1008             int d = mIn.read();
   1009             if (mStartOfLine && d == '.') {
   1010                 d = mIn.read();
   1011                 if (d == '\r') {
   1012                     mFinished = true;
   1013                     mIn.read();
   1014                     return -1;
   1015                 }
   1016             }
   1017 
   1018             mStartOfLine = (d == '\n');
   1019 
   1020             return d;
   1021         }
   1022     }
   1023 }
   1024