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