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 android.content.Context;
     20 import android.os.Build;
     21 import android.os.Bundle;
     22 import android.telephony.TelephonyManager;
     23 import android.text.TextUtils;
     24 import android.util.Base64;
     25 
     26 import com.android.email.LegacyConversions;
     27 import com.android.email.Preferences;
     28 import com.android.email.mail.Store;
     29 import com.android.email.mail.store.imap.ImapConstants;
     30 import com.android.email.mail.store.imap.ImapResponse;
     31 import com.android.email.mail.store.imap.ImapString;
     32 import com.android.email.mail.transport.MailTransport;
     33 import com.android.emailcommon.Logging;
     34 import com.android.emailcommon.VendorPolicyLoader;
     35 import com.android.emailcommon.internet.MimeMessage;
     36 import com.android.emailcommon.mail.AuthenticationFailedException;
     37 import com.android.emailcommon.mail.Flag;
     38 import com.android.emailcommon.mail.Folder;
     39 import com.android.emailcommon.mail.Message;
     40 import com.android.emailcommon.mail.MessagingException;
     41 import com.android.emailcommon.provider.Account;
     42 import com.android.emailcommon.provider.Credential;
     43 import com.android.emailcommon.provider.HostAuth;
     44 import com.android.emailcommon.provider.Mailbox;
     45 import com.android.emailcommon.service.EmailServiceProxy;
     46 import com.android.emailcommon.utility.Utility;
     47 import com.android.mail.utils.LogUtils;
     48 import com.beetstra.jutf7.CharsetProvider;
     49 import com.google.common.annotations.VisibleForTesting;
     50 
     51 import java.io.IOException;
     52 import java.io.InputStream;
     53 import java.nio.ByteBuffer;
     54 import java.nio.charset.Charset;
     55 import java.security.MessageDigest;
     56 import java.security.NoSuchAlgorithmException;
     57 import java.util.Collection;
     58 import java.util.HashMap;
     59 import java.util.List;
     60 import java.util.Set;
     61 import java.util.concurrent.ConcurrentLinkedQueue;
     62 import java.util.regex.Pattern;
     63 
     64 
     65 /**
     66  * <pre>
     67  * TODO Need to start keeping track of UIDVALIDITY
     68  * TODO Need a default response handler for things like folder updates
     69  * TODO In fetch(), if we need a ImapMessage and were given
     70  *      something else we can try to do a pre-fetch first.
     71  * TODO Collect ALERT messages and show them to users.
     72  *
     73  * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for
     74  * certain information in a FETCH command, the server may return the requested
     75  * information in any order, not necessarily in the order that it was requested.
     76  * Further, the server may return the information in separate FETCH responses
     77  * and may also return information that was not explicitly requested (to reflect
     78  * to the client changes in the state of the subject message).
     79  * </pre>
     80  */
     81 public class ImapStore extends Store {
     82     /** Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. */
     83     private static final Charset MODIFIED_UTF_7_CHARSET =
     84             new CharsetProvider().charsetForName("X-RFC-3501");
     85 
     86     @VisibleForTesting static String sImapId = null;
     87     @VisibleForTesting String mPathPrefix;
     88     @VisibleForTesting String mPathSeparator;
     89 
     90     private boolean mUseOAuth;
     91 
     92     private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool =
     93             new ConcurrentLinkedQueue<ImapConnection>();
     94 
     95     /**
     96      * Static named constructor.
     97      */
     98     public static Store newInstance(Account account, Context context) throws MessagingException {
     99         return new ImapStore(context, account);
    100     }
    101 
    102     /**
    103      * Creates a new store for the given account. Always use
    104      * {@link #newInstance(Account, Context)} to create an IMAP store.
    105      */
    106     private ImapStore(Context context, Account account) throws MessagingException {
    107         mContext = context;
    108         mAccount = account;
    109 
    110         HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
    111         if (recvAuth == null) {
    112             throw new MessagingException("No HostAuth in ImapStore?");
    113         }
    114         mTransport = new MailTransport(context, "IMAP", recvAuth);
    115 
    116         String[] userInfo = recvAuth.getLogin();
    117         mUsername = userInfo[0];
    118         mPassword = userInfo[1];
    119         final Credential cred = recvAuth.getCredential(context);
    120         mUseOAuth = (cred != null);
    121         mPathPrefix = recvAuth.mDomain;
    122     }
    123 
    124     boolean getUseOAuth() {
    125         return mUseOAuth;
    126     }
    127 
    128     String getUsername() {
    129         return mUsername;
    130     }
    131 
    132     String getPassword() {
    133         return mPassword;
    134     }
    135 
    136     @VisibleForTesting
    137     Collection<ImapConnection> getConnectionPoolForTest() {
    138         return mConnectionPool;
    139     }
    140 
    141     /**
    142      * For testing only.  Injects a different root transport (it will be copied using
    143      * newInstanceWithConfiguration() each time IMAP sets up a new channel).  The transport
    144      * should already be set up and ready to use.  Do not use for real code.
    145      * @param testTransport The Transport to inject and use for all future communication.
    146      */
    147     @VisibleForTesting
    148     void setTransportForTest(MailTransport testTransport) {
    149         mTransport = testTransport;
    150     }
    151 
    152     /**
    153      * Return, or create and return, an string suitable for use in an IMAP ID message.
    154      * This is constructed similarly to the way the browser sets up its user-agent strings.
    155      * See RFC 2971 for more details.  The output of this command will be a series of key-value
    156      * pairs delimited by spaces (there is no point in returning a structured result because
    157      * this will be sent as-is to the IMAP server).  No tokens, parenthesis or "ID" are included,
    158      * because some connections may append additional values.
    159      *
    160      * The following IMAP ID keys may be included:
    161      *   name                   Android package name of the program
    162      *   os                     "android"
    163      *   os-version             "version; model; build-id"
    164      *   vendor                 Vendor of the client/server
    165      *   x-android-device-model Model (only revealed if release build)
    166      *   x-android-net-operator Mobile network operator (if known)
    167      *   AGUID                  A device+account UID
    168      *
    169      * In addition, a vendor policy .apk can append key/value pairs.
    170      *
    171      * @param userName the username of the account
    172      * @param host the host (server) of the account
    173      * @param capabilities a list of the capabilities from the server
    174      * @return a String for use in an IMAP ID message.
    175      */
    176     public static String getImapId(Context context, String userName, String host,
    177             String capabilities) {
    178         // The first section is global to all IMAP connections, and generates the fixed
    179         // values in any IMAP ID message
    180         synchronized (ImapStore.class) {
    181             if (sImapId == null) {
    182                 TelephonyManager tm =
    183                         (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    184                 String networkOperator = tm.getNetworkOperatorName();
    185                 if (networkOperator == null) networkOperator = "";
    186 
    187                 sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE,
    188                         Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER,
    189                         networkOperator);
    190             }
    191         }
    192 
    193         // This section is per Store, and adds in a dynamic elements like UID's.
    194         // We don't cache the result of this work, because the caller does anyway.
    195         StringBuilder id = new StringBuilder(sImapId);
    196 
    197         // Optionally add any vendor-supplied id keys
    198         String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host,
    199                 capabilities);
    200         if (vendorId != null) {
    201             id.append(' ');
    202             id.append(vendorId);
    203         }
    204 
    205         // Generate a UID that mixes a "stable" device UID with the email address
    206         try {
    207             String devUID = Preferences.getPreferences(context).getDeviceUID();
    208             MessageDigest messageDigest;
    209             messageDigest = MessageDigest.getInstance("SHA-1");
    210             messageDigest.update(userName.getBytes());
    211             messageDigest.update(devUID.getBytes());
    212             byte[] uid = messageDigest.digest();
    213             String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP);
    214             id.append(" \"AGUID\" \"");
    215             id.append(hexUid);
    216             id.append('\"');
    217         } catch (NoSuchAlgorithmException e) {
    218             LogUtils.d(Logging.LOG_TAG, "couldn't obtain SHA-1 hash for device UID");
    219         }
    220         return id.toString();
    221     }
    222 
    223     /**
    224      * Helper function that actually builds the static part of the IMAP ID string.  This is
    225      * separated from getImapId for testability.  There is no escaping or encoding in IMAP ID so
    226      * any rogue chars must be filtered here.
    227      *
    228      * @param packageName context.getPackageName()
    229      * @param version Build.VERSION.RELEASE
    230      * @param codeName Build.VERSION.CODENAME
    231      * @param model Build.MODEL
    232      * @param id Build.ID
    233      * @param vendor Build.MANUFACTURER
    234      * @param networkOperator TelephonyManager.getNetworkOperatorName()
    235      * @return the static (never changes) portion of the IMAP ID
    236      */
    237     @VisibleForTesting
    238     static String makeCommonImapId(String packageName, String version,
    239             String codeName, String model, String id, String vendor, String networkOperator) {
    240 
    241         // Before building up IMAP ID string, pre-filter the input strings for "legal" chars
    242         // This is using a fairly arbitrary char set intended to pass through most reasonable
    243         // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space>
    244         // The most important thing is *not* to pass parens, quotes, or CRLF, which would break
    245         // the format of the IMAP ID list.
    246         Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]");
    247         packageName = p.matcher(packageName).replaceAll("");
    248         version = p.matcher(version).replaceAll("");
    249         codeName = p.matcher(codeName).replaceAll("");
    250         model = p.matcher(model).replaceAll("");
    251         id = p.matcher(id).replaceAll("");
    252         vendor = p.matcher(vendor).replaceAll("");
    253         networkOperator = p.matcher(networkOperator).replaceAll("");
    254 
    255         // "name" "com.android.email"
    256         StringBuilder sb = new StringBuilder("\"name\" \"");
    257         sb.append(packageName);
    258         sb.append("\"");
    259 
    260         // "os" "android"
    261         sb.append(" \"os\" \"android\"");
    262 
    263         // "os-version" "version; build-id"
    264         sb.append(" \"os-version\" \"");
    265         if (version.length() > 0) {
    266             sb.append(version);
    267         } else {
    268             // default to "1.0"
    269             sb.append("1.0");
    270         }
    271         // add the build ID or build #
    272         if (id.length() > 0) {
    273             sb.append("; ");
    274             sb.append(id);
    275         }
    276         sb.append("\"");
    277 
    278         // "vendor" "the vendor"
    279         if (vendor.length() > 0) {
    280             sb.append(" \"vendor\" \"");
    281             sb.append(vendor);
    282             sb.append("\"");
    283         }
    284 
    285         // "x-android-device-model" the device model (on release builds only)
    286         if ("REL".equals(codeName)) {
    287             if (model.length() > 0) {
    288                 sb.append(" \"x-android-device-model\" \"");
    289                 sb.append(model);
    290                 sb.append("\"");
    291             }
    292         }
    293 
    294         // "x-android-mobile-net-operator" "name of network operator"
    295         if (networkOperator.length() > 0) {
    296             sb.append(" \"x-android-mobile-net-operator\" \"");
    297             sb.append(networkOperator);
    298             sb.append("\"");
    299         }
    300 
    301         return sb.toString();
    302     }
    303 
    304 
    305     @Override
    306     public Folder getFolder(String name) {
    307         return new ImapFolder(this, name);
    308     }
    309 
    310     /**
    311      * Creates a mailbox hierarchy out of the flat data provided by the server.
    312      */
    313     @VisibleForTesting
    314     static void createHierarchy(HashMap<String, ImapFolder> mailboxes) {
    315         Set<String> pathnames = mailboxes.keySet();
    316         for (String path : pathnames) {
    317             final ImapFolder folder = mailboxes.get(path);
    318             final Mailbox mailbox = folder.mMailbox;
    319             int delimiterIdx = mailbox.mServerId.lastIndexOf(mailbox.mDelimiter);
    320             long parentKey = Mailbox.NO_MAILBOX;
    321             String parentPath = null;
    322             if (delimiterIdx != -1) {
    323                 parentPath = path.substring(0, delimiterIdx);
    324                 final ImapFolder parentFolder = mailboxes.get(parentPath);
    325                 final Mailbox parentMailbox = (parentFolder == null) ? null : parentFolder.mMailbox;
    326                 if (parentMailbox != null) {
    327                     parentKey = parentMailbox.mId;
    328                     parentMailbox.mFlags
    329                             |= (Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE);
    330                 }
    331             }
    332             mailbox.mParentKey = parentKey;
    333             mailbox.mParentServerId = parentPath;
    334         }
    335     }
    336 
    337     /**
    338      * Creates a {@link Folder} and associated {@link Mailbox}. If the folder does not already
    339      * exist in the local database, a new row will immediately be created in the mailbox table.
    340      * Otherwise, the existing row will be used. Any changes to existing rows, will not be stored
    341      * to the database immediately.
    342      * @param accountId The ID of the account the mailbox is to be associated with
    343      * @param mailboxPath The path of the mailbox to add
    344      * @param delimiter A path delimiter. May be {@code null} if there is no delimiter.
    345      * @param selectable If {@code true}, the mailbox can be selected and used to store messages.
    346      * @param mailbox If not null, mailbox is used instead of querying for the Mailbox.
    347      */
    348     private ImapFolder addMailbox(Context context, long accountId, String mailboxPath,
    349             char delimiter, boolean selectable, Mailbox mailbox) {
    350         // TODO: pass in the mailbox type, or do a proper lookup here
    351         final int mailboxType;
    352         if (mailbox == null) {
    353             mailboxType = LegacyConversions.inferMailboxTypeFromName(context, mailboxPath);
    354             mailbox = Mailbox.getMailboxForPath(context, accountId, mailboxPath);
    355         } else {
    356             mailboxType = mailbox.mType;
    357         }
    358         final ImapFolder folder = (ImapFolder) getFolder(mailboxPath);
    359         if (mailbox.isSaved()) {
    360             // existing mailbox
    361             // mailbox retrieved from database; save hash _before_ updating fields
    362             folder.mHash = mailbox.getHashes();
    363         }
    364         updateMailbox(mailbox, accountId, mailboxPath, delimiter, selectable, mailboxType);
    365         if (folder.mHash == null) {
    366             // new mailbox
    367             // save hash after updating. allows tracking changes if the mailbox is saved
    368             // outside of #saveMailboxList()
    369             folder.mHash = mailbox.getHashes();
    370             // We must save this here to make sure we have a valid ID for later
    371             mailbox.save(mContext);
    372         }
    373         folder.mMailbox = mailbox;
    374         return folder;
    375     }
    376 
    377     /**
    378      * Persists the folders in the given list.
    379      */
    380     private static void saveMailboxList(Context context, HashMap<String, ImapFolder> folderMap) {
    381         for (ImapFolder imapFolder : folderMap.values()) {
    382             imapFolder.save(context);
    383         }
    384     }
    385 
    386     @Override
    387     public Folder[] updateFolders() throws MessagingException {
    388         // TODO: There is nothing that ever closes this connection. Trouble is, it's not exactly
    389         // clear when we should close it, we'd like to keep it open until we're really done
    390         // using it.
    391         ImapConnection connection = getConnection();
    392         try {
    393             final HashMap<String, ImapFolder> mailboxes = new HashMap<String, ImapFolder>();
    394             // Establish a connection to the IMAP server; if necessary
    395             // This ensures a valid prefix if the prefix is automatically set by the server
    396             connection.executeSimpleCommand(ImapConstants.NOOP);
    397             String imapCommand = ImapConstants.LIST + " \"\" \"*\"";
    398             if (mPathPrefix != null) {
    399                 imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\"";
    400             }
    401             List<ImapResponse> responses = connection.executeSimpleCommand(imapCommand);
    402             for (ImapResponse response : responses) {
    403                 // S: * LIST (\Noselect) "/" ~/Mail/foo
    404                 if (response.isDataResponse(0, ImapConstants.LIST)) {
    405                     // Get folder name.
    406                     ImapString encodedFolder = response.getStringOrEmpty(3);
    407                     if (encodedFolder.isEmpty()) continue;
    408 
    409                     String folderName = decodeFolderName(encodedFolder.getString(), mPathPrefix);
    410 
    411                     if (ImapConstants.INBOX.equalsIgnoreCase(folderName)) continue;
    412 
    413                     // Parse attributes.
    414                     boolean selectable =
    415                         !response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT);
    416                     String delimiter = response.getStringOrEmpty(2).getString();
    417                     char delimiterChar = '\0';
    418                     if (!TextUtils.isEmpty(delimiter)) {
    419                         delimiterChar = delimiter.charAt(0);
    420                     }
    421                     ImapFolder folder = addMailbox(
    422                             mContext, mAccount.mId, folderName, delimiterChar, selectable, null);
    423                     mailboxes.put(folderName, folder);
    424                 }
    425             }
    426 
    427             // In order to properly map INBOX -> Inbox, handle it as a special case.
    428             final Mailbox inbox =
    429                     Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
    430             final ImapFolder newFolder = addMailbox(
    431                     mContext, mAccount.mId, inbox.mServerId, '\0', true /*selectable*/, inbox);
    432             mailboxes.put(ImapConstants.INBOX, newFolder);
    433 
    434             createHierarchy(mailboxes);
    435             saveMailboxList(mContext, mailboxes);
    436             return mailboxes.values().toArray(new Folder[mailboxes.size()]);
    437         } catch (IOException ioe) {
    438             connection.close();
    439             throw new MessagingException("Unable to get folder list", ioe);
    440         } catch (AuthenticationFailedException afe) {
    441             // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT
    442             // commands to the server
    443             connection.destroyResponses();
    444             connection = null;
    445             throw afe;
    446         } finally {
    447             if (connection != null) {
    448                 // We keep our connection out of the pool as long as we are using it, then
    449                 // put it back into the pool so it can be reused.
    450                 poolConnection(connection);
    451             }
    452         }
    453     }
    454 
    455     @Override
    456     public Bundle checkSettings() throws MessagingException {
    457         int result = MessagingException.NO_ERROR;
    458         Bundle bundle = new Bundle();
    459         // TODO: why doesn't this use getConnection()? I guess this is only done during setup,
    460         // so there's need to look for a pooled connection?
    461         // But then why doesn't it use poolConnection() after it's done?
    462         ImapConnection connection = new ImapConnection(this);
    463         try {
    464             connection.open();
    465             connection.close();
    466         } catch (IOException ioe) {
    467             bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage());
    468             result = MessagingException.IOERROR;
    469         } finally {
    470             connection.destroyResponses();
    471         }
    472         bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
    473         return bundle;
    474     }
    475 
    476     /**
    477      * Returns whether or not the prefix has been set by the user. This can be determined by
    478      * the fact that the prefix is set, but, the path separator is not set.
    479      */
    480     boolean isUserPrefixSet() {
    481         return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix);
    482     }
    483 
    484     /** Sets the path separator */
    485     void setPathSeparator(String pathSeparator) {
    486         mPathSeparator = pathSeparator;
    487     }
    488 
    489     /** Sets the prefix */
    490     void setPathPrefix(String pathPrefix) {
    491         mPathPrefix = pathPrefix;
    492     }
    493 
    494     /** Gets the context for this store */
    495     Context getContext() {
    496         return mContext;
    497     }
    498 
    499     /** Returns a clone of the transport associated with this store. */
    500     MailTransport cloneTransport() {
    501         return mTransport.clone();
    502     }
    503 
    504     /**
    505      * Fixes the path prefix, if necessary. The path prefix must always end with the
    506      * path separator.
    507      */
    508     void ensurePrefixIsValid() {
    509         // Make sure the path prefix ends with the path separator
    510         if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) {
    511             if (!mPathPrefix.endsWith(mPathSeparator)) {
    512                 mPathPrefix = mPathPrefix + mPathSeparator;
    513             }
    514         }
    515     }
    516 
    517     /**
    518      * Gets a connection if one is available from the pool, or creates a new one if not.
    519      */
    520     ImapConnection getConnection() {
    521         // TODO Why would we ever have (or need to have) more than one active connection?
    522         // TODO We set new username/password each time, but we don't actually close the transport
    523         // when we do this. So if that information has changed, this connection will fail.
    524         ImapConnection connection;
    525         while ((connection = mConnectionPool.poll()) != null) {
    526             try {
    527                 connection.setStore(this);
    528                 connection.executeSimpleCommand(ImapConstants.NOOP);
    529                 break;
    530             } catch (MessagingException e) {
    531                 // Fall through
    532             } catch (IOException e) {
    533                 // Fall through
    534             }
    535             connection.close();
    536         }
    537 
    538         if (connection == null) {
    539             connection = new ImapConnection(this);
    540         }
    541         return connection;
    542     }
    543 
    544     /**
    545      * Save a {@link ImapConnection} in the pool for reuse. Any responses associated with the
    546      * connection are destroyed before adding the connection to the pool.
    547      */
    548     void poolConnection(ImapConnection connection) {
    549         if (connection != null) {
    550             connection.destroyResponses();
    551             mConnectionPool.add(connection);
    552         }
    553     }
    554 
    555     /**
    556      * Prepends the folder name with the given prefix and UTF-7 encodes it.
    557      */
    558     static String encodeFolderName(String name, String prefix) {
    559         // do NOT add the prefix to the special name "INBOX"
    560         if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name;
    561 
    562         // Prepend prefix
    563         if (prefix != null) {
    564             name = prefix + name;
    565         }
    566 
    567         // TODO bypass the conversion if name doesn't have special char.
    568         ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name);
    569         byte[] b = new byte[bb.limit()];
    570         bb.get(b);
    571 
    572         return Utility.fromAscii(b);
    573     }
    574 
    575     /**
    576      * UTF-7 decodes the folder name and removes the given path prefix.
    577      */
    578     static String decodeFolderName(String name, String prefix) {
    579         // TODO bypass the conversion if name doesn't have special char.
    580         String folder;
    581         folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString();
    582         if ((prefix != null) && folder.startsWith(prefix)) {
    583             folder = folder.substring(prefix.length());
    584         }
    585         return folder;
    586     }
    587 
    588     /**
    589      * Returns UIDs of Messages joined with "," as the separator.
    590      */
    591     static String joinMessageUids(Message[] messages) {
    592         StringBuilder sb = new StringBuilder();
    593         boolean notFirst = false;
    594         for (Message m : messages) {
    595             if (notFirst) {
    596                 sb.append(',');
    597             }
    598             sb.append(m.getUid());
    599             notFirst = true;
    600         }
    601         return sb.toString();
    602     }
    603 
    604     static class ImapMessage extends MimeMessage {
    605         ImapMessage(String uid, ImapFolder folder) {
    606             mUid = uid;
    607             mFolder = folder;
    608         }
    609 
    610         public void setSize(int size) {
    611             mSize = size;
    612         }
    613 
    614         @Override
    615         public void parse(InputStream in) throws IOException, MessagingException {
    616             super.parse(in);
    617         }
    618 
    619         public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
    620             super.setFlag(flag, set);
    621         }
    622 
    623         @Override
    624         public void setFlag(Flag flag, boolean set) throws MessagingException {
    625             super.setFlag(flag, set);
    626             mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
    627         }
    628     }
    629 
    630     static class ImapException extends MessagingException {
    631         private static final long serialVersionUID = 1L;
    632 
    633         private final String mAlertText;
    634         private final String mResponseCode;
    635 
    636         public ImapException(String message, String alertText, String responseCode) {
    637             super(message);
    638             mAlertText = alertText;
    639             mResponseCode = responseCode;
    640         }
    641 
    642         public String getAlertText() {
    643             return mAlertText;
    644         }
    645 
    646         public String getResponseCode() {
    647             return mResponseCode;
    648         }
    649     }
    650 
    651     public void closeConnections() {
    652         ImapConnection connection;
    653         while ((connection = mConnectionPool.poll()) != null) {
    654             connection.close();
    655         }
    656     }
    657 }
    658