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