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