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