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