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