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