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