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