Home | History | Annotate | Download | only in store
      1 /*
      2  * Copyright (C) 2008 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 com.android.email.Email;
     20 import com.android.email.Preferences;
     21 import com.android.email.Utility;
     22 import com.android.email.VendorPolicyLoader;
     23 import com.android.email.mail.AuthenticationFailedException;
     24 import com.android.email.mail.CertificateValidationException;
     25 import com.android.email.mail.FetchProfile;
     26 import com.android.email.mail.Flag;
     27 import com.android.email.mail.Folder;
     28 import com.android.email.mail.Message;
     29 import com.android.email.mail.MessagingException;
     30 import com.android.email.mail.Part;
     31 import com.android.email.mail.Store;
     32 import com.android.email.mail.Transport;
     33 import com.android.email.mail.internet.MimeBodyPart;
     34 import com.android.email.mail.internet.MimeHeader;
     35 import com.android.email.mail.internet.MimeMessage;
     36 import com.android.email.mail.internet.MimeMultipart;
     37 import com.android.email.mail.internet.MimeUtility;
     38 import com.android.email.mail.store.imap.ImapConstants;
     39 import com.android.email.mail.store.imap.ImapElement;
     40 import com.android.email.mail.store.imap.ImapList;
     41 import com.android.email.mail.store.imap.ImapResponse;
     42 import com.android.email.mail.store.imap.ImapResponseParser;
     43 import com.android.email.mail.store.imap.ImapString;
     44 import com.android.email.mail.transport.CountingOutputStream;
     45 import com.android.email.mail.transport.DiscourseLogger;
     46 import com.android.email.mail.transport.EOLConvertingOutputStream;
     47 import com.android.email.mail.transport.MailTransport;
     48 import com.beetstra.jutf7.CharsetProvider;
     49 
     50 import android.content.Context;
     51 import android.os.Build;
     52 import android.telephony.TelephonyManager;
     53 import android.text.TextUtils;
     54 import android.util.Base64;
     55 import android.util.Config;
     56 import android.util.Log;
     57 
     58 import java.io.IOException;
     59 import java.io.InputStream;
     60 import java.net.URI;
     61 import java.net.URISyntaxException;
     62 import java.nio.ByteBuffer;
     63 import java.nio.charset.Charset;
     64 import java.security.MessageDigest;
     65 import java.security.NoSuchAlgorithmException;
     66 import java.util.ArrayList;
     67 import java.util.Collection;
     68 import java.util.Date;
     69 import java.util.HashMap;
     70 import java.util.LinkedHashSet;
     71 import java.util.List;
     72 import java.util.concurrent.ConcurrentLinkedQueue;
     73 import java.util.concurrent.atomic.AtomicInteger;
     74 import java.util.regex.Pattern;
     75 
     76 import javax.net.ssl.SSLException;
     77 
     78 /**
     79  * <pre>
     80  * TODO Need to start keeping track of UIDVALIDITY
     81  * TODO Need a default response handler for things like folder updates
     82  * TODO In fetch(), if we need a ImapMessage and were given
     83  * TODO Collect ALERT messages and show them to users.
     84  * something else we can try to do a pre-fetch first.
     85  *
     86  * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for
     87  * certain information in a FETCH command, the server may return the requested
     88  * information in any order, not necessarily in the order that it was requested.
     89  * Further, the server may return the information in separate FETCH responses
     90  * and may also return information that was not explicitly requested (to reflect
     91  * to the client changes in the state of the subject message).
     92  * </pre>
     93  */
     94 public class ImapStore extends Store {
     95 
     96     // Always check in FALSE
     97     private static final boolean DEBUG_FORCE_SEND_ID = false;
     98 
     99     private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED };
    100 
    101     private final Context mContext;
    102     private Transport mRootTransport;
    103     private String mUsername;
    104     private String mPassword;
    105     private String mLoginPhrase;
    106     private String mPathPrefix;
    107     private String mIdPhrase = null;
    108     private static String sImapId = null;
    109 
    110     private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool =
    111             new ConcurrentLinkedQueue<ImapConnection>();
    112 
    113     /**
    114      * Charset used for converting folder names to and from UTF-7 as defined by RFC 3501.
    115      */
    116     private static final Charset MODIFIED_UTF_7_CHARSET =
    117             new CharsetProvider().charsetForName("X-RFC-3501");
    118 
    119     /**
    120      * Cache of ImapFolder objects. ImapFolders are attached to a given folder on the server
    121      * and as long as their associated connection remains open they are reusable between
    122      * requests. This cache lets us make sure we always reuse, if possible, for a given
    123      * folder name.
    124      */
    125     private HashMap<String, ImapFolder> mFolderCache = new HashMap<String, ImapFolder>();
    126 
    127     /**
    128      * Next tag to use.  All connections associated to the same ImapStore instance share the same
    129      * counter to make tests simpler.
    130      * (Some of the tests involve multiple connections but only have a single counter to track the
    131      * tag.)
    132      */
    133     private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
    134 
    135     /**
    136      * Static named constructor.
    137      */
    138     public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks)
    139             throws MessagingException {
    140         return new ImapStore(context, uri);
    141     }
    142 
    143     /**
    144      * Allowed formats for the Uri:
    145      * imap://user:password@server:port
    146      * imap+tls+://user:password@server:port
    147      * imap+tls+trustallcerts://user:password@server:port
    148      * imap+ssl+://user:password@server:port
    149      * imap+ssl+trustallcerts://user:password@server:port
    150      *
    151      * @param uriString the Uri containing information to configure this store
    152      */
    153     private ImapStore(Context context, String uriString) throws MessagingException {
    154         mContext = context;
    155         URI uri;
    156         try {
    157             uri = new URI(uriString);
    158         } catch (URISyntaxException use) {
    159             throw new MessagingException("Invalid ImapStore URI", use);
    160         }
    161 
    162         String scheme = uri.getScheme();
    163         if (scheme == null || !scheme.startsWith(STORE_SCHEME_IMAP)) {
    164             throw new MessagingException("Unsupported protocol");
    165         }
    166         // defaults, which can be changed by security modifiers
    167         int connectionSecurity = Transport.CONNECTION_SECURITY_NONE;
    168         int defaultPort = 143;
    169         // check for security modifiers and apply changes
    170         if (scheme.contains("+ssl")) {
    171             connectionSecurity = Transport.CONNECTION_SECURITY_SSL;
    172             defaultPort = 993;
    173         } else if (scheme.contains("+tls")) {
    174             connectionSecurity = Transport.CONNECTION_SECURITY_TLS;
    175         }
    176         boolean trustCertificates = scheme.contains(STORE_SECURITY_TRUST_CERTIFICATES);
    177 
    178         mRootTransport = new MailTransport("IMAP");
    179         mRootTransport.setUri(uri, defaultPort);
    180         mRootTransport.setSecurity(connectionSecurity, trustCertificates);
    181 
    182         String[] userInfoParts = mRootTransport.getUserInfoParts();
    183         if (userInfoParts != null) {
    184             mUsername = userInfoParts[0];
    185             if (userInfoParts.length > 1) {
    186                 mPassword = userInfoParts[1];
    187 
    188                 // build the LOGIN string once (instead of over-and-over again.)
    189                 // apply the quoting here around the built-up password
    190                 mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " "
    191                         + Utility.imapQuoted(mPassword);
    192             }
    193         }
    194 
    195         if ((uri.getPath() != null) && (uri.getPath().length() > 0)) {
    196             mPathPrefix = uri.getPath().substring(1);
    197         }
    198     }
    199 
    200     /* package */ Collection<ImapConnection> getConnectionPoolForTest() {
    201         return mConnectionPool;
    202     }
    203 
    204     /**
    205      * For testing only.  Injects a different root transport (it will be copied using
    206      * newInstanceWithConfiguration() each time IMAP sets up a new channel).  The transport
    207      * should already be set up and ready to use.  Do not use for real code.
    208      * @param testTransport The Transport to inject and use for all future communication.
    209      */
    210     /* package */ void setTransport(Transport testTransport) {
    211         mRootTransport = testTransport;
    212     }
    213 
    214     /**
    215      * Return, or create and return, an string suitable for use in an IMAP ID message.
    216      * This is constructed similarly to the way the browser sets up its user-agent strings.
    217      * See RFC 2971 for more details.  The output of this command will be a series of key-value
    218      * pairs delimited by spaces (there is no point in returning a structured result because
    219      * this will be sent as-is to the IMAP server).  No tokens, parenthesis or "ID" are included,
    220      * because some connections may append additional values.
    221      *
    222      * The following IMAP ID keys may be included:
    223      *   name                   Android package name of the program
    224      *   os                     "android"
    225      *   os-version             "version; model; build-id"
    226      *   vendor                 Vendor of the client/server
    227      *   x-android-device-model Model (only revealed if release build)
    228      *   x-android-net-operator Mobile network operator (if known)
    229      *   AGUID                  A device+account UID
    230      *
    231      * In addition, a vendor policy .apk can append key/value pairs.
    232      *
    233      * @param userName the username of the account
    234      * @param host the host (server) of the account
    235      * @param capability the capabilities string from the server
    236      * @return a String for use in an IMAP ID message.
    237      */
    238     /* package */ static String getImapId(Context context, String userName, String host,
    239             ImapResponse capabilityResponse) {
    240         // The first section is global to all IMAP connections, and generates the fixed
    241         // values in any IMAP ID message
    242         synchronized (ImapStore.class) {
    243             if (sImapId == null) {
    244                 TelephonyManager tm =
    245                         (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    246                 String networkOperator = tm.getNetworkOperatorName();
    247                 if (networkOperator == null) networkOperator = "";
    248 
    249                 sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE,
    250                         Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER,
    251                         networkOperator);
    252             }
    253         }
    254 
    255         // This section is per Store, and adds in a dynamic elements like UID's.
    256         // We don't cache the result of this work, because the caller does anyway.
    257         StringBuilder id = new StringBuilder(sImapId);
    258 
    259         // Optionally add any vendor-supplied id keys
    260         String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host,
    261                 capabilityResponse.flatten());
    262         if (vendorId != null) {
    263             id.append(' ');
    264             id.append(vendorId);
    265         }
    266 
    267         // Generate a UID that mixes a "stable" device UID with the email address
    268         try {
    269             String devUID = Preferences.getPreferences(context).getDeviceUID();
    270             MessageDigest messageDigest;
    271             messageDigest = MessageDigest.getInstance("SHA-1");
    272             messageDigest.update(userName.getBytes());
    273             messageDigest.update(devUID.getBytes());
    274             byte[] uid = messageDigest.digest();
    275             String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP);
    276             id.append(" \"AGUID\" \"");
    277             id.append(hexUid);
    278             id.append('\"');
    279         } catch (NoSuchAlgorithmException e) {
    280             Log.d(Email.LOG_TAG, "couldn't obtain SHA-1 hash for device UID");
    281         }
    282         return id.toString();
    283     }
    284 
    285     /**
    286      * Helper function that actually builds the static part of the IMAP ID string.  This is
    287      * separated from getImapId for testability.  There is no escaping or encoding in IMAP ID so
    288      * any rogue chars must be filtered here.
    289      *
    290      * @param packageName context.getPackageName()
    291      * @param version Build.VERSION.RELEASE
    292      * @param codeName Build.VERSION.CODENAME
    293      * @param model Build.MODEL
    294      * @param id Build.ID
    295      * @param vendor Build.MANUFACTURER
    296      * @param networkOperator TelephonyManager.getNetworkOperatorName()
    297      * @return the static (never changes) portion of the IMAP ID
    298      */
    299     /* package */ static String makeCommonImapId(String packageName, String version,
    300             String codeName, String model, String id, String vendor, String networkOperator) {
    301 
    302         // Before building up IMAP ID string, pre-filter the input strings for "legal" chars
    303         // This is using a fairly arbitrary char set intended to pass through most reasonable
    304         // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space>
    305         // The most important thing is *not* to pass parens, quotes, or CRLF, which would break
    306         // the format of the IMAP ID list.
    307         Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]");
    308         packageName = p.matcher(packageName).replaceAll("");
    309         version = p.matcher(version).replaceAll("");
    310         codeName = p.matcher(codeName).replaceAll("");
    311         model = p.matcher(model).replaceAll("");
    312         id = p.matcher(id).replaceAll("");
    313         vendor = p.matcher(vendor).replaceAll("");
    314         networkOperator = p.matcher(networkOperator).replaceAll("");
    315 
    316         // "name" "com.android.email"
    317         StringBuffer sb = new StringBuffer("\"name\" \"");
    318         sb.append(packageName);
    319         sb.append("\"");
    320 
    321         // "os" "android"
    322         sb.append(" \"os\" \"android\"");
    323 
    324         // "os-version" "version; build-id"
    325         sb.append(" \"os-version\" \"");
    326         if (version.length() > 0) {
    327             sb.append(version);
    328         } else {
    329             // default to "1.0"
    330             sb.append("1.0");
    331         }
    332         // add the build ID or build #
    333         if (id.length() > 0) {
    334             sb.append("; ");
    335             sb.append(id);
    336         }
    337         sb.append("\"");
    338 
    339         // "vendor" "the vendor"
    340         if (vendor.length() > 0) {
    341             sb.append(" \"vendor\" \"");
    342             sb.append(vendor);
    343             sb.append("\"");
    344         }
    345 
    346         // "x-android-device-model" the device model (on release builds only)
    347         if ("REL".equals(codeName)) {
    348             if (model.length() > 0) {
    349                 sb.append(" \"x-android-device-model\" \"");
    350                 sb.append(model);
    351                 sb.append("\"");
    352             }
    353         }
    354 
    355         // "x-android-mobile-net-operator" "name of network operator"
    356         if (networkOperator.length() > 0) {
    357             sb.append(" \"x-android-mobile-net-operator\" \"");
    358             sb.append(networkOperator);
    359             sb.append("\"");
    360         }
    361 
    362         return sb.toString();
    363     }
    364 
    365 
    366     @Override
    367     public Folder getFolder(String name) throws MessagingException {
    368         ImapFolder folder;
    369         synchronized (mFolderCache) {
    370             folder = mFolderCache.get(name);
    371             if (folder == null) {
    372                 folder = new ImapFolder(this, name);
    373                 mFolderCache.put(name, folder);
    374             }
    375         }
    376         return folder;
    377     }
    378 
    379     @Override
    380     public Folder[] getPersonalNamespaces() throws MessagingException {
    381         ImapConnection connection = getConnection();
    382         try {
    383             ArrayList<Folder> folders = new ArrayList<Folder>();
    384             List<ImapResponse> responses = connection.executeSimpleCommand(
    385                     String.format(ImapConstants.LIST + " \"\" \"%s*\"",
    386                             mPathPrefix == null ? "" : mPathPrefix));
    387             for (ImapResponse response : responses) {
    388                 // S: * LIST (\Noselect) "/" ~/Mail/foo
    389                 if (response.isDataResponse(0, ImapConstants.LIST)) {
    390                     boolean includeFolder = true;
    391 
    392                     // Get folder name.
    393                     ImapString encodedFolder = response.getStringOrEmpty(3);
    394                     if (encodedFolder.isEmpty()) continue;
    395                     String folder = decodeFolderName(encodedFolder.getString());
    396                     if (ImapConstants.INBOX.equalsIgnoreCase(folder)) {
    397                         continue;
    398                     }
    399 
    400                     // Parse attributes.
    401                     if (response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT)) {
    402                         includeFolder = false;
    403                     }
    404                     if (includeFolder) {
    405                         folders.add(getFolder(folder));
    406                     }
    407                 }
    408             }
    409             folders.add(getFolder(ImapConstants.INBOX));
    410             return folders.toArray(new Folder[] {});
    411         } catch (IOException ioe) {
    412             connection.close();
    413             throw new MessagingException("Unable to get folder list.", ioe);
    414         } finally {
    415             connection.destroyResponses();
    416             poolConnection(connection);
    417         }
    418     }
    419 
    420     @Override
    421     public void checkSettings() throws MessagingException {
    422         ImapConnection connection = new ImapConnection();
    423         try {
    424             connection.open();
    425             connection.close();
    426         } catch (IOException ioe) {
    427             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
    428         } finally {
    429             connection.destroyResponses();
    430         }
    431     }
    432 
    433     /**
    434      * Gets a connection if one is available from the pool, or creates a new one if not.
    435      */
    436     /* package */ ImapConnection getConnection() {
    437         ImapConnection connection = null;
    438         while ((connection = mConnectionPool.poll()) != null) {
    439             try {
    440                 connection.executeSimpleCommand(ImapConstants.NOOP);
    441                 break;
    442             } catch (MessagingException e) {
    443                 // Fall through
    444             } catch (IOException e) {
    445                 // Fall through
    446             } finally {
    447                 connection.destroyResponses();
    448             }
    449             connection.close();
    450             connection = null;
    451         }
    452         if (connection == null) {
    453             connection = new ImapConnection();
    454         }
    455         return connection;
    456     }
    457 
    458     /**
    459      * Save a {@link ImapConnection} in the pool for reuse.
    460      */
    461     /* package */ void poolConnection(ImapConnection connection) {
    462         if (connection != null) {
    463             mConnectionPool.add(connection);
    464         }
    465     }
    466 
    467     /* package */ static String encodeFolderName(String name) {
    468         // TODO bypass the conversion if name doesn't have special char.
    469         ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name);
    470         byte[] b = new byte[bb.limit()];
    471         bb.get(b);
    472         return Utility.fromAscii(b);
    473     }
    474 
    475     /* package */ static String decodeFolderName(String name) {
    476         // TODO bypass the conversion if name doesn't have special char.
    477         /*
    478          * Convert the encoded name to US-ASCII, then pass it through the modified UTF-7
    479          * decoder and return the Unicode String.
    480          */
    481         return MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString();
    482     }
    483 
    484     /**
    485      * Returns UIDs of Messages joined with "," as the separator.
    486      */
    487     /* package */ static String joinMessageUids(Message[] messages) {
    488         StringBuilder sb = new StringBuilder();
    489         boolean notFirst = false;
    490         for (Message m : messages) {
    491             if (notFirst) {
    492                 sb.append(',');
    493             }
    494             sb.append(m.getUid());
    495             notFirst = true;
    496         }
    497         return sb.toString();
    498     }
    499 
    500     static class ImapFolder extends Folder {
    501         private final ImapStore mStore;
    502         private final String mName;
    503         private int mMessageCount = -1;
    504         private ImapConnection mConnection;
    505         private OpenMode mMode;
    506         private boolean mExists;
    507 
    508         public ImapFolder(ImapStore store, String name) {
    509             mStore = store;
    510             mName = name;
    511         }
    512 
    513         private void destroyResponses() {
    514             if (mConnection != null) {
    515                 mConnection.destroyResponses();
    516             }
    517         }
    518 
    519         @Override
    520         public void open(OpenMode mode, PersistentDataCallbacks callbacks)
    521                 throws MessagingException {
    522             try {
    523                 if (isOpen()) {
    524                     if (mMode == mode) {
    525                         // Make sure the connection is valid.
    526                         // If it's not we'll close it down and continue on to get a new one.
    527                         try {
    528                             mConnection.executeSimpleCommand(ImapConstants.NOOP);
    529                             return;
    530 
    531                         } catch (IOException ioe) {
    532                             ioExceptionHandler(mConnection, ioe);
    533                         } finally {
    534                             destroyResponses();
    535                         }
    536                     } else {
    537                         // Return the connection to the pool, if exists.
    538                         close(false);
    539                     }
    540                 }
    541                 synchronized (this) {
    542                     mConnection = mStore.getConnection();
    543                 }
    544                 // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
    545                 // $MDNSent)
    546                 // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
    547                 // NonJunk $MDNSent \*)] Flags permitted.
    548                 // * 23 EXISTS
    549                 // * 0 RECENT
    550                 // * OK [UIDVALIDITY 1125022061] UIDs valid
    551                 // * OK [UIDNEXT 57576] Predicted next UID
    552                 // 2 OK [READ-WRITE] Select completed.
    553                 try {
    554                     List<ImapResponse> responses = mConnection.executeSimpleCommand(
    555                             String.format(ImapConstants.SELECT + " \"%s\"",
    556                                     encodeFolderName(mName)));
    557                     /*
    558                      * If the command succeeds we expect the folder has been opened read-write
    559                      * unless we are notified otherwise in the responses.
    560                      */
    561                     mMode = OpenMode.READ_WRITE;
    562 
    563                     int messageCount = -1;
    564                     for (ImapResponse response : responses) {
    565                         if (response.isDataResponse(1, ImapConstants.EXISTS)) {
    566                             messageCount = response.getStringOrEmpty(0).getNumberOrZero();
    567 
    568                         } else if (response.isOk()) {
    569                             final ImapString responseCode = response.getResponseCodeOrEmpty();
    570                             if (responseCode.is(ImapConstants.READ_ONLY)) {
    571                                 mMode = OpenMode.READ_ONLY;
    572                             } else if (responseCode.is(ImapConstants.READ_WRITE)) {
    573                                 mMode = OpenMode.READ_WRITE;
    574                             }
    575                         } else if (response.isTagged()) { // Not OK
    576                             throw new MessagingException("Can't open mailbox: "
    577                                     + response.getStatusResponseTextOrEmpty());
    578                         }
    579                     }
    580 
    581                     if (messageCount == -1) {
    582                         throw new MessagingException("Did not find message count during select");
    583                     }
    584                     mMessageCount = messageCount;
    585                     mExists = true;
    586 
    587                 } catch (IOException ioe) {
    588                     throw ioExceptionHandler(mConnection, ioe);
    589                 } finally {
    590                     destroyResponses();
    591                 }
    592             } catch (MessagingException e) {
    593                 mExists = false;
    594                 close(false);
    595                 throw e;
    596             }
    597         }
    598 
    599         @Override
    600         public boolean isOpen() {
    601             return mExists && mConnection != null;
    602         }
    603 
    604         @Override
    605         public OpenMode getMode() throws MessagingException {
    606             return mMode;
    607         }
    608 
    609         @Override
    610         public void close(boolean expunge) {
    611             // TODO implement expunge
    612             mMessageCount = -1;
    613             synchronized (this) {
    614                 destroyResponses();
    615                 mStore.poolConnection(mConnection);
    616                 mConnection = null;
    617             }
    618         }
    619 
    620         @Override
    621         public String getName() {
    622             return mName;
    623         }
    624 
    625         @Override
    626         public boolean exists() throws MessagingException {
    627             if (mExists) {
    628                 return true;
    629             }
    630             /*
    631              * This method needs to operate in the unselected mode as well as the selected mode
    632              * so we must get the connection ourselves if it's not there. We are specifically
    633              * not calling checkOpen() since we don't care if the folder is open.
    634              */
    635             ImapConnection connection = null;
    636             synchronized(this) {
    637                 if (mConnection == null) {
    638                     connection = mStore.getConnection();
    639                 } else {
    640                     connection = mConnection;
    641                 }
    642             }
    643             try {
    644                 connection.executeSimpleCommand(String.format(
    645                         ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")",
    646                         encodeFolderName(mName)));
    647                 mExists = true;
    648                 return true;
    649 
    650             } catch (MessagingException me) {
    651                 return false;
    652 
    653             } catch (IOException ioe) {
    654                 throw ioExceptionHandler(connection, ioe);
    655 
    656             } finally {
    657                 connection.destroyResponses();
    658                 if (mConnection == null) {
    659                     mStore.poolConnection(connection);
    660                 }
    661             }
    662         }
    663 
    664         // IMAP supports folder creation
    665         @Override
    666         public boolean canCreate(FolderType type) {
    667             return true;
    668         }
    669 
    670         @Override
    671         public boolean create(FolderType type) throws MessagingException {
    672             /*
    673              * This method needs to operate in the unselected mode as well as the selected mode
    674              * so we must get the connection ourselves if it's not there. We are specifically
    675              * not calling checkOpen() since we don't care if the folder is open.
    676              */
    677             ImapConnection connection = null;
    678             synchronized(this) {
    679                 if (mConnection == null) {
    680                     connection = mStore.getConnection();
    681                 } else {
    682                     connection = mConnection;
    683                 }
    684             }
    685             try {
    686                 connection.executeSimpleCommand(String.format(ImapConstants.CREATE + " \"%s\"",
    687                         encodeFolderName(mName)));
    688                 return true;
    689 
    690             } catch (MessagingException me) {
    691                 return false;
    692 
    693             } catch (IOException ioe) {
    694                 throw ioExceptionHandler(connection, ioe);
    695 
    696             } finally {
    697                 connection.destroyResponses();
    698                 if (mConnection == null) {
    699                     mStore.poolConnection(connection);
    700                 }
    701             }
    702         }
    703 
    704         @Override
    705         public void copyMessages(Message[] messages, Folder folder,
    706                 MessageUpdateCallbacks callbacks) throws MessagingException {
    707             checkOpen();
    708             try {
    709                 mConnection.executeSimpleCommand(
    710                         String.format(ImapConstants.UID_COPY + " %s \"%s\"",
    711                                 joinMessageUids(messages),
    712                                 encodeFolderName(folder.getName())));
    713             } catch (IOException ioe) {
    714                 throw ioExceptionHandler(mConnection, ioe);
    715             } finally {
    716                 destroyResponses();
    717             }
    718         }
    719 
    720         @Override
    721         public int getMessageCount() {
    722             return mMessageCount;
    723         }
    724 
    725         @Override
    726         public int getUnreadMessageCount() throws MessagingException {
    727             checkOpen();
    728             try {
    729                 int unreadMessageCount = 0;
    730                 List<ImapResponse> responses = mConnection.executeSimpleCommand(String.format(
    731                         ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")",
    732                         encodeFolderName(mName)));
    733                 // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292)
    734                 for (ImapResponse response : responses) {
    735                     if (response.isDataResponse(0, ImapConstants.STATUS)) {
    736                         unreadMessageCount = response.getListOrEmpty(2)
    737                                 .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero();
    738                     }
    739                 }
    740                 return unreadMessageCount;
    741             } catch (IOException ioe) {
    742                 throw ioExceptionHandler(mConnection, ioe);
    743             } finally {
    744                 destroyResponses();
    745             }
    746         }
    747 
    748         @Override
    749         public void delete(boolean recurse) throws MessagingException {
    750             throw new Error("ImapStore.delete() not yet implemented");
    751         }
    752 
    753         /* package */ String[] searchForUids(String searchCriteria)
    754                 throws MessagingException {
    755             checkOpen();
    756             List<ImapResponse> responses;
    757             try {
    758                 try {
    759                     responses = mConnection.executeSimpleCommand(
    760                             ImapConstants.UID_SEARCH + " " + searchCriteria);
    761                 } catch (ImapException e) {
    762                     return Utility.EMPTY_STRINGS; // not found;
    763                 } catch (IOException ioe) {
    764                     throw ioExceptionHandler(mConnection, ioe);
    765                 }
    766                 // S: * SEARCH 2 3 6
    767                 final ArrayList<String> uids = new ArrayList<String>();
    768                 for (ImapResponse response : responses) {
    769                     if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
    770                         continue;
    771                     }
    772                     // Found SEARCH response data
    773                     for (int i = 1; i < response.size(); i++) {
    774                         ImapString s = response.getStringOrEmpty(i);
    775                         if (s.isString()) {
    776                             uids.add(s.getString());
    777                         }
    778                     }
    779                 }
    780                 return uids.toArray(Utility.EMPTY_STRINGS);
    781             } finally {
    782                 destroyResponses();
    783             }
    784         }
    785 
    786         @Override
    787         public Message getMessage(String uid) throws MessagingException {
    788             checkOpen();
    789 
    790             String[] uids = searchForUids(ImapConstants.UID + " " + uid);
    791             for (int i = 0; i < uids.length; i++) {
    792                 if (uids[i].equals(uid)) {
    793                     return new ImapMessage(uid, this);
    794                 }
    795             }
    796             return null;
    797         }
    798 
    799         @Override
    800         public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
    801                 throws MessagingException {
    802             if (start < 1 || end < 1 || end < start) {
    803                 throw new MessagingException(String.format("Invalid range: %d %d", start, end));
    804             }
    805             return getMessagesInternal(
    806                     searchForUids(String.format("%d:%d NOT DELETED", start, end)), listener);
    807         }
    808 
    809         @Override
    810         public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException {
    811             return getMessages(null, listener);
    812         }
    813 
    814         @Override
    815         public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
    816                 throws MessagingException {
    817             if (uids == null) {
    818                 uids = searchForUids("1:* NOT DELETED");
    819             }
    820             return getMessagesInternal(uids, listener);
    821         }
    822 
    823         public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener)
    824                 throws MessagingException {
    825             final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
    826             for (int i = 0; i < uids.length; i++) {
    827                 final String uid = uids[i];
    828                 final ImapMessage message = new ImapMessage(uid, this);
    829                 messages.add(message);
    830                 if (listener != null) {
    831                     listener.messageRetrieved(message);
    832                 }
    833             }
    834             return messages.toArray(Message.EMPTY_ARRAY);
    835         }
    836 
    837         @Override
    838         public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
    839                 throws MessagingException {
    840             try {
    841                 fetchInternal(messages, fp, listener);
    842             } catch (RuntimeException e) { // Probably a parser error.
    843                 Log.w(Email.LOG_TAG, "Exception detected: " + e.getMessage());
    844                 if (mConnection != null) {
    845                     mConnection.logLastDiscourse();
    846                 }
    847                 throw e;
    848             }
    849         }
    850 
    851         public void fetchInternal(Message[] messages, FetchProfile fp,
    852                 MessageRetrievalListener listener) throws MessagingException {
    853             if (messages.length == 0) {
    854                 return;
    855             }
    856             checkOpen();
    857             HashMap<String, Message> messageMap = new HashMap<String, Message>();
    858             for (Message m : messages) {
    859                 messageMap.put(m.getUid(), m);
    860             }
    861 
    862             /*
    863              * Figure out what command we are going to run:
    864              * FLAGS     - UID FETCH (FLAGS)
    865              * ENVELOPE  - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
    866              *                            HEADER.FIELDS (date subject from content-type to cc)])
    867              * STRUCTURE - UID FETCH (BODYSTRUCTURE)
    868              * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
    869              * BODY      - UID FETCH (BODY.PEEK[])
    870              * Part      - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
    871              */
    872 
    873             final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
    874 
    875             fetchFields.add(ImapConstants.UID);
    876             if (fp.contains(FetchProfile.Item.FLAGS)) {
    877                 fetchFields.add(ImapConstants.FLAGS);
    878             }
    879             if (fp.contains(FetchProfile.Item.ENVELOPE)) {
    880                 fetchFields.add(ImapConstants.INTERNALDATE);
    881                 fetchFields.add(ImapConstants.RFC822_SIZE);
    882                 fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
    883             }
    884             if (fp.contains(FetchProfile.Item.STRUCTURE)) {
    885                 fetchFields.add(ImapConstants.BODYSTRUCTURE);
    886             }
    887 
    888             if (fp.contains(FetchProfile.Item.BODY_SANE)) {
    889                 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
    890             }
    891             if (fp.contains(FetchProfile.Item.BODY)) {
    892                 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
    893             }
    894 
    895             final Part fetchPart = fp.getFirstPart();
    896             if (fetchPart != null) {
    897                 String[] partIds =
    898                         fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
    899                 if (partIds != null) {
    900                     fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE
    901                             + "[" + partIds[0] + "]");
    902                 }
    903             }
    904 
    905             try {
    906                 mConnection.sendCommand(String.format(
    907                         ImapConstants.UID_FETCH + " %s (%s)", joinMessageUids(messages),
    908                         Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
    909                         ), false);
    910                 ImapResponse response;
    911                 int messageNumber = 0;
    912                 do {
    913                     response = null;
    914                     try {
    915                         response = mConnection.readResponse();
    916 
    917                         if (!response.isDataResponse(1, ImapConstants.FETCH)) {
    918                             continue; // Ignore
    919                         }
    920                         final ImapList fetchList = response.getListOrEmpty(2);
    921                         final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID)
    922                                 .getString();
    923                         if (TextUtils.isEmpty(uid)) continue;
    924 
    925                         ImapMessage message = (ImapMessage) messageMap.get(uid);
    926                         if (message == null) continue;
    927 
    928                         if (fp.contains(FetchProfile.Item.FLAGS)) {
    929                             final ImapList flags =
    930                                 fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
    931                             for (int i = 0, count = flags.size(); i < count; i++) {
    932                                 final ImapString flag = flags.getStringOrEmpty(i);
    933                                 if (flag.is(ImapConstants.FLAG_DELETED)) {
    934                                     message.setFlagInternal(Flag.DELETED, true);
    935                                 } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
    936                                     message.setFlagInternal(Flag.ANSWERED, true);
    937                                 } else if (flag.is(ImapConstants.FLAG_SEEN)) {
    938                                     message.setFlagInternal(Flag.SEEN, true);
    939                                 } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
    940                                     message.setFlagInternal(Flag.FLAGGED, true);
    941                                 }
    942                             }
    943                         }
    944                         if (fp.contains(FetchProfile.Item.ENVELOPE)) {
    945                             final Date internalDate = fetchList.getKeyedStringOrEmpty(
    946                                     ImapConstants.INTERNALDATE).getDateOrNull();
    947                             final int size = fetchList.getKeyedStringOrEmpty(
    948                                     ImapConstants.RFC822_SIZE).getNumberOrZero();
    949                             final String header = fetchList.getKeyedStringOrEmpty(
    950                                     ImapConstants.BODY_BRACKET_HEADER, true).getString();
    951 
    952                             message.setInternalDate(internalDate);
    953                             message.setSize(size);
    954                             message.parse(Utility.streamFromAsciiString(header));
    955                         }
    956                         if (fp.contains(FetchProfile.Item.STRUCTURE)) {
    957                             ImapList bs = fetchList.getKeyedListOrEmpty(
    958                                     ImapConstants.BODYSTRUCTURE);
    959                             if (!bs.isEmpty()) {
    960                                 try {
    961                                     parseBodyStructure(bs, message, ImapConstants.TEXT);
    962                                 } catch (MessagingException e) {
    963                                     if (Email.LOGD) {
    964                                         Log.v(Email.LOG_TAG, "Error handling message", e);
    965                                     }
    966                                     message.setBody(null);
    967                                 }
    968                             }
    969                         }
    970                         if (fp.contains(FetchProfile.Item.BODY)
    971                                 || fp.contains(FetchProfile.Item.BODY_SANE)) {
    972                             // Body is keyed by "BODY[...".
    973                             // TOOD Should we accept "RFC822" as well??
    974                             // The old code didn't really check the key, so it accepted any literal
    975                             // that first appeared.
    976                             ImapString body = fetchList.getKeyedStringOrEmpty("BODY[", true);
    977                             InputStream bodyStream = body.getAsStream();
    978                             message.parse(bodyStream);
    979                         }
    980                         if (fetchPart != null && fetchPart.getSize() > 0) {
    981                             InputStream bodyStream =
    982                                     fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
    983                             String contentType = fetchPart.getContentType();
    984                             String contentTransferEncoding = fetchPart.getHeader(
    985                                     MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0];
    986 
    987                             // TODO Don't create 2 temp files.
    988                             // decodeBody creates BinaryTempFileBody, but we could avoid this
    989                             // if we implement ImapStringBody.
    990                             // (We'll need to share a temp file.  Protect it with a ref-count.)
    991                             fetchPart.setBody(MimeUtility.decodeBody(
    992                                     bodyStream,
    993                                     contentTransferEncoding));
    994                         }
    995 
    996                         if (listener != null) {
    997                             listener.messageRetrieved(message);
    998                         }
    999                     } finally {
   1000                         destroyResponses();
   1001                     }
   1002                 } while (!response.isTagged());
   1003             } catch (IOException ioe) {
   1004                 throw ioExceptionHandler(mConnection, ioe);
   1005             }
   1006         }
   1007 
   1008         @Override
   1009         public Flag[] getPermanentFlags() throws MessagingException {
   1010             return PERMANENT_FLAGS;
   1011         }
   1012 
   1013         /**
   1014          * Handle any untagged responses that the caller doesn't care to handle themselves.
   1015          * @param responses
   1016          */
   1017         private void handleUntaggedResponses(List<ImapResponse> responses) {
   1018             for (ImapResponse response : responses) {
   1019                 handleUntaggedResponse(response);
   1020             }
   1021         }
   1022 
   1023         /**
   1024          * Handle an untagged response that the caller doesn't care to handle themselves.
   1025          * @param response
   1026          */
   1027         private void handleUntaggedResponse(ImapResponse response) {
   1028             if (response.isDataResponse(1, ImapConstants.EXISTS)) {
   1029                 mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
   1030             }
   1031         }
   1032 
   1033         private static void parseBodyStructure(ImapList bs, Part part, String id)
   1034                 throws MessagingException {
   1035             if (bs.getElementOrNone(0).isList()) {
   1036                 /*
   1037                  * This is a multipart/*
   1038                  */
   1039                 MimeMultipart mp = new MimeMultipart();
   1040                 for (int i = 0, count = bs.size(); i < count; i++) {
   1041                     ImapElement e = bs.getElementOrNone(i);
   1042                     if (e.isList()) {
   1043                         /*
   1044                          * For each part in the message we're going to add a new BodyPart and parse
   1045                          * into it.
   1046                          */
   1047                         MimeBodyPart bp = new MimeBodyPart();
   1048                         if (id.equals(ImapConstants.TEXT)) {
   1049                             parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
   1050 
   1051                         } else {
   1052                             parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
   1053                         }
   1054                         mp.addBodyPart(bp);
   1055 
   1056                     } else {
   1057                         if (e.isString()) {
   1058                             mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase());
   1059                         }
   1060                         break; // Ignore the rest of the list.
   1061                     }
   1062                 }
   1063                 part.setBody(mp);
   1064             } else {
   1065                 /*
   1066                  * This is a body. We need to add as much information as we can find out about
   1067                  * it to the Part.
   1068                  */
   1069 
   1070                 /*
   1071                  body type
   1072                  body subtype
   1073                  body parameter parenthesized list
   1074                  body id
   1075                  body description
   1076                  body encoding
   1077                  body size
   1078                  */
   1079 
   1080                 final ImapString type = bs.getStringOrEmpty(0);
   1081                 final ImapString subType = bs.getStringOrEmpty(1);
   1082                 final String mimeType =
   1083                         (type.getString() + "/" + subType.getString()).toLowerCase();
   1084 
   1085                 final ImapList bodyParams = bs.getListOrEmpty(2);
   1086                 final ImapString cid = bs.getStringOrEmpty(3);
   1087                 final ImapString encoding = bs.getStringOrEmpty(5);
   1088                 final int size = bs.getStringOrEmpty(6).getNumberOrZero();
   1089 
   1090                 if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
   1091                     // A body type of type MESSAGE and subtype RFC822
   1092                     // contains, immediately after the basic fields, the
   1093                     // envelope structure, body structure, and size in
   1094                     // text lines of the encapsulated message.
   1095                     // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
   1096                     //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
   1097                     /*
   1098                      * This will be caught by fetch and handled appropriately.
   1099                      */
   1100                     throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822
   1101                             + " not yet supported.");
   1102                 }
   1103 
   1104                 /*
   1105                  * Set the content type with as much information as we know right now.
   1106                  */
   1107                 final StringBuilder contentType = new StringBuilder(mimeType);
   1108 
   1109                 /*
   1110                  * If there are body params we might be able to get some more information out
   1111                  * of them.
   1112                  */
   1113                 for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
   1114 
   1115                     // TODO We need to convert " into %22, but
   1116                     // because MimeUtility.getHeaderParameter doesn't recognize it,
   1117                     // we can't fix it for now.
   1118                     contentType.append(String.format(";\n %s=\"%s\"",
   1119                             bodyParams.getStringOrEmpty(i - 1).getString(),
   1120                             bodyParams.getStringOrEmpty(i).getString()));
   1121                 }
   1122 
   1123                 part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
   1124 
   1125                 // Extension items
   1126                 final ImapList bodyDisposition;
   1127 
   1128                 if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
   1129                     // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
   1130                     // So, if it's not a list, use 10th element.
   1131                     // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
   1132                     bodyDisposition = bs.getListOrEmpty(9);
   1133                 } else {
   1134                     bodyDisposition = bs.getListOrEmpty(8);
   1135                 }
   1136 
   1137                 final StringBuilder contentDisposition = new StringBuilder();
   1138 
   1139                 if (bodyDisposition.size() > 0) {
   1140                     final String bodyDisposition0Str =
   1141                             bodyDisposition.getStringOrEmpty(0).getString().toLowerCase();
   1142                     if (!TextUtils.isEmpty(bodyDisposition0Str)) {
   1143                         contentDisposition.append(bodyDisposition0Str);
   1144                     }
   1145 
   1146                     final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
   1147                     if (!bodyDispositionParams.isEmpty()) {
   1148                         /*
   1149                          * If there is body disposition information we can pull some more
   1150                          * information about the attachment out.
   1151                          */
   1152                         for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
   1153 
   1154                             // TODO We need to convert " into %22.  See above.
   1155                             contentDisposition.append(String.format(";\n %s=\"%s\"",
   1156                                     bodyDispositionParams.getStringOrEmpty(i - 1)
   1157                                             .getString().toLowerCase(),
   1158                                     bodyDispositionParams.getStringOrEmpty(i).getString()));
   1159                         }
   1160                     }
   1161                 }
   1162 
   1163                 if ((size > 0)
   1164                         && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size")
   1165                                 == null)) {
   1166                     contentDisposition.append(String.format(";\n size=%d", size));
   1167                 }
   1168 
   1169                 if (contentDisposition.length() > 0) {
   1170                     /*
   1171                      * Set the content disposition containing at least the size. Attachment
   1172                      * handling code will use this down the road.
   1173                      */
   1174                     part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
   1175                             contentDisposition.toString());
   1176                 }
   1177 
   1178                 /*
   1179                  * Set the Content-Transfer-Encoding header. Attachment code will use this
   1180                  * to parse the body.
   1181                  */
   1182                 if (!encoding.isEmpty()) {
   1183                     part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
   1184                             encoding.getString());
   1185                 }
   1186 
   1187                 /*
   1188                  * Set the Content-ID header.
   1189                  */
   1190                 if (!cid.isEmpty()) {
   1191                     part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
   1192                 }
   1193 
   1194                 if (size > 0) {
   1195                     if (part instanceof ImapMessage) {
   1196                         ((ImapMessage) part).setSize(size);
   1197                     } else if (part instanceof MimeBodyPart) {
   1198                         ((MimeBodyPart) part).setSize(size);
   1199                     } else {
   1200                         throw new MessagingException("Unknown part type " + part.toString());
   1201                     }
   1202                 }
   1203                 part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
   1204             }
   1205 
   1206         }
   1207 
   1208         /**
   1209          * Appends the given messages to the selected folder. This implementation also determines
   1210          * the new UID of the given message on the IMAP server and sets the Message's UID to the
   1211          * new server UID.
   1212          */
   1213         @Override
   1214         public void appendMessages(Message[] messages) throws MessagingException {
   1215             checkOpen();
   1216             try {
   1217                 for (Message message : messages) {
   1218                     // Create output count
   1219                     CountingOutputStream out = new CountingOutputStream();
   1220                     EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out);
   1221                     message.writeTo(eolOut);
   1222                     eolOut.flush();
   1223                     // Create flag list (most often this will be "\SEEN")
   1224                     String flagList = "";
   1225                     Flag[] flags = message.getFlags();
   1226                     if (flags.length > 0) {
   1227                         StringBuilder sb = new StringBuilder();
   1228                         for (int i = 0, count = flags.length; i < count; i++) {
   1229                             Flag flag = flags[i];
   1230                             if (flag == Flag.SEEN) {
   1231                                 sb.append(" " + ImapConstants.FLAG_SEEN);
   1232                             } else if (flag == Flag.FLAGGED) {
   1233                                 sb.append(" " + ImapConstants.FLAG_FLAGGED);
   1234                             }
   1235                         }
   1236                         if (sb.length() > 0) {
   1237                             flagList = sb.substring(1);
   1238                         }
   1239                     }
   1240 
   1241                     mConnection.sendCommand(
   1242                             String.format(ImapConstants.APPEND + " \"%s\" (%s) {%d}",
   1243                                     encodeFolderName(mName),
   1244                                     flagList,
   1245                                     out.getCount()), false);
   1246                     ImapResponse response;
   1247                     do {
   1248                         response = mConnection.readResponse();
   1249                         if (response.isContinuationRequest()) {
   1250                             eolOut = new EOLConvertingOutputStream(
   1251                                     mConnection.mTransport.getOutputStream());
   1252                             message.writeTo(eolOut);
   1253                             eolOut.write('\r');
   1254                             eolOut.write('\n');
   1255                             eolOut.flush();
   1256                         } else if (!response.isTagged()) {
   1257                             handleUntaggedResponse(response);
   1258                         }
   1259                     } while (!response.isTagged());
   1260 
   1261                     // TODO Why not check the response?
   1262 
   1263                     /*
   1264                      * Try to recover the UID of the message from an APPENDUID response.
   1265                      * e.g. 11 OK [APPENDUID 2 238268] APPEND completed
   1266                      */
   1267                     final ImapList appendList = response.getListOrEmpty(1);
   1268                     if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) {
   1269                         String serverUid = appendList.getStringOrEmpty(2).getString();
   1270                         if (!TextUtils.isEmpty(serverUid)) {
   1271                             message.setUid(serverUid);
   1272                             continue;
   1273                         }
   1274                     }
   1275 
   1276                     /*
   1277                      * Try to find the UID of the message we just appended using the
   1278                      * Message-ID header.  If there are more than one response, take the
   1279                      * last one, as it's most likely the newest (the one we just uploaded).
   1280                      */
   1281                     String messageId = message.getMessageId();
   1282                     if (messageId == null || messageId.length() == 0) {
   1283                         continue;
   1284                     }
   1285                     String[] uids = searchForUids(
   1286                             String.format("(HEADER MESSAGE-ID %s)", messageId));
   1287                     if (uids.length > 0) {
   1288                         message.setUid(uids[0]);
   1289                     }
   1290                 }
   1291             } catch (IOException ioe) {
   1292                 throw ioExceptionHandler(mConnection, ioe);
   1293             } finally {
   1294                 destroyResponses();
   1295             }
   1296         }
   1297 
   1298         @Override
   1299         public Message[] expunge() throws MessagingException {
   1300             checkOpen();
   1301             try {
   1302                 handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
   1303             } catch (IOException ioe) {
   1304                 throw ioExceptionHandler(mConnection, ioe);
   1305             } finally {
   1306                 destroyResponses();
   1307             }
   1308             return null;
   1309         }
   1310 
   1311         @Override
   1312         public void setFlags(Message[] messages, Flag[] flags, boolean value)
   1313                 throws MessagingException {
   1314             checkOpen();
   1315 
   1316             String allFlags = "";
   1317             if (flags.length > 0) {
   1318                 StringBuilder flagList = new StringBuilder();
   1319                 for (int i = 0, count = flags.length; i < count; i++) {
   1320                     Flag flag = flags[i];
   1321                     if (flag == Flag.SEEN) {
   1322                         flagList.append(" " + ImapConstants.FLAG_SEEN);
   1323                     } else if (flag == Flag.DELETED) {
   1324                         flagList.append(" " + ImapConstants.FLAG_DELETED);
   1325                     } else if (flag == Flag.FLAGGED) {
   1326                         flagList.append(" " + ImapConstants.FLAG_FLAGGED);
   1327                     }
   1328                 }
   1329                 allFlags = flagList.substring(1);
   1330             }
   1331             try {
   1332                 mConnection.executeSimpleCommand(String.format(
   1333                         ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
   1334                         joinMessageUids(messages),
   1335                         value ? "+" : "-",
   1336                         allFlags));
   1337 
   1338             } catch (IOException ioe) {
   1339                 throw ioExceptionHandler(mConnection, ioe);
   1340             } finally {
   1341                 destroyResponses();
   1342             }
   1343         }
   1344 
   1345         private void checkOpen() throws MessagingException {
   1346             if (!isOpen()) {
   1347                 throw new MessagingException("Folder " + mName + " is not open.");
   1348             }
   1349         }
   1350 
   1351         private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe)
   1352                 throws MessagingException {
   1353             if (Email.DEBUG) {
   1354                 Log.d(Email.LOG_TAG, "IO Exception detected: ", ioe);
   1355             }
   1356             connection.destroyResponses();
   1357             connection.close();
   1358             if (connection == mConnection) {
   1359                 mConnection = null; // To prevent close() from returning the connection to the pool.
   1360                 close(false);
   1361             }
   1362             return new MessagingException("IO Error", ioe);
   1363         }
   1364 
   1365         @Override
   1366         public boolean equals(Object o) {
   1367             if (o instanceof ImapFolder) {
   1368                 return ((ImapFolder)o).mName.equals(mName);
   1369             }
   1370             return super.equals(o);
   1371         }
   1372 
   1373         @Override
   1374         public Message createMessage(String uid) throws MessagingException {
   1375             return new ImapMessage(uid, this);
   1376         }
   1377     }
   1378 
   1379     /**
   1380      * A cacheable class that stores the details for a single IMAP connection.
   1381      */
   1382     class ImapConnection {
   1383         private static final String IMAP_DEDACTED_LOG = "[IMAP command redacted]";
   1384         private Transport mTransport;
   1385         private ImapResponseParser mParser;
   1386         /** # of command/response lines to log upon crash. */
   1387         private static final int DISCOURSE_LOGGER_SIZE = 64;
   1388         private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
   1389 
   1390         public void open() throws IOException, MessagingException {
   1391             if (mTransport != null && mTransport.isOpen()) {
   1392                 return;
   1393             }
   1394 
   1395             try {
   1396                 // copy configuration into a clean transport, if necessary
   1397                 if (mTransport == null) {
   1398                     mTransport = mRootTransport.newInstanceWithConfiguration();
   1399                 }
   1400 
   1401                 mTransport.open();
   1402                 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
   1403 
   1404                 createParser();
   1405 
   1406                 // BANNER
   1407                 mParser.readResponse();
   1408 
   1409                 // CAPABILITY
   1410                 ImapResponse capabilityResponse = null;
   1411                 for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
   1412                     if (r.is(0, ImapConstants.CAPABILITY)) {
   1413                         capabilityResponse = r;
   1414                         break;
   1415                     }
   1416                 }
   1417                 if (capabilityResponse == null) {
   1418                     throw new MessagingException("Invalid CAPABILITY response received");
   1419                 }
   1420 
   1421                 if (mTransport.canTryTlsSecurity()) {
   1422                     if (capabilityResponse.contains(ImapConstants.STARTTLS)) {
   1423                         // STARTTLS
   1424                         executeSimpleCommand(ImapConstants.STARTTLS);
   1425 
   1426                         mTransport.reopenTls();
   1427                         mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
   1428                         createParser();
   1429                     } else {
   1430                         if (Config.LOGD && Email.DEBUG) {
   1431                             Log.d(Email.LOG_TAG, "TLS not supported but required");
   1432                         }
   1433                         throw new MessagingException(MessagingException.TLS_REQUIRED);
   1434                     }
   1435                 }
   1436 
   1437                 // Assign user-agent string (for RFC2971 ID command)
   1438                 String mUserAgent = getImapId(mContext, mUsername, mRootTransport.getHost(),
   1439                         capabilityResponse);
   1440                 if (mUserAgent != null) {
   1441                     mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
   1442                 } else if (DEBUG_FORCE_SEND_ID) {
   1443                     mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
   1444                 }
   1445                 // else: mIdPhrase = null, no ID will be emitted
   1446 
   1447                 // Send user-agent in an RFC2971 ID command
   1448                 if (mIdPhrase != null) {
   1449                     try {
   1450                         executeSimpleCommand(mIdPhrase);
   1451                     } catch (ImapException ie) {
   1452                         // Log for debugging, but this is not a fatal problem.
   1453                         if (Config.LOGD && Email.DEBUG) {
   1454                             Log.d(Email.LOG_TAG, ie.toString());
   1455                         }
   1456                     } catch (IOException ioe) {
   1457                         // Special case to handle malformed OK responses and ignore them.
   1458                         // A true IOException will recur on the following login steps
   1459                         // This can go away after the parser is fixed - see bug 2138981 for details
   1460                     }
   1461                 }
   1462 
   1463                 try {
   1464                     // TODO eventually we need to add additional authentication
   1465                     // options such as SASL
   1466                     executeSimpleCommand(mLoginPhrase, true);
   1467                 } catch (ImapException ie) {
   1468                     if (Config.LOGD && Email.DEBUG) {
   1469                         Log.d(Email.LOG_TAG, ie.toString());
   1470                     }
   1471                     throw new AuthenticationFailedException(ie.getAlertText(), ie);
   1472 
   1473                 } catch (MessagingException me) {
   1474                     throw new AuthenticationFailedException(null, me);
   1475                 }
   1476             } catch (SSLException e) {
   1477                 if (Config.LOGD && Email.DEBUG) {
   1478                     Log.d(Email.LOG_TAG, e.toString());
   1479                 }
   1480                 throw new CertificateValidationException(e.getMessage(), e);
   1481             } catch (IOException ioe) {
   1482                 // NOTE:  Unlike similar code in POP3, I'm going to rethrow as-is.  There is a lot
   1483                 // of other code here that catches IOException and I don't want to break it.
   1484                 // This catch is only here to enhance logging of connection-time issues.
   1485                 if (Config.LOGD && Email.DEBUG) {
   1486                     Log.d(Email.LOG_TAG, ioe.toString());
   1487                 }
   1488                 throw ioe;
   1489             } finally {
   1490                 destroyResponses();
   1491             }
   1492         }
   1493 
   1494         public void close() {
   1495             if (mTransport != null) {
   1496                 mTransport.close();
   1497                 mTransport = null;
   1498             }
   1499         }
   1500 
   1501         /**
   1502          * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
   1503          * set it to {@link #mParser}.
   1504          *
   1505          * If we already have an {@link ImapResponseParser}, we
   1506          * {@link #destroyResponses()} and throw it away.
   1507          */
   1508         private void createParser() {
   1509             destroyResponses();
   1510             mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse);
   1511         }
   1512 
   1513         public void destroyResponses() {
   1514             if (mParser != null) {
   1515                 mParser.destroyResponses();
   1516             }
   1517         }
   1518 
   1519         /* package */ boolean isTransportOpenForTest() {
   1520             return mTransport != null ? mTransport.isOpen() : false;
   1521         }
   1522 
   1523         public ImapResponse readResponse() throws IOException, MessagingException {
   1524             return mParser.readResponse();
   1525         }
   1526 
   1527         /**
   1528          * Send a single command to the server.  The command will be preceded by an IMAP command
   1529          * tag and followed by \r\n (caller need not supply them).
   1530          *
   1531          * @param command The command to send to the server
   1532          * @param sensitive If true, the command will not be logged
   1533          * @return Returns the command tag that was sent
   1534          */
   1535         public String sendCommand(String command, boolean sensitive)
   1536             throws MessagingException, IOException {
   1537             open();
   1538             String tag = Integer.toString(mNextCommandTag.incrementAndGet());
   1539             String commandToSend = tag + " " + command;
   1540             mTransport.writeLine(commandToSend, sensitive ? IMAP_DEDACTED_LOG : null);
   1541             mDiscourse.addSentCommand(sensitive ? IMAP_DEDACTED_LOG : commandToSend);
   1542             return tag;
   1543         }
   1544 
   1545         public List<ImapResponse> executeSimpleCommand(String command) throws IOException,
   1546                 MessagingException {
   1547             return executeSimpleCommand(command, false);
   1548         }
   1549 
   1550         public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
   1551                 throws IOException, MessagingException {
   1552             String tag = sendCommand(command, sensitive);
   1553             ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
   1554             ImapResponse response;
   1555             do {
   1556                 response = mParser.readResponse();
   1557                 responses.add(response);
   1558             } while (!response.isTagged());
   1559             if (!response.isOk()) {
   1560                 final String toString = response.toString();
   1561                 final String alert = response.getAlertTextOrEmpty().getString();
   1562                 destroyResponses();
   1563                 throw new ImapException(toString, alert);
   1564             }
   1565             return responses;
   1566         }
   1567 
   1568         /** @see ImapResponseParser#logLastDiscourse() */
   1569         public void logLastDiscourse() {
   1570             mDiscourse.logLastDiscourse();
   1571         }
   1572     }
   1573 
   1574     static class ImapMessage extends MimeMessage {
   1575         ImapMessage(String uid, Folder folder) throws MessagingException {
   1576             this.mUid = uid;
   1577             this.mFolder = folder;
   1578         }
   1579 
   1580         public void setSize(int size) {
   1581             this.mSize = size;
   1582         }
   1583 
   1584         @Override
   1585         public void parse(InputStream in) throws IOException, MessagingException {
   1586             super.parse(in);
   1587         }
   1588 
   1589         public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
   1590             super.setFlag(flag, set);
   1591         }
   1592 
   1593         @Override
   1594         public void setFlag(Flag flag, boolean set) throws MessagingException {
   1595             super.setFlag(flag, set);
   1596             mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
   1597         }
   1598     }
   1599 
   1600     static class ImapException extends MessagingException {
   1601         String mAlertText;
   1602 
   1603         public ImapException(String message, String alertText, Throwable throwable) {
   1604             super(message, throwable);
   1605             this.mAlertText = alertText;
   1606         }
   1607 
   1608         public ImapException(String message, String alertText) {
   1609             super(message);
   1610             this.mAlertText = alertText;
   1611         }
   1612 
   1613         public String getAlertText() {
   1614             return mAlertText;
   1615         }
   1616 
   1617         public void setAlertText(String alertText) {
   1618             mAlertText = alertText;
   1619         }
   1620     }
   1621 }
   1622