Home | History | Annotate | Download | only in store
      1 /*
      2  * Copyright (C) 2015 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 package com.android.voicemail.impl.mail.store;
     17 
     18 import android.content.Context;
     19 import android.support.annotation.Nullable;
     20 import android.support.annotation.VisibleForTesting;
     21 import android.text.TextUtils;
     22 import android.util.ArrayMap;
     23 import android.util.Base64DataException;
     24 import com.android.voicemail.impl.OmtpEvents;
     25 import com.android.voicemail.impl.VvmLog;
     26 import com.android.voicemail.impl.mail.AuthenticationFailedException;
     27 import com.android.voicemail.impl.mail.Body;
     28 import com.android.voicemail.impl.mail.FetchProfile;
     29 import com.android.voicemail.impl.mail.Flag;
     30 import com.android.voicemail.impl.mail.Message;
     31 import com.android.voicemail.impl.mail.MessagingException;
     32 import com.android.voicemail.impl.mail.Part;
     33 import com.android.voicemail.impl.mail.internet.BinaryTempFileBody;
     34 import com.android.voicemail.impl.mail.internet.MimeBodyPart;
     35 import com.android.voicemail.impl.mail.internet.MimeHeader;
     36 import com.android.voicemail.impl.mail.internet.MimeMultipart;
     37 import com.android.voicemail.impl.mail.internet.MimeUtility;
     38 import com.android.voicemail.impl.mail.store.ImapStore.ImapException;
     39 import com.android.voicemail.impl.mail.store.ImapStore.ImapMessage;
     40 import com.android.voicemail.impl.mail.store.imap.ImapConstants;
     41 import com.android.voicemail.impl.mail.store.imap.ImapElement;
     42 import com.android.voicemail.impl.mail.store.imap.ImapList;
     43 import com.android.voicemail.impl.mail.store.imap.ImapResponse;
     44 import com.android.voicemail.impl.mail.store.imap.ImapString;
     45 import com.android.voicemail.impl.mail.utils.Utility;
     46 import java.io.IOException;
     47 import java.io.InputStream;
     48 import java.io.OutputStream;
     49 import java.util.ArrayList;
     50 import java.util.Date;
     51 import java.util.LinkedHashSet;
     52 import java.util.List;
     53 import java.util.Locale;
     54 
     55 public class ImapFolder {
     56   private static final String TAG = "ImapFolder";
     57   private static final String[] PERMANENT_FLAGS = {
     58     Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED
     59   };
     60   private static final int COPY_BUFFER_SIZE = 16 * 1024;
     61 
     62   private final ImapStore mStore;
     63   private final String mName;
     64   private int mMessageCount = -1;
     65   private ImapConnection mConnection;
     66   private String mMode;
     67   private boolean mExists;
     68   /** A set of hashes that can be used to track dirtiness */
     69   Object[] mHash;
     70 
     71   public static final String MODE_READ_ONLY = "mode_read_only";
     72   public static final String MODE_READ_WRITE = "mode_read_write";
     73 
     74   public ImapFolder(ImapStore store, String name) {
     75     mStore = store;
     76     mName = name;
     77   }
     78 
     79   /** Callback for each message retrieval. */
     80   public interface MessageRetrievalListener {
     81     public void messageRetrieved(Message message);
     82   }
     83 
     84   private void destroyResponses() {
     85     if (mConnection != null) {
     86       mConnection.destroyResponses();
     87     }
     88   }
     89 
     90   public void open(String mode) throws MessagingException {
     91     try {
     92       if (isOpen()) {
     93         throw new AssertionError("Duplicated open on ImapFolder");
     94       }
     95       synchronized (this) {
     96         mConnection = mStore.getConnection();
     97       }
     98       // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
     99       // $MDNSent)
    100       // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
    101       // NonJunk $MDNSent \*)] Flags permitted.
    102       // * 23 EXISTS
    103       // * 0 RECENT
    104       // * OK [UIDVALIDITY 1125022061] UIDs valid
    105       // * OK [UIDNEXT 57576] Predicted next UID
    106       // 2 OK [READ-WRITE] Select completed.
    107       try {
    108         doSelect();
    109       } catch (IOException ioe) {
    110         throw ioExceptionHandler(mConnection, ioe);
    111       } finally {
    112         destroyResponses();
    113       }
    114     } catch (AuthenticationFailedException e) {
    115       // Don't cache this connection, so we're forced to try connecting/login again
    116       mConnection = null;
    117       close(false);
    118       throw e;
    119     } catch (MessagingException e) {
    120       mExists = false;
    121       close(false);
    122       throw e;
    123     }
    124   }
    125 
    126   public boolean isOpen() {
    127     return mExists && mConnection != null;
    128   }
    129 
    130   public String getMode() {
    131     return mMode;
    132   }
    133 
    134   public void close(boolean expunge) {
    135     if (expunge) {
    136       try {
    137         expunge();
    138       } catch (MessagingException e) {
    139         VvmLog.e(TAG, "Messaging Exception", e);
    140       }
    141     }
    142     mMessageCount = -1;
    143     synchronized (this) {
    144       mConnection = null;
    145     }
    146   }
    147 
    148   public int getMessageCount() {
    149     return mMessageCount;
    150   }
    151 
    152   String[] getSearchUids(List<ImapResponse> responses) {
    153     // S: * SEARCH 2 3 6
    154     final ArrayList<String> uids = new ArrayList<String>();
    155     for (ImapResponse response : responses) {
    156       if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
    157         continue;
    158       }
    159       // Found SEARCH response data
    160       for (int i = 1; i < response.size(); i++) {
    161         ImapString s = response.getStringOrEmpty(i);
    162         if (s.isString()) {
    163           uids.add(s.getString());
    164         }
    165       }
    166     }
    167     return uids.toArray(Utility.EMPTY_STRINGS);
    168   }
    169 
    170   @VisibleForTesting
    171   String[] searchForUids(String searchCriteria) throws MessagingException {
    172     checkOpen();
    173     try {
    174       try {
    175         final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
    176         final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
    177         VvmLog.d(TAG, "searchForUids '" + searchCriteria + "' results: " + result.length);
    178         return result;
    179       } catch (ImapException me) {
    180         VvmLog.d(TAG, "ImapException in search: " + searchCriteria, me);
    181         return Utility.EMPTY_STRINGS; // Not found
    182       } catch (IOException ioe) {
    183         VvmLog.d(TAG, "IOException in search: " + searchCriteria, ioe);
    184         mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
    185         throw ioExceptionHandler(mConnection, ioe);
    186       }
    187     } finally {
    188       destroyResponses();
    189     }
    190   }
    191 
    192   @Nullable
    193   public Message getMessage(String uid) throws MessagingException {
    194     checkOpen();
    195 
    196     final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
    197     for (int i = 0; i < uids.length; i++) {
    198       if (uids[i].equals(uid)) {
    199         return new ImapMessage(uid, this);
    200       }
    201     }
    202     VvmLog.e(TAG, "UID " + uid + " not found on server");
    203     return null;
    204   }
    205 
    206   @VisibleForTesting
    207   protected static boolean isAsciiString(String str) {
    208     int len = str.length();
    209     for (int i = 0; i < len; i++) {
    210       char c = str.charAt(i);
    211       if (c >= 128) return false;
    212     }
    213     return true;
    214   }
    215 
    216   public Message[] getMessages(String[] uids) throws MessagingException {
    217     if (uids == null) {
    218       uids = searchForUids("1:* NOT DELETED");
    219     }
    220     return getMessagesInternal(uids);
    221   }
    222 
    223   public Message[] getMessagesInternal(String[] uids) {
    224     final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
    225     for (int i = 0; i < uids.length; i++) {
    226       final String uid = uids[i];
    227       final ImapMessage message = new ImapMessage(uid, this);
    228       messages.add(message);
    229     }
    230     return messages.toArray(Message.EMPTY_ARRAY);
    231   }
    232 
    233   public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
    234       throws MessagingException {
    235     try {
    236       fetchInternal(messages, fp, listener);
    237     } catch (RuntimeException e) { // Probably a parser error.
    238       VvmLog.w(TAG, "Exception detected: " + e.getMessage());
    239       throw e;
    240     }
    241   }
    242 
    243   public void fetchInternal(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
    244       throws MessagingException {
    245     if (messages.length == 0) {
    246       return;
    247     }
    248     checkOpen();
    249     ArrayMap<String, Message> messageMap = new ArrayMap<String, Message>();
    250     for (Message m : messages) {
    251       messageMap.put(m.getUid(), m);
    252     }
    253 
    254     /*
    255      * Figure out what command we are going to run:
    256      * FLAGS     - UID FETCH (FLAGS)
    257      * ENVELOPE  - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
    258      *                            HEADER.FIELDS (date subject from content-type to cc)])
    259      * STRUCTURE - UID FETCH (BODYSTRUCTURE)
    260      * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
    261      * BODY      - UID FETCH (BODY.PEEK[])
    262      * Part      - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
    263      */
    264 
    265     final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
    266 
    267     fetchFields.add(ImapConstants.UID);
    268     if (fp.contains(FetchProfile.Item.FLAGS)) {
    269       fetchFields.add(ImapConstants.FLAGS);
    270     }
    271     if (fp.contains(FetchProfile.Item.ENVELOPE)) {
    272       fetchFields.add(ImapConstants.INTERNALDATE);
    273       fetchFields.add(ImapConstants.RFC822_SIZE);
    274       fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
    275     }
    276     if (fp.contains(FetchProfile.Item.STRUCTURE)) {
    277       fetchFields.add(ImapConstants.BODYSTRUCTURE);
    278     }
    279 
    280     if (fp.contains(FetchProfile.Item.BODY_SANE)) {
    281       fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
    282     }
    283     if (fp.contains(FetchProfile.Item.BODY)) {
    284       fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
    285     }
    286 
    287     // TODO Why are we only fetching the first part given?
    288     final Part fetchPart = fp.getFirstPart();
    289     if (fetchPart != null) {
    290       final String[] partIds = fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
    291       // TODO Why can a single part have more than one Id? And why should we only fetch
    292       // the first id if there are more than one?
    293       if (partIds != null) {
    294         fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE + "[" + partIds[0] + "]");
    295       }
    296     }
    297 
    298     try {
    299       mConnection.sendCommand(
    300           String.format(
    301               Locale.US,
    302               ImapConstants.UID_FETCH + " %s (%s)",
    303               ImapStore.joinMessageUids(messages),
    304               Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')),
    305           false);
    306       ImapResponse response;
    307       do {
    308         response = null;
    309         try {
    310           response = mConnection.readResponse();
    311 
    312           if (!response.isDataResponse(1, ImapConstants.FETCH)) {
    313             continue; // Ignore
    314           }
    315           final ImapList fetchList = response.getListOrEmpty(2);
    316           final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID).getString();
    317           if (TextUtils.isEmpty(uid)) continue;
    318 
    319           ImapMessage message = (ImapMessage) messageMap.get(uid);
    320           if (message == null) continue;
    321 
    322           if (fp.contains(FetchProfile.Item.FLAGS)) {
    323             final ImapList flags = fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
    324             for (int i = 0, count = flags.size(); i < count; i++) {
    325               final ImapString flag = flags.getStringOrEmpty(i);
    326               if (flag.is(ImapConstants.FLAG_DELETED)) {
    327                 message.setFlagInternal(Flag.DELETED, true);
    328               } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
    329                 message.setFlagInternal(Flag.ANSWERED, true);
    330               } else if (flag.is(ImapConstants.FLAG_SEEN)) {
    331                 message.setFlagInternal(Flag.SEEN, true);
    332               } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
    333                 message.setFlagInternal(Flag.FLAGGED, true);
    334               }
    335             }
    336           }
    337           if (fp.contains(FetchProfile.Item.ENVELOPE)) {
    338             final Date internalDate =
    339                 fetchList.getKeyedStringOrEmpty(ImapConstants.INTERNALDATE).getDateOrNull();
    340             final int size =
    341                 fetchList.getKeyedStringOrEmpty(ImapConstants.RFC822_SIZE).getNumberOrZero();
    342             final String header =
    343                 fetchList
    344                     .getKeyedStringOrEmpty(ImapConstants.BODY_BRACKET_HEADER, true)
    345                     .getString();
    346 
    347             message.setInternalDate(internalDate);
    348             message.setSize(size);
    349             try {
    350               message.parse(Utility.streamFromAsciiString(header));
    351             } catch (Exception e) {
    352               VvmLog.e(TAG, "Error parsing header %s", e);
    353             }
    354           }
    355           if (fp.contains(FetchProfile.Item.STRUCTURE)) {
    356             ImapList bs = fetchList.getKeyedListOrEmpty(ImapConstants.BODYSTRUCTURE);
    357             if (!bs.isEmpty()) {
    358               try {
    359                 parseBodyStructure(bs, message, ImapConstants.TEXT);
    360               } catch (MessagingException e) {
    361                 VvmLog.v(TAG, "Error handling message", e);
    362                 message.setBody(null);
    363               }
    364             }
    365           }
    366           if (fp.contains(FetchProfile.Item.BODY) || fp.contains(FetchProfile.Item.BODY_SANE)) {
    367             // Body is keyed by "BODY[]...".
    368             // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
    369             // TODO Should we accept "RFC822" as well??
    370             ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
    371             InputStream bodyStream = body.getAsStream();
    372             try {
    373               message.parse(bodyStream);
    374             } catch (Exception e) {
    375               VvmLog.e(TAG, "Error parsing body %s", e);
    376             }
    377           }
    378           if (fetchPart != null) {
    379             InputStream bodyStream = fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
    380             String[] encodings = fetchPart.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
    381 
    382             String contentTransferEncoding = null;
    383             if (encodings != null && encodings.length > 0) {
    384               contentTransferEncoding = encodings[0];
    385             } else {
    386               // According to http://tools.ietf.org/html/rfc2045#section-6.1
    387               // "7bit" is the default.
    388               contentTransferEncoding = "7bit";
    389             }
    390 
    391             try {
    392               // TODO Don't create 2 temp files.
    393               // decodeBody creates BinaryTempFileBody, but we could avoid this
    394               // if we implement ImapStringBody.
    395               // (We'll need to share a temp file.  Protect it with a ref-count.)
    396               message.setBody(
    397                   decodeBody(
    398                       mStore.getContext(),
    399                       bodyStream,
    400                       contentTransferEncoding,
    401                       fetchPart.getSize(),
    402                       listener));
    403             } catch (Exception e) {
    404               // TODO: Figure out what kinds of exceptions might actually be thrown
    405               // from here. This blanket catch-all is because we're not sure what to
    406               // do if we don't have a contentTransferEncoding, and we don't have
    407               // time to figure out what exceptions might be thrown.
    408               VvmLog.e(TAG, "Error fetching body %s", e);
    409             }
    410           }
    411 
    412           if (listener != null) {
    413             listener.messageRetrieved(message);
    414           }
    415         } finally {
    416           destroyResponses();
    417         }
    418       } while (!response.isTagged());
    419     } catch (IOException ioe) {
    420       mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
    421       throw ioExceptionHandler(mConnection, ioe);
    422     }
    423   }
    424 
    425   /**
    426    * Removes any content transfer encoding from the stream and returns a Body. This code is
    427    * taken/condensed from MimeUtility.decodeBody
    428    */
    429   private static Body decodeBody(
    430       Context context,
    431       InputStream in,
    432       String contentTransferEncoding,
    433       int size,
    434       MessageRetrievalListener listener)
    435       throws IOException {
    436     // Get a properly wrapped input stream
    437     in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
    438     BinaryTempFileBody tempBody = new BinaryTempFileBody();
    439     OutputStream out = tempBody.getOutputStream();
    440     try {
    441       byte[] buffer = new byte[COPY_BUFFER_SIZE];
    442       int n = 0;
    443       int count = 0;
    444       while (-1 != (n = in.read(buffer))) {
    445         out.write(buffer, 0, n);
    446         count += n;
    447       }
    448     } catch (Base64DataException bde) {
    449       String warning = "\n\nThere was an error while decoding the message.";
    450       out.write(warning.getBytes());
    451     } finally {
    452       out.close();
    453     }
    454     return tempBody;
    455   }
    456 
    457   public String[] getPermanentFlags() {
    458     return PERMANENT_FLAGS;
    459   }
    460 
    461   /**
    462    * Handle any untagged responses that the caller doesn't care to handle themselves.
    463    *
    464    * @param responses
    465    */
    466   private void handleUntaggedResponses(List<ImapResponse> responses) {
    467     for (ImapResponse response : responses) {
    468       handleUntaggedResponse(response);
    469     }
    470   }
    471 
    472   /**
    473    * Handle an untagged response that the caller doesn't care to handle themselves.
    474    *
    475    * @param response
    476    */
    477   private void handleUntaggedResponse(ImapResponse response) {
    478     if (response.isDataResponse(1, ImapConstants.EXISTS)) {
    479       mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
    480     }
    481   }
    482 
    483   private static void parseBodyStructure(ImapList bs, Part part, String id)
    484       throws MessagingException {
    485     if (bs.getElementOrNone(0).isList()) {
    486       /*
    487        * This is a multipart/*
    488        */
    489       MimeMultipart mp = new MimeMultipart();
    490       for (int i = 0, count = bs.size(); i < count; i++) {
    491         ImapElement e = bs.getElementOrNone(i);
    492         if (e.isList()) {
    493           /*
    494            * For each part in the message we're going to add a new BodyPart and parse
    495            * into it.
    496            */
    497           MimeBodyPart bp = new MimeBodyPart();
    498           if (id.equals(ImapConstants.TEXT)) {
    499             parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
    500 
    501           } else {
    502             parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
    503           }
    504           mp.addBodyPart(bp);
    505 
    506         } else {
    507           if (e.isString()) {
    508             mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
    509           }
    510           break; // Ignore the rest of the list.
    511         }
    512       }
    513       part.setBody(mp);
    514     } else {
    515       /*
    516        * This is a body. We need to add as much information as we can find out about
    517        * it to the Part.
    518        */
    519 
    520       /*
    521       body type
    522       body subtype
    523       body parameter parenthesized list
    524       body id
    525       body description
    526       body encoding
    527       body size
    528       */
    529 
    530       final ImapString type = bs.getStringOrEmpty(0);
    531       final ImapString subType = bs.getStringOrEmpty(1);
    532       final String mimeType = (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
    533 
    534       final ImapList bodyParams = bs.getListOrEmpty(2);
    535       final ImapString cid = bs.getStringOrEmpty(3);
    536       final ImapString encoding = bs.getStringOrEmpty(5);
    537       final int size = bs.getStringOrEmpty(6).getNumberOrZero();
    538 
    539       if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
    540         // A body type of type MESSAGE and subtype RFC822
    541         // contains, immediately after the basic fields, the
    542         // envelope structure, body structure, and size in
    543         // text lines of the encapsulated message.
    544         // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
    545         //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
    546         /*
    547          * This will be caught by fetch and handled appropriately.
    548          */
    549         throw new MessagingException(
    550             "BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 + " not yet supported.");
    551       }
    552 
    553       /*
    554        * Set the content type with as much information as we know right now.
    555        */
    556       final StringBuilder contentType = new StringBuilder(mimeType);
    557 
    558       /*
    559        * If there are body params we might be able to get some more information out
    560        * of them.
    561        */
    562       for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
    563 
    564         // TODO We need to convert " into %22, but
    565         // because MimeUtility.getHeaderParameter doesn't recognize it,
    566         // we can't fix it for now.
    567         contentType.append(
    568             String.format(
    569                 ";\n %s=\"%s\"",
    570                 bodyParams.getStringOrEmpty(i - 1).getString(),
    571                 bodyParams.getStringOrEmpty(i).getString()));
    572       }
    573 
    574       part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
    575 
    576       // Extension items
    577       final ImapList bodyDisposition;
    578 
    579       if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
    580         // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
    581         // So, if it's not a list, use 10th element.
    582         // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
    583         bodyDisposition = bs.getListOrEmpty(9);
    584       } else {
    585         bodyDisposition = bs.getListOrEmpty(8);
    586       }
    587 
    588       final StringBuilder contentDisposition = new StringBuilder();
    589 
    590       if (bodyDisposition.size() > 0) {
    591         final String bodyDisposition0Str =
    592             bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
    593         if (!TextUtils.isEmpty(bodyDisposition0Str)) {
    594           contentDisposition.append(bodyDisposition0Str);
    595         }
    596 
    597         final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
    598         if (!bodyDispositionParams.isEmpty()) {
    599           /*
    600            * If there is body disposition information we can pull some more
    601            * information about the attachment out.
    602            */
    603           for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
    604 
    605             // TODO We need to convert " into %22.  See above.
    606             contentDisposition.append(
    607                 String.format(
    608                     Locale.US,
    609                     ";\n %s=\"%s\"",
    610                     bodyDispositionParams
    611                         .getStringOrEmpty(i - 1)
    612                         .getString()
    613                         .toLowerCase(Locale.US),
    614                     bodyDispositionParams.getStringOrEmpty(i).getString()));
    615           }
    616         }
    617       }
    618 
    619       if ((size > 0)
    620           && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") == null)) {
    621         contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
    622       }
    623 
    624       if (contentDisposition.length() > 0) {
    625         /*
    626          * Set the content disposition containing at least the size. Attachment
    627          * handling code will use this down the road.
    628          */
    629         part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition.toString());
    630       }
    631 
    632       /*
    633        * Set the Content-Transfer-Encoding header. Attachment code will use this
    634        * to parse the body.
    635        */
    636       if (!encoding.isEmpty()) {
    637         part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding.getString());
    638       }
    639 
    640       /*
    641        * Set the Content-ID header.
    642        */
    643       if (!cid.isEmpty()) {
    644         part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
    645       }
    646 
    647       if (size > 0) {
    648         if (part instanceof ImapMessage) {
    649           ((ImapMessage) part).setSize(size);
    650         } else if (part instanceof MimeBodyPart) {
    651           ((MimeBodyPart) part).setSize(size);
    652         } else {
    653           throw new MessagingException("Unknown part type " + part.toString());
    654         }
    655       }
    656       part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
    657     }
    658   }
    659 
    660   public Message[] expunge() throws MessagingException {
    661     checkOpen();
    662     try {
    663       handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
    664     } catch (IOException ioe) {
    665       mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
    666       throw ioExceptionHandler(mConnection, ioe);
    667     } finally {
    668       destroyResponses();
    669     }
    670     return null;
    671   }
    672 
    673   public void setFlags(Message[] messages, String[] flags, boolean value)
    674       throws MessagingException {
    675     checkOpen();
    676 
    677     String allFlags = "";
    678     if (flags.length > 0) {
    679       StringBuilder flagList = new StringBuilder();
    680       for (int i = 0, count = flags.length; i < count; i++) {
    681         String flag = flags[i];
    682         if (flag == Flag.SEEN) {
    683           flagList.append(" " + ImapConstants.FLAG_SEEN);
    684         } else if (flag == Flag.DELETED) {
    685           flagList.append(" " + ImapConstants.FLAG_DELETED);
    686         } else if (flag == Flag.FLAGGED) {
    687           flagList.append(" " + ImapConstants.FLAG_FLAGGED);
    688         } else if (flag == Flag.ANSWERED) {
    689           flagList.append(" " + ImapConstants.FLAG_ANSWERED);
    690         }
    691       }
    692       allFlags = flagList.substring(1);
    693     }
    694     try {
    695       mConnection.executeSimpleCommand(
    696           String.format(
    697               Locale.US,
    698               ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
    699               ImapStore.joinMessageUids(messages),
    700               value ? "+" : "-",
    701               allFlags));
    702 
    703     } catch (IOException ioe) {
    704       mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
    705       throw ioExceptionHandler(mConnection, ioe);
    706     } finally {
    707       destroyResponses();
    708     }
    709   }
    710 
    711   /**
    712    * Selects the folder for use. Before performing any operations on this folder, it must be
    713    * selected.
    714    */
    715   private void doSelect() throws IOException, MessagingException {
    716     final List<ImapResponse> responses =
    717         mConnection.executeSimpleCommand(
    718             String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", mName));
    719 
    720     // Assume the folder is opened read-write; unless we are notified otherwise
    721     mMode = MODE_READ_WRITE;
    722     int messageCount = -1;
    723     for (ImapResponse response : responses) {
    724       if (response.isDataResponse(1, ImapConstants.EXISTS)) {
    725         messageCount = response.getStringOrEmpty(0).getNumberOrZero();
    726       } else if (response.isOk()) {
    727         final ImapString responseCode = response.getResponseCodeOrEmpty();
    728         if (responseCode.is(ImapConstants.READ_ONLY)) {
    729           mMode = MODE_READ_ONLY;
    730         } else if (responseCode.is(ImapConstants.READ_WRITE)) {
    731           mMode = MODE_READ_WRITE;
    732         }
    733       } else if (response.isTagged()) { // Not OK
    734         mStore.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED);
    735         throw new MessagingException(
    736             "Can't open mailbox: " + response.getStatusResponseTextOrEmpty());
    737       }
    738     }
    739     if (messageCount == -1) {
    740       throw new MessagingException("Did not find message count during select");
    741     }
    742     mMessageCount = messageCount;
    743     mExists = true;
    744   }
    745 
    746   public class Quota {
    747 
    748     public final int occupied;
    749     public final int total;
    750 
    751     public Quota(int occupied, int total) {
    752       this.occupied = occupied;
    753       this.total = total;
    754     }
    755   }
    756 
    757   public Quota getQuota() throws MessagingException {
    758     try {
    759       final List<ImapResponse> responses =
    760           mConnection.executeSimpleCommand(
    761               String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", mName));
    762 
    763       for (ImapResponse response : responses) {
    764         if (!response.isDataResponse(0, ImapConstants.QUOTA)) {
    765           continue;
    766         }
    767         ImapList list = response.getListOrEmpty(2);
    768         for (int i = 0; i < list.size(); i += 3) {
    769           if (!list.getStringOrEmpty(i).is("voice")) {
    770             continue;
    771           }
    772           return new Quota(
    773               list.getStringOrEmpty(i + 1).getNumber(-1),
    774               list.getStringOrEmpty(i + 2).getNumber(-1));
    775         }
    776       }
    777     } catch (IOException ioe) {
    778       mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
    779       throw ioExceptionHandler(mConnection, ioe);
    780     } finally {
    781       destroyResponses();
    782     }
    783     return null;
    784   }
    785 
    786   private void checkOpen() throws MessagingException {
    787     if (!isOpen()) {
    788       throw new MessagingException("Folder " + mName + " is not open.");
    789     }
    790   }
    791 
    792   private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
    793     VvmLog.d(TAG, "IO Exception detected: ", ioe);
    794     connection.close();
    795     if (connection == mConnection) {
    796       mConnection = null; // To prevent close() from returning the connection to the pool.
    797       close(false);
    798     }
    799     return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
    800   }
    801 
    802   public Message createMessage(String uid) {
    803     return new ImapMessage(uid, this);
    804   }
    805 }
    806