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