• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
newInstance(Account account, Context context)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      */
ImapStore(Context context, Account account)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 
getUseOAuth()124     boolean getUseOAuth() {
125         return mUseOAuth;
126     }
127 
getUsername()128     String getUsername() {
129         return mUsername;
130     }
131 
getPassword()132     String getPassword() {
133         return mPassword;
134     }
135 
136     @VisibleForTesting
getConnectionPoolForTest()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
setTransportForTest(MailTransport testTransport)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      */
getImapId(Context context, String userName, String host, String capabilities)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
makeCommonImapId(String packageName, String version, String codeName, String model, String id, String vendor, String networkOperator)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
getFolder(String name)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
createHierarchy(HashMap<String, ImapFolder> mailboxes)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      */
addMailbox(Context context, long accountId, String mailboxPath, char delimiter, boolean selectable, Mailbox mailbox)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      */
saveMailboxList(Context context, HashMap<String, ImapFolder> folderMap)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
updateFolders()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
checkSettings()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      */
isUserPrefixSet()480     boolean isUserPrefixSet() {
481         return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix);
482     }
483 
484     /** Sets the path separator */
setPathSeparator(String pathSeparator)485     void setPathSeparator(String pathSeparator) {
486         mPathSeparator = pathSeparator;
487     }
488 
489     /** Sets the prefix */
setPathPrefix(String pathPrefix)490     void setPathPrefix(String pathPrefix) {
491         mPathPrefix = pathPrefix;
492     }
493 
494     /** Gets the context for this store */
getContext()495     Context getContext() {
496         return mContext;
497     }
498 
499     /** Returns a clone of the transport associated with this store. */
cloneTransport()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      */
ensurePrefixIsValid()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      */
getConnection()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      */
poolConnection(ImapConnection connection)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      */
encodeFolderName(String name, String prefix)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      */
decodeFolderName(String name, String prefix)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      */
joinMessageUids(Message[] messages)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 {
ImapMessage(String uid, ImapFolder folder)605         ImapMessage(String uid, ImapFolder folder) {
606             mUid = uid;
607             mFolder = folder;
608         }
609 
setSize(int size)610         public void setSize(int size) {
611             mSize = size;
612         }
613 
614         @Override
parse(InputStream in)615         public void parse(InputStream in) throws IOException, MessagingException {
616             super.parse(in);
617         }
618 
setFlagInternal(Flag flag, boolean set)619         public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
620             super.setFlag(flag, set);
621         }
622 
623         @Override
setFlag(Flag flag, boolean set)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 
ImapException(String message, String alertText, String responseCode)636         public ImapException(String message, String alertText, String responseCode) {
637             super(message);
638             mAlertText = alertText;
639             mResponseCode = responseCode;
640         }
641 
getAlertText()642         public String getAlertText() {
643             return mAlertText;
644         }
645 
getResponseCode()646         public String getResponseCode() {
647             return mResponseCode;
648         }
649     }
650 
closeConnections()651     public void closeConnections() {
652         ImapConnection connection;
653         while ((connection = mConnectionPool.poll()) != null) {
654             connection.close();
655         }
656     }
657 }
658