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