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