Home | History | Annotate | Download | only in store
      1 /*
      2  * Copyright (C) 2011 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.text.TextUtils;
     21 import android.text.format.DateUtils;
     22 import android.util.Base64DataException;
     23 
     24 import com.android.email.mail.store.ImapStore.ImapException;
     25 import com.android.email.mail.store.ImapStore.ImapMessage;
     26 import com.android.email.mail.store.imap.ImapConstants;
     27 import com.android.email.mail.store.imap.ImapElement;
     28 import com.android.email.mail.store.imap.ImapList;
     29 import com.android.email.mail.store.imap.ImapResponse;
     30 import com.android.email.mail.store.imap.ImapString;
     31 import com.android.email.mail.store.imap.ImapUtility;
     32 import com.android.email2.ui.MailActivityEmail;
     33 import com.android.emailcommon.Logging;
     34 import com.android.emailcommon.internet.BinaryTempFileBody;
     35 import com.android.emailcommon.internet.MimeBodyPart;
     36 import com.android.emailcommon.internet.MimeHeader;
     37 import com.android.emailcommon.internet.MimeMultipart;
     38 import com.android.emailcommon.internet.MimeUtility;
     39 import com.android.emailcommon.mail.AuthenticationFailedException;
     40 import com.android.emailcommon.mail.Body;
     41 import com.android.emailcommon.mail.FetchProfile;
     42 import com.android.emailcommon.mail.Flag;
     43 import com.android.emailcommon.mail.Folder;
     44 import com.android.emailcommon.mail.Message;
     45 import com.android.emailcommon.mail.MessagingException;
     46 import com.android.emailcommon.mail.Part;
     47 import com.android.emailcommon.provider.Mailbox;
     48 import com.android.emailcommon.service.SearchParams;
     49 import com.android.emailcommon.utility.CountingOutputStream;
     50 import com.android.emailcommon.utility.EOLConvertingOutputStream;
     51 import com.android.emailcommon.utility.Utility;
     52 import com.android.mail.utils.LogUtils;
     53 import com.google.common.annotations.VisibleForTesting;
     54 
     55 import java.io.IOException;
     56 import java.io.InputStream;
     57 import java.io.OutputStream;
     58 import java.text.SimpleDateFormat;
     59 import java.util.ArrayList;
     60 import java.util.Arrays;
     61 import java.util.Date;
     62 import java.util.Locale;
     63 import java.util.HashMap;
     64 import java.util.LinkedHashSet;
     65 import java.util.List;
     66 import java.util.TimeZone;
     67 
     68 class ImapFolder extends Folder {
     69     private final static Flag[] PERMANENT_FLAGS =
     70         { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED };
     71     private static final int COPY_BUFFER_SIZE = 16*1024;
     72 
     73     private final ImapStore mStore;
     74     private final String mName;
     75     private int mMessageCount = -1;
     76     private ImapConnection mConnection;
     77     private OpenMode mMode;
     78     private boolean mExists;
     79     /** The local mailbox associated with this remote folder */
     80     Mailbox mMailbox;
     81     /** A set of hashes that can be used to track dirtiness */
     82     Object mHash[];
     83 
     84     /*package*/ ImapFolder(ImapStore store, String name) {
     85         mStore = store;
     86         mName = name;
     87     }
     88 
     89     private void destroyResponses() {
     90         if (mConnection != null) {
     91             mConnection.destroyResponses();
     92         }
     93     }
     94 
     95     @Override
     96     public void open(OpenMode mode)
     97             throws MessagingException {
     98         try {
     99             if (isOpen()) {
    100                 if (mMode == mode) {
    101                     // Make sure the connection is valid.
    102                     // If it's not we'll close it down and continue on to get a new one.
    103                     try {
    104                         mConnection.executeSimpleCommand(ImapConstants.NOOP);
    105                         return;
    106 
    107                     } catch (IOException ioe) {
    108                         ioExceptionHandler(mConnection, ioe);
    109                     } finally {
    110                         destroyResponses();
    111                     }
    112                 } else {
    113                     // Return the connection to the pool, if exists.
    114                     close(false);
    115                 }
    116             }
    117             synchronized (this) {
    118                 mConnection = mStore.getConnection();
    119             }
    120             // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
    121             // $MDNSent)
    122             // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
    123             // NonJunk $MDNSent \*)] Flags permitted.
    124             // * 23 EXISTS
    125             // * 0 RECENT
    126             // * OK [UIDVALIDITY 1125022061] UIDs valid
    127             // * OK [UIDNEXT 57576] Predicted next UID
    128             // 2 OK [READ-WRITE] Select completed.
    129             try {
    130                 doSelect();
    131             } catch (IOException ioe) {
    132                 throw ioExceptionHandler(mConnection, ioe);
    133             } finally {
    134                 destroyResponses();
    135             }
    136         } catch (AuthenticationFailedException e) {
    137             // Don't cache this connection, so we're forced to try connecting/login again
    138             mConnection = null;
    139             close(false);
    140             throw e;
    141         } catch (MessagingException e) {
    142             mExists = false;
    143             close(false);
    144             throw e;
    145         }
    146     }
    147 
    148     @Override
    149     @VisibleForTesting
    150     public boolean isOpen() {
    151         return mExists && mConnection != null;
    152     }
    153 
    154     @Override
    155     public OpenMode getMode() {
    156         return mMode;
    157     }
    158 
    159     @Override
    160     public void close(boolean expunge) {
    161         // TODO implement expunge
    162         mMessageCount = -1;
    163         synchronized (this) {
    164             mStore.poolConnection(mConnection);
    165             mConnection = null;
    166         }
    167     }
    168 
    169     @Override
    170     public String getName() {
    171         return mName;
    172     }
    173 
    174     @Override
    175     public boolean exists() throws MessagingException {
    176         if (mExists) {
    177             return true;
    178         }
    179         /*
    180          * This method needs to operate in the unselected mode as well as the selected mode
    181          * so we must get the connection ourselves if it's not there. We are specifically
    182          * not calling checkOpen() since we don't care if the folder is open.
    183          */
    184         ImapConnection connection = null;
    185         synchronized(this) {
    186             if (mConnection == null) {
    187                 connection = mStore.getConnection();
    188             } else {
    189                 connection = mConnection;
    190             }
    191         }
    192         try {
    193             connection.executeSimpleCommand(String.format(Locale.US,
    194                     ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")",
    195                     ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
    196             mExists = true;
    197             return true;
    198 
    199         } catch (MessagingException me) {
    200             // Treat IOERROR messaging exception as IOException
    201             if (me.getExceptionType() == MessagingException.IOERROR) {
    202                 throw me;
    203             }
    204             return false;
    205 
    206         } catch (IOException ioe) {
    207             throw ioExceptionHandler(connection, ioe);
    208 
    209         } finally {
    210             connection.destroyResponses();
    211             if (mConnection == null) {
    212                 mStore.poolConnection(connection);
    213             }
    214         }
    215     }
    216 
    217     // IMAP supports folder creation
    218     @Override
    219     public boolean canCreate(FolderType type) {
    220         return true;
    221     }
    222 
    223     @Override
    224     public boolean create(FolderType type) throws MessagingException {
    225         /*
    226          * This method needs to operate in the unselected mode as well as the selected mode
    227          * so we must get the connection ourselves if it's not there. We are specifically
    228          * not calling checkOpen() since we don't care if the folder is open.
    229          */
    230         ImapConnection connection = null;
    231         synchronized(this) {
    232             if (mConnection == null) {
    233                 connection = mStore.getConnection();
    234             } else {
    235                 connection = mConnection;
    236             }
    237         }
    238         try {
    239             connection.executeSimpleCommand(String.format(Locale.US,
    240                     ImapConstants.CREATE + " \"%s\"",
    241                     ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
    242             return true;
    243 
    244         } catch (MessagingException me) {
    245             return false;
    246 
    247         } catch (IOException ioe) {
    248             throw ioExceptionHandler(connection, ioe);
    249 
    250         } finally {
    251             connection.destroyResponses();
    252             if (mConnection == null) {
    253                 mStore.poolConnection(connection);
    254             }
    255         }
    256     }
    257 
    258     @Override
    259     public void copyMessages(Message[] messages, Folder folder,
    260             MessageUpdateCallbacks callbacks) throws MessagingException {
    261         checkOpen();
    262         try {
    263             List<ImapResponse> responseList = mConnection.executeSimpleCommand(
    264                     String.format(Locale.US, ImapConstants.UID_COPY + " %s \"%s\"",
    265                             ImapStore.joinMessageUids(messages),
    266                             ImapStore.encodeFolderName(folder.getName(), mStore.mPathPrefix)));
    267             // Build a message map for faster UID matching
    268             HashMap<String, Message> messageMap = new HashMap<String, Message>();
    269             boolean handledUidPlus = false;
    270             for (Message m : messages) {
    271                 messageMap.put(m.getUid(), m);
    272             }
    273             // Process response to get the new UIDs
    274             for (ImapResponse response : responseList) {
    275                 // All "BAD" responses are bad. Only "NO", tagged responses are bad.
    276                 if (response.isBad() || (response.isNo() && response.isTagged())) {
    277                     String responseText = response.getStatusResponseTextOrEmpty().getString();
    278                     throw new MessagingException(responseText);
    279                 }
    280                 // Skip untagged responses; they're just status
    281                 if (!response.isTagged()) {
    282                     continue;
    283                 }
    284                 // No callback provided to report of UID changes; nothing more to do here
    285                 // NOTE: We check this here to catch any server errors
    286                 if (callbacks == null) {
    287                     continue;
    288                 }
    289                 ImapList copyResponse = response.getListOrEmpty(1);
    290                 String responseCode = copyResponse.getStringOrEmpty(0).getString();
    291                 if (ImapConstants.COPYUID.equals(responseCode)) {
    292                     handledUidPlus = true;
    293                     String origIdSet = copyResponse.getStringOrEmpty(2).getString();
    294                     String newIdSet = copyResponse.getStringOrEmpty(3).getString();
    295                     String[] origIdArray = ImapUtility.getImapSequenceValues(origIdSet);
    296                     String[] newIdArray = ImapUtility.getImapSequenceValues(newIdSet);
    297                     // There has to be a 1:1 mapping between old and new IDs
    298                     if (origIdArray.length != newIdArray.length) {
    299                         throw new MessagingException("Set length mis-match; orig IDs \"" +
    300                                 origIdSet + "\"  new IDs \"" + newIdSet + "\"");
    301                     }
    302                     for (int i = 0; i < origIdArray.length; i++) {
    303                         final String id = origIdArray[i];
    304                         final Message m = messageMap.get(id);
    305                         if (m != null) {
    306                             callbacks.onMessageUidChange(m, newIdArray[i]);
    307                         }
    308                     }
    309                 }
    310             }
    311             // If the server doesn't support UIDPLUS, try a different way to get the new UID(s)
    312             if (callbacks != null && !handledUidPlus) {
    313                 final ImapFolder newFolder = (ImapFolder)folder;
    314                 try {
    315                     // Temporarily select the destination folder
    316                     newFolder.open(OpenMode.READ_WRITE);
    317                     // Do the search(es) ...
    318                     for (Message m : messages) {
    319                         final String searchString =
    320                                 "HEADER Message-Id \"" + m.getMessageId() + "\"";
    321                         final String[] newIdArray = newFolder.searchForUids(searchString);
    322                         if (newIdArray.length == 1) {
    323                             callbacks.onMessageUidChange(m, newIdArray[0]);
    324                         }
    325                     }
    326                 } catch (MessagingException e) {
    327                     // Log, but, don't abort; failures here don't need to be propagated
    328                     LogUtils.d(Logging.LOG_TAG, "Failed to find message", e);
    329                 } finally {
    330                     newFolder.close(false);
    331                 }
    332                 // Re-select the original folder
    333                 doSelect();
    334             }
    335         } catch (IOException ioe) {
    336             throw ioExceptionHandler(mConnection, ioe);
    337         } finally {
    338             destroyResponses();
    339         }
    340     }
    341 
    342     @Override
    343     public int getMessageCount() {
    344         return mMessageCount;
    345     }
    346 
    347     @Override
    348     public int getUnreadMessageCount() throws MessagingException {
    349         checkOpen();
    350         try {
    351             int unreadMessageCount = 0;
    352             final List<ImapResponse> responses = mConnection.executeSimpleCommand(
    353                     String.format(Locale.US,
    354                             ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")",
    355                             ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
    356             // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292)
    357             for (ImapResponse response : responses) {
    358                 if (response.isDataResponse(0, ImapConstants.STATUS)) {
    359                     unreadMessageCount = response.getListOrEmpty(2)
    360                             .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero();
    361                 }
    362             }
    363             return unreadMessageCount;
    364         } catch (IOException ioe) {
    365             throw ioExceptionHandler(mConnection, ioe);
    366         } finally {
    367             destroyResponses();
    368         }
    369     }
    370 
    371     @Override
    372     public void delete(boolean recurse) {
    373         throw new Error("ImapStore.delete() not yet implemented");
    374     }
    375 
    376     String[] getSearchUids(List<ImapResponse> responses) {
    377         // S: * SEARCH 2 3 6
    378         final ArrayList<String> uids = new ArrayList<String>();
    379         for (ImapResponse response : responses) {
    380             if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
    381                 continue;
    382             }
    383             // Found SEARCH response data
    384             for (int i = 1; i < response.size(); i++) {
    385                 ImapString s = response.getStringOrEmpty(i);
    386                 if (s.isString()) {
    387                     uids.add(s.getString());
    388                 }
    389             }
    390         }
    391         return uids.toArray(Utility.EMPTY_STRINGS);
    392     }
    393 
    394     String[] searchForUids(String searchCriteria) throws MessagingException {
    395         return searchForUids(searchCriteria, true);
    396     }
    397 
    398     /**
    399      * I'm not a fan of having a parameter that determines whether to throw exceptions or
    400      * consume them, but getMessage() for a date range needs to differentiate between
    401      * a failure and just a legitimate empty result.
    402      * See b/11183568.
    403      * TODO:
    404      * Either figure out how to make getMessage() with a date range work without this
    405      * exception information, or make all users of searchForUids() handle the ImapException.
    406      * It's too late in the release cycle to add this risk right now.
    407      */
    408     @VisibleForTesting
    409     String[] searchForUids(String searchCriteria, boolean swallowException)
    410             throws MessagingException {
    411         checkOpen();
    412         try {
    413             try {
    414                 final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
    415                 final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
    416                 LogUtils.d(Logging.LOG_TAG, "searchForUids '" + searchCriteria + "' results: " +
    417                         result.length);
    418                 return result;
    419             } catch (ImapException me) {
    420                 LogUtils.d(Logging.LOG_TAG, me, "ImapException in search: " + searchCriteria);
    421                 if (swallowException) {
    422                     return Utility.EMPTY_STRINGS; // Not found
    423                 } else {
    424                     throw me;
    425                 }
    426             } catch (IOException ioe) {
    427                 LogUtils.d(Logging.LOG_TAG, ioe, "IOException in search: " + searchCriteria);
    428                 throw ioExceptionHandler(mConnection, ioe);
    429             }
    430         } finally {
    431             destroyResponses();
    432         }
    433     }
    434 
    435     @Override
    436     @VisibleForTesting
    437     public Message getMessage(String uid) throws MessagingException {
    438         checkOpen();
    439 
    440         final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
    441         for (int i = 0; i < uids.length; i++) {
    442             if (uids[i].equals(uid)) {
    443                 return new ImapMessage(uid, this);
    444             }
    445         }
    446         return null;
    447     }
    448 
    449     @VisibleForTesting
    450     protected static boolean isAsciiString(String str) {
    451         int len = str.length();
    452         for (int i = 0; i < len; i++) {
    453             char c = str.charAt(i);
    454             if (c >= 128) return false;
    455         }
    456         return true;
    457     }
    458 
    459     /**
    460      * Retrieve messages based on search parameters.  We search FROM, TO, CC, SUBJECT, and BODY
    461      * We send: SEARCH OR FROM "foo" (OR TO "foo" (OR CC "foo" (OR SUBJECT "foo" BODY "foo"))), but
    462      * with the additional CHARSET argument and sending "foo" as a literal (e.g. {3}<CRLF>foo}
    463      */
    464     @Override
    465     @VisibleForTesting
    466     public Message[] getMessages(SearchParams params, MessageRetrievalListener listener)
    467             throws MessagingException {
    468         List<String> commands = new ArrayList<String>();
    469         final String filter = params.mFilter;
    470         // All servers MUST accept US-ASCII, so we'll send this as the CHARSET unless we're really
    471         // dealing with a string that contains non-ascii characters
    472         String charset = "US-ASCII";
    473         if (!isAsciiString(filter)) {
    474             charset = "UTF-8";
    475         }
    476         // This is the length of the string in octets (bytes), formatted as a string literal {n}
    477         final String octetLength = "{" + filter.getBytes().length + "}";
    478         // Break the command up into pieces ending with the string literal length
    479         commands.add(ImapConstants.UID_SEARCH + " CHARSET " + charset + " OR FROM " + octetLength);
    480         commands.add(filter + " (OR TO " + octetLength);
    481         commands.add(filter + " (OR CC " + octetLength);
    482         commands.add(filter + " (OR SUBJECT " + octetLength);
    483         commands.add(filter + " BODY " + octetLength);
    484         commands.add(filter + ")))");
    485         return getMessagesInternal(complexSearchForUids(commands), listener);
    486     }
    487 
    488     /* package */ String[] complexSearchForUids(List<String> commands) throws MessagingException {
    489         checkOpen();
    490         try {
    491             try {
    492                 return getSearchUids(mConnection.executeComplexCommand(commands, false));
    493             } catch (ImapException e) {
    494                 return Utility.EMPTY_STRINGS; // not found;
    495             } catch (IOException ioe) {
    496                 throw ioExceptionHandler(mConnection, ioe);
    497             }
    498         } finally {
    499             destroyResponses();
    500         }
    501     }
    502 
    503     @Override
    504     @VisibleForTesting
    505     public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
    506             throws MessagingException {
    507         if (start < 1 || end < 1 || end < start) {
    508             throw new MessagingException(String.format("Invalid range: %d %d", start, end));
    509         }
    510         LogUtils.d(Logging.LOG_TAG, "getMessages number " + start + " - " + end);
    511         return getMessagesInternal(
    512                 searchForUids(String.format(Locale.US, "%d:%d NOT DELETED", start, end)), listener);
    513     }
    514 
    515     private String generateDateRangeCommand(final long startDate, final long endDate,
    516             boolean useQuotes)
    517             throws MessagingException {
    518         // Dates must be formatted like: 7-Feb-1994. Time info within a date is not
    519         // universally supported.
    520         // XXX can I limit the maximum number of results?
    521         final SimpleDateFormat formatter = new SimpleDateFormat("dd-LLL-yyyy", Locale.US);
    522         formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
    523         final String sinceDateStr = formatter.format(endDate);
    524 
    525         StringBuilder queryParam = new StringBuilder();
    526         queryParam.append( "1:* ");
    527         // If the caller requests a startDate of zero, then ignore the BEFORE parameter.
    528         // This makes sure that we can always query for the newest messages, even if our
    529         // time is different from the imap server's time.
    530         if (startDate != 0) {
    531             final String beforeDateStr = formatter.format(startDate);
    532             if (startDate < endDate) {
    533                 throw new MessagingException(String.format("Invalid date range: %s - %s",
    534                         sinceDateStr, beforeDateStr));
    535             }
    536             queryParam.append("BEFORE ");
    537             if (useQuotes) queryParam.append('\"');
    538             queryParam.append(beforeDateStr);
    539             if (useQuotes) queryParam.append('\"');
    540             queryParam.append(" ");
    541         }
    542         queryParam.append("SINCE ");
    543         if (useQuotes) queryParam.append('\"');
    544         queryParam.append(sinceDateStr);
    545         if (useQuotes) queryParam.append('\"');
    546 
    547         return queryParam.toString();
    548     }
    549 
    550     @Override
    551     @VisibleForTesting
    552     public Message[] getMessages(long startDate, long endDate, MessageRetrievalListener listener)
    553             throws MessagingException {
    554         String [] uids = null;
    555         String command = generateDateRangeCommand(startDate, endDate, false);
    556         LogUtils.d(Logging.LOG_TAG, "getMessages dateRange " + command.toString());
    557 
    558         try {
    559             uids = searchForUids(command.toString(), false);
    560         } catch (ImapException e) {
    561             // TODO: This is a last minute hack to make certain servers work. Some servers
    562             // demand that the date in the date range be surrounded by double quotes, other
    563             // servers won't accept that. So if we can an ImapException using one method,
    564             // try the other.
    565             // See b/11183568
    566             LogUtils.d(Logging.LOG_TAG, e, "query failed %s, trying alternate",
    567                     command.toString());
    568             command = generateDateRangeCommand(startDate, endDate, true);
    569             try {
    570                 uids = searchForUids(command, true);
    571             } catch (ImapException e2) {
    572                 LogUtils.w(Logging.LOG_TAG, e2, "query failed %s, fatal", command);
    573                 uids = null;
    574             }
    575         }
    576         return getMessagesInternal(uids, listener);
    577     }
    578 
    579     @Override
    580     @VisibleForTesting
    581     public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
    582             throws MessagingException {
    583         if (uids == null) {
    584             uids = searchForUids("1:* NOT DELETED");
    585         }
    586         return getMessagesInternal(uids, listener);
    587     }
    588 
    589     public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) {
    590         final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
    591         for (int i = 0; i < uids.length; i++) {
    592             final String uid = uids[i];
    593             final ImapMessage message = new ImapMessage(uid, this);
    594             messages.add(message);
    595             if (listener != null) {
    596                 listener.messageRetrieved(message);
    597             }
    598         }
    599         return messages.toArray(Message.EMPTY_ARRAY);
    600     }
    601 
    602     @Override
    603     public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
    604             throws MessagingException {
    605         try {
    606             fetchInternal(messages, fp, listener);
    607         } catch (RuntimeException e) { // Probably a parser error.
    608             LogUtils.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage());
    609             if (mConnection != null) {
    610                 mConnection.logLastDiscourse();
    611             }
    612             throw e;
    613         }
    614     }
    615 
    616     public void fetchInternal(Message[] messages, FetchProfile fp,
    617             MessageRetrievalListener listener) throws MessagingException {
    618         if (messages.length == 0) {
    619             return;
    620         }
    621         checkOpen();
    622         HashMap<String, Message> messageMap = new HashMap<String, Message>();
    623         for (Message m : messages) {
    624             messageMap.put(m.getUid(), m);
    625         }
    626 
    627         /*
    628          * Figure out what command we are going to run:
    629          * FLAGS     - UID FETCH (FLAGS)
    630          * ENVELOPE  - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
    631          *                            HEADER.FIELDS (date subject from content-type to cc)])
    632          * STRUCTURE - UID FETCH (BODYSTRUCTURE)
    633          * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
    634          * BODY      - UID FETCH (BODY.PEEK[])
    635          * Part      - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
    636          */
    637 
    638         final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
    639 
    640         fetchFields.add(ImapConstants.UID);
    641         if (fp.contains(FetchProfile.Item.FLAGS)) {
    642             fetchFields.add(ImapConstants.FLAGS);
    643         }
    644         if (fp.contains(FetchProfile.Item.ENVELOPE)) {
    645             fetchFields.add(ImapConstants.INTERNALDATE);
    646             fetchFields.add(ImapConstants.RFC822_SIZE);
    647             fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
    648         }
    649         if (fp.contains(FetchProfile.Item.STRUCTURE)) {
    650             fetchFields.add(ImapConstants.BODYSTRUCTURE);
    651         }
    652 
    653         if (fp.contains(FetchProfile.Item.BODY_SANE)) {
    654             fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
    655         }
    656         if (fp.contains(FetchProfile.Item.BODY)) {
    657             fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
    658         }
    659 
    660         // TODO Why are we only fetching the first part given?
    661         final Part fetchPart = fp.getFirstPart();
    662         if (fetchPart != null) {
    663             final String[] partIds =
    664                     fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
    665             // TODO Why can a single part have more than one Id? And why should we only fetch
    666             // the first id if there are more than one?
    667             if (partIds != null) {
    668                 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE
    669                         + "[" + partIds[0] + "]");
    670             }
    671         }
    672 
    673         try {
    674             mConnection.sendCommand(String.format(Locale.US,
    675                     ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages),
    676                     Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
    677                     ), false);
    678             ImapResponse response;
    679             do {
    680                 response = null;
    681                 try {
    682                     response = mConnection.readResponse();
    683 
    684                     if (!response.isDataResponse(1, ImapConstants.FETCH)) {
    685                         continue; // Ignore
    686                     }
    687                     final ImapList fetchList = response.getListOrEmpty(2);
    688                     final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID)
    689                             .getString();
    690                     if (TextUtils.isEmpty(uid)) continue;
    691 
    692                     ImapMessage message = (ImapMessage) messageMap.get(uid);
    693                     if (message == null) continue;
    694 
    695                     if (fp.contains(FetchProfile.Item.FLAGS)) {
    696                         final ImapList flags =
    697                             fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
    698                         for (int i = 0, count = flags.size(); i < count; i++) {
    699                             final ImapString flag = flags.getStringOrEmpty(i);
    700                             if (flag.is(ImapConstants.FLAG_DELETED)) {
    701                                 message.setFlagInternal(Flag.DELETED, true);
    702                             } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
    703                                 message.setFlagInternal(Flag.ANSWERED, true);
    704                             } else if (flag.is(ImapConstants.FLAG_SEEN)) {
    705                                 message.setFlagInternal(Flag.SEEN, true);
    706                             } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
    707                                 message.setFlagInternal(Flag.FLAGGED, true);
    708                             }
    709                         }
    710                     }
    711                     if (fp.contains(FetchProfile.Item.ENVELOPE)) {
    712                         final Date internalDate = fetchList.getKeyedStringOrEmpty(
    713                                 ImapConstants.INTERNALDATE).getDateOrNull();
    714                         final int size = fetchList.getKeyedStringOrEmpty(
    715                                 ImapConstants.RFC822_SIZE).getNumberOrZero();
    716                         final String header = fetchList.getKeyedStringOrEmpty(
    717                                 ImapConstants.BODY_BRACKET_HEADER, true).getString();
    718 
    719                         message.setInternalDate(internalDate);
    720                         message.setSize(size);
    721                         message.parse(Utility.streamFromAsciiString(header));
    722                     }
    723                     if (fp.contains(FetchProfile.Item.STRUCTURE)) {
    724                         ImapList bs = fetchList.getKeyedListOrEmpty(
    725                                 ImapConstants.BODYSTRUCTURE);
    726                         if (!bs.isEmpty()) {
    727                             try {
    728                                 parseBodyStructure(bs, message, ImapConstants.TEXT);
    729                             } catch (MessagingException e) {
    730                                 if (Logging.LOGD) {
    731                                     LogUtils.v(Logging.LOG_TAG, e, "Error handling message");
    732                                 }
    733                                 message.setBody(null);
    734                             }
    735                         }
    736                     }
    737                     if (fp.contains(FetchProfile.Item.BODY)
    738                             || fp.contains(FetchProfile.Item.BODY_SANE)) {
    739                         // Body is keyed by "BODY[]...".
    740                         // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
    741                         // TODO Should we accept "RFC822" as well??
    742                         ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
    743                         InputStream bodyStream = body.getAsStream();
    744                         message.parse(bodyStream);
    745                     }
    746                     if (fetchPart != null) {
    747                         InputStream bodyStream =
    748                                 fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
    749                         String encodings[] = fetchPart.getHeader(
    750                                 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
    751 
    752                         String contentTransferEncoding = null;
    753                         if (encodings != null && encodings.length > 0) {
    754                             contentTransferEncoding = encodings[0];
    755                         } else {
    756                             // According to http://tools.ietf.org/html/rfc2045#section-6.1
    757                             // "7bit" is the default.
    758                             contentTransferEncoding = "7bit";
    759                         }
    760 
    761                         try {
    762                             // TODO Don't create 2 temp files.
    763                             // decodeBody creates BinaryTempFileBody, but we could avoid this
    764                             // if we implement ImapStringBody.
    765                             // (We'll need to share a temp file.  Protect it with a ref-count.)
    766                             fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding,
    767                                     fetchPart.getSize(), listener));
    768                         } catch(Exception e) {
    769                             // TODO: Figure out what kinds of exceptions might actually be thrown
    770                             // from here. This blanket catch-all is because we're not sure what to
    771                             // do if we don't have a contentTransferEncoding, and we don't have
    772                             // time to figure out what exceptions might be thrown.
    773                             LogUtils.e(Logging.LOG_TAG, "Error fetching body %s", e);
    774                         }
    775                     }
    776 
    777                     if (listener != null) {
    778                         listener.messageRetrieved(message);
    779                     }
    780                 } finally {
    781                     destroyResponses();
    782                 }
    783             } while (!response.isTagged());
    784         } catch (IOException ioe) {
    785             throw ioExceptionHandler(mConnection, ioe);
    786         }
    787     }
    788 
    789     /**
    790      * Removes any content transfer encoding from the stream and returns a Body.
    791      * This code is taken/condensed from MimeUtility.decodeBody
    792      */
    793     private static Body decodeBody(InputStream in, String contentTransferEncoding, int size,
    794             MessageRetrievalListener listener) throws IOException {
    795         // Get a properly wrapped input stream
    796         in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
    797         BinaryTempFileBody tempBody = new BinaryTempFileBody();
    798         OutputStream out = tempBody.getOutputStream();
    799         try {
    800             byte[] buffer = new byte[COPY_BUFFER_SIZE];
    801             int n = 0;
    802             int count = 0;
    803             while (-1 != (n = in.read(buffer))) {
    804                 out.write(buffer, 0, n);
    805                 count += n;
    806                 if (listener != null) {
    807                     if (size == 0) {
    808                         // We don't know how big the file is, so just fake it.
    809                         listener.loadAttachmentProgress((int)Math.ceil(100 * (1-1.0/count)));
    810                     } else {
    811                         listener.loadAttachmentProgress(count * 100 / size);
    812                     }
    813                 }
    814             }
    815         } catch (Base64DataException bde) {
    816             String warning = "\n\n" + MailActivityEmail.getMessageDecodeErrorString();
    817             out.write(warning.getBytes());
    818         } finally {
    819             out.close();
    820         }
    821         return tempBody;
    822     }
    823 
    824     @Override
    825     public Flag[] getPermanentFlags() {
    826         return PERMANENT_FLAGS;
    827     }
    828 
    829     /**
    830      * Handle any untagged responses that the caller doesn't care to handle themselves.
    831      * @param responses
    832      */
    833     private void handleUntaggedResponses(List<ImapResponse> responses) {
    834         for (ImapResponse response : responses) {
    835             handleUntaggedResponse(response);
    836         }
    837     }
    838 
    839     /**
    840      * Handle an untagged response that the caller doesn't care to handle themselves.
    841      * @param response
    842      */
    843     private void handleUntaggedResponse(ImapResponse response) {
    844         if (response.isDataResponse(1, ImapConstants.EXISTS)) {
    845             mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
    846         }
    847     }
    848 
    849     private static void parseBodyStructure(ImapList bs, Part part, String id)
    850             throws MessagingException {
    851         if (bs.getElementOrNone(0).isList()) {
    852             /*
    853              * This is a multipart/*
    854              */
    855             MimeMultipart mp = new MimeMultipart();
    856             for (int i = 0, count = bs.size(); i < count; i++) {
    857                 ImapElement e = bs.getElementOrNone(i);
    858                 if (e.isList()) {
    859                     /*
    860                      * For each part in the message we're going to add a new BodyPart and parse
    861                      * into it.
    862                      */
    863                     MimeBodyPart bp = new MimeBodyPart();
    864                     if (id.equals(ImapConstants.TEXT)) {
    865                         parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
    866 
    867                     } else {
    868                         parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
    869                     }
    870                     mp.addBodyPart(bp);
    871 
    872                 } else {
    873                     if (e.isString()) {
    874                         mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
    875                     }
    876                     break; // Ignore the rest of the list.
    877                 }
    878             }
    879             part.setBody(mp);
    880         } else {
    881             /*
    882              * This is a body. We need to add as much information as we can find out about
    883              * it to the Part.
    884              */
    885 
    886             /*
    887              body type
    888              body subtype
    889              body parameter parenthesized list
    890              body id
    891              body description
    892              body encoding
    893              body size
    894              */
    895 
    896             final ImapString type = bs.getStringOrEmpty(0);
    897             final ImapString subType = bs.getStringOrEmpty(1);
    898             final String mimeType =
    899                     (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
    900 
    901             final ImapList bodyParams = bs.getListOrEmpty(2);
    902             final ImapString cid = bs.getStringOrEmpty(3);
    903             final ImapString encoding = bs.getStringOrEmpty(5);
    904             final int size = bs.getStringOrEmpty(6).getNumberOrZero();
    905 
    906             if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
    907                 // A body type of type MESSAGE and subtype RFC822
    908                 // contains, immediately after the basic fields, the
    909                 // envelope structure, body structure, and size in
    910                 // text lines of the encapsulated message.
    911                 // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
    912                 //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
    913                 /*
    914                  * This will be caught by fetch and handled appropriately.
    915                  */
    916                 throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822
    917                         + " not yet supported.");
    918             }
    919 
    920             /*
    921              * Set the content type with as much information as we know right now.
    922              */
    923             final StringBuilder contentType = new StringBuilder(mimeType);
    924 
    925             /*
    926              * If there are body params we might be able to get some more information out
    927              * of them.
    928              */
    929             for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
    930 
    931                 // TODO We need to convert " into %22, but
    932                 // because MimeUtility.getHeaderParameter doesn't recognize it,
    933                 // we can't fix it for now.
    934                 contentType.append(String.format(";\n %s=\"%s\"",
    935                         bodyParams.getStringOrEmpty(i - 1).getString(),
    936                         bodyParams.getStringOrEmpty(i).getString()));
    937             }
    938 
    939             part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
    940 
    941             // Extension items
    942             final ImapList bodyDisposition;
    943 
    944             if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
    945                 // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
    946                 // So, if it's not a list, use 10th element.
    947                 // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
    948                 bodyDisposition = bs.getListOrEmpty(9);
    949             } else {
    950                 bodyDisposition = bs.getListOrEmpty(8);
    951             }
    952 
    953             final StringBuilder contentDisposition = new StringBuilder();
    954 
    955             if (bodyDisposition.size() > 0) {
    956                 final String bodyDisposition0Str =
    957                         bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
    958                 if (!TextUtils.isEmpty(bodyDisposition0Str)) {
    959                     contentDisposition.append(bodyDisposition0Str);
    960                 }
    961 
    962                 final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
    963                 if (!bodyDispositionParams.isEmpty()) {
    964                     /*
    965                      * If there is body disposition information we can pull some more
    966                      * information about the attachment out.
    967                      */
    968                     for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
    969 
    970                         // TODO We need to convert " into %22.  See above.
    971                         contentDisposition.append(String.format(Locale.US, ";\n %s=\"%s\"",
    972                                 bodyDispositionParams.getStringOrEmpty(i - 1)
    973                                         .getString().toLowerCase(Locale.US),
    974                                 bodyDispositionParams.getStringOrEmpty(i).getString()));
    975                     }
    976                 }
    977             }
    978 
    979             if ((size > 0)
    980                     && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size")
    981                             == null)) {
    982                 contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
    983             }
    984 
    985             if (contentDisposition.length() > 0) {
    986                 /*
    987                  * Set the content disposition containing at least the size. Attachment
    988                  * handling code will use this down the road.
    989                  */
    990                 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
    991                         contentDisposition.toString());
    992             }
    993 
    994             /*
    995              * Set the Content-Transfer-Encoding header. Attachment code will use this
    996              * to parse the body.
    997              */
    998             if (!encoding.isEmpty()) {
    999                 part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
   1000                         encoding.getString());
   1001             }
   1002 
   1003             /*
   1004              * Set the Content-ID header.
   1005              */
   1006             if (!cid.isEmpty()) {
   1007                 part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
   1008             }
   1009 
   1010             if (size > 0) {
   1011                 if (part instanceof ImapMessage) {
   1012                     ((ImapMessage) part).setSize(size);
   1013                 } else if (part instanceof MimeBodyPart) {
   1014                     ((MimeBodyPart) part).setSize(size);
   1015                 } else {
   1016                     throw new MessagingException("Unknown part type " + part.toString());
   1017                 }
   1018             }
   1019             part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
   1020         }
   1021 
   1022     }
   1023 
   1024     /**
   1025      * Appends the given messages to the selected folder. This implementation also determines
   1026      * the new UID of the given message on the IMAP server and sets the Message's UID to the
   1027      * new server UID.
   1028      */
   1029     @Override
   1030     public void appendMessages(Message[] messages) throws MessagingException {
   1031         checkOpen();
   1032         try {
   1033             for (Message message : messages) {
   1034                 // Create output count
   1035                 CountingOutputStream out = new CountingOutputStream();
   1036                 EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out);
   1037                 message.writeTo(eolOut);
   1038                 eolOut.flush();
   1039                 // Create flag list (most often this will be "\SEEN")
   1040                 String flagList = "";
   1041                 Flag[] flags = message.getFlags();
   1042                 if (flags.length > 0) {
   1043                     StringBuilder sb = new StringBuilder();
   1044                     for (int i = 0, count = flags.length; i < count; i++) {
   1045                         Flag flag = flags[i];
   1046                         if (flag == Flag.SEEN) {
   1047                             sb.append(" " + ImapConstants.FLAG_SEEN);
   1048                         } else if (flag == Flag.FLAGGED) {
   1049                             sb.append(" " + ImapConstants.FLAG_FLAGGED);
   1050                         }
   1051                     }
   1052                     if (sb.length() > 0) {
   1053                         flagList = sb.substring(1);
   1054                     }
   1055                 }
   1056 
   1057                 mConnection.sendCommand(
   1058                         String.format(Locale.US, ImapConstants.APPEND + " \"%s\" (%s) {%d}",
   1059                                 ImapStore.encodeFolderName(mName, mStore.mPathPrefix),
   1060                                 flagList,
   1061                                 out.getCount()), false);
   1062                 ImapResponse response;
   1063                 do {
   1064                     response = mConnection.readResponse();
   1065                     if (response.isContinuationRequest()) {
   1066                         eolOut = new EOLConvertingOutputStream(
   1067                                 mConnection.mTransport.getOutputStream());
   1068                         message.writeTo(eolOut);
   1069                         eolOut.write('\r');
   1070                         eolOut.write('\n');
   1071                         eolOut.flush();
   1072                     } else if (!response.isTagged()) {
   1073                         handleUntaggedResponse(response);
   1074                     }
   1075                 } while (!response.isTagged());
   1076 
   1077                 // TODO Why not check the response?
   1078 
   1079                 /*
   1080                  * Try to recover the UID of the message from an APPENDUID response.
   1081                  * e.g. 11 OK [APPENDUID 2 238268] APPEND completed
   1082                  */
   1083                 final ImapList appendList = response.getListOrEmpty(1);
   1084                 if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) {
   1085                     String serverUid = appendList.getStringOrEmpty(2).getString();
   1086                     if (!TextUtils.isEmpty(serverUid)) {
   1087                         message.setUid(serverUid);
   1088                         continue;
   1089                     }
   1090                 }
   1091 
   1092                 /*
   1093                  * Try to find the UID of the message we just appended using the
   1094                  * Message-ID header.  If there are more than one response, take the
   1095                  * last one, as it's most likely the newest (the one we just uploaded).
   1096                  */
   1097                 final String messageId = message.getMessageId();
   1098                 if (messageId == null || messageId.length() == 0) {
   1099                     continue;
   1100                 }
   1101                 // Most servers don't care about parenthesis in the search query [and, some
   1102                 // fail to work if they are used]
   1103                 String[] uids = searchForUids(
   1104                         String.format(Locale.US, "HEADER MESSAGE-ID %s", messageId));
   1105                 if (uids.length > 0) {
   1106                     message.setUid(uids[0]);
   1107                 }
   1108                 // However, there's at least one server [AOL] that fails to work unless there
   1109                 // are parenthesis, so, try this as a last resort
   1110                 uids = searchForUids(String.format(Locale.US, "(HEADER MESSAGE-ID %s)", messageId));
   1111                 if (uids.length > 0) {
   1112                     message.setUid(uids[0]);
   1113                 }
   1114             }
   1115         } catch (IOException ioe) {
   1116             throw ioExceptionHandler(mConnection, ioe);
   1117         } finally {
   1118             destroyResponses();
   1119         }
   1120     }
   1121 
   1122     @Override
   1123     public Message[] expunge() throws MessagingException {
   1124         checkOpen();
   1125         try {
   1126             handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
   1127         } catch (IOException ioe) {
   1128             throw ioExceptionHandler(mConnection, ioe);
   1129         } finally {
   1130             destroyResponses();
   1131         }
   1132         return null;
   1133     }
   1134 
   1135     @Override
   1136     public void setFlags(Message[] messages, Flag[] flags, boolean value)
   1137             throws MessagingException {
   1138         checkOpen();
   1139 
   1140         String allFlags = "";
   1141         if (flags.length > 0) {
   1142             StringBuilder flagList = new StringBuilder();
   1143             for (int i = 0, count = flags.length; i < count; i++) {
   1144                 Flag flag = flags[i];
   1145                 if (flag == Flag.SEEN) {
   1146                     flagList.append(" " + ImapConstants.FLAG_SEEN);
   1147                 } else if (flag == Flag.DELETED) {
   1148                     flagList.append(" " + ImapConstants.FLAG_DELETED);
   1149                 } else if (flag == Flag.FLAGGED) {
   1150                     flagList.append(" " + ImapConstants.FLAG_FLAGGED);
   1151                 } else if (flag == Flag.ANSWERED) {
   1152                     flagList.append(" " + ImapConstants.FLAG_ANSWERED);
   1153                 }
   1154             }
   1155             allFlags = flagList.substring(1);
   1156         }
   1157         try {
   1158             mConnection.executeSimpleCommand(String.format(Locale.US,
   1159                     ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
   1160                     ImapStore.joinMessageUids(messages),
   1161                     value ? "+" : "-",
   1162                     allFlags));
   1163 
   1164         } catch (IOException ioe) {
   1165             throw ioExceptionHandler(mConnection, ioe);
   1166         } finally {
   1167             destroyResponses();
   1168         }
   1169     }
   1170 
   1171     /**
   1172      * Persists this folder. We will always perform the proper database operation (e.g.
   1173      * 'save' or 'update'). As an optimization, if a folder has not been modified, no
   1174      * database operations are performed.
   1175      */
   1176     void save(Context context) {
   1177         final Mailbox mailbox = mMailbox;
   1178         if (!mailbox.isSaved()) {
   1179             mailbox.save(context);
   1180             mHash = mailbox.getHashes();
   1181         } else {
   1182             Object[] hash = mailbox.getHashes();
   1183             if (!Arrays.equals(mHash, hash)) {
   1184                 mailbox.update(context, mailbox.toContentValues());
   1185                 mHash = hash;  // Save updated hash
   1186             }
   1187         }
   1188     }
   1189 
   1190     /**
   1191      * Selects the folder for use. Before performing any operations on this folder, it
   1192      * must be selected.
   1193      */
   1194     private void doSelect() throws IOException, MessagingException {
   1195         final List<ImapResponse> responses = mConnection.executeSimpleCommand(
   1196                 String.format(Locale.US, ImapConstants.SELECT + " \"%s\"",
   1197                         ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
   1198 
   1199         // Assume the folder is opened read-write; unless we are notified otherwise
   1200         mMode = OpenMode.READ_WRITE;
   1201         int messageCount = -1;
   1202         for (ImapResponse response : responses) {
   1203             if (response.isDataResponse(1, ImapConstants.EXISTS)) {
   1204                 messageCount = response.getStringOrEmpty(0).getNumberOrZero();
   1205             } else if (response.isOk()) {
   1206                 final ImapString responseCode = response.getResponseCodeOrEmpty();
   1207                 if (responseCode.is(ImapConstants.READ_ONLY)) {
   1208                     mMode = OpenMode.READ_ONLY;
   1209                 } else if (responseCode.is(ImapConstants.READ_WRITE)) {
   1210                     mMode = OpenMode.READ_WRITE;
   1211                 }
   1212             } else if (response.isTagged()) { // Not OK
   1213                 throw new MessagingException("Can't open mailbox: "
   1214                         + response.getStatusResponseTextOrEmpty());
   1215             }
   1216         }
   1217         if (messageCount == -1) {
   1218             throw new MessagingException("Did not find message count during select");
   1219         }
   1220         mMessageCount = messageCount;
   1221         mExists = true;
   1222     }
   1223 
   1224     private void checkOpen() throws MessagingException {
   1225         if (!isOpen()) {
   1226             throw new MessagingException("Folder " + mName + " is not open.");
   1227         }
   1228     }
   1229 
   1230     private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
   1231         if (MailActivityEmail.DEBUG) {
   1232             LogUtils.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
   1233         }
   1234         connection.close();
   1235         if (connection == mConnection) {
   1236             mConnection = null; // To prevent close() from returning the connection to the pool.
   1237             close(false);
   1238         }
   1239         return new MessagingException("IO Error", ioe);
   1240     }
   1241 
   1242     @Override
   1243     public boolean equals(Object o) {
   1244         if (o instanceof ImapFolder) {
   1245             return ((ImapFolder)o).mName.equals(mName);
   1246         }
   1247         return super.equals(o);
   1248     }
   1249 
   1250     @Override
   1251     public Message createMessage(String uid) {
   1252         return new ImapMessage(uid, this);
   1253     }
   1254 }
   1255