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