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