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