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