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