ImapStore.java revision 34f29c8a7478cf8c85578d176ac27d973ecca7e4
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 capability the capabilities string 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        @Override
514        public void open(OpenMode mode, PersistentDataCallbacks callbacks)
515                throws MessagingException {
516
517            try {
518                if (isOpen()) {
519                    if (mMode == mode) {
520                        // Make sure the connection is valid.
521                        // If it's not we'll close it down and continue on to get a new one.
522                        try {
523                            mConnection.executeSimpleCommand(ImapConstants.NOOP);
524                            return;
525
526                        } catch (IOException ioe) {
527                            ioExceptionHandler(mConnection, ioe);
528                        } finally {
529                            mConnection.destroyResponses();
530                        }
531                    } else {
532                        // Return the connection to the pool, if exists.
533                        close(false);
534                    }
535                }
536                synchronized (this) {
537                    mConnection = mStore.getConnection();
538                }
539                // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
540                // $MDNSent)
541                // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
542                // NonJunk $MDNSent \*)] Flags permitted.
543                // * 23 EXISTS
544                // * 0 RECENT
545                // * OK [UIDVALIDITY 1125022061] UIDs valid
546                // * OK [UIDNEXT 57576] Predicted next UID
547                // 2 OK [READ-WRITE] Select completed.
548                try {
549                    List<ImapResponse> responses = mConnection.executeSimpleCommand(
550                            String.format(ImapConstants.SELECT + " \"%s\"",
551                                    encodeFolderName(mName)));
552                    /*
553                     * If the command succeeds we expect the folder has been opened read-write
554                     * unless we are notified otherwise in the responses.
555                     */
556                    mMode = OpenMode.READ_WRITE;
557
558                    int messageCount = -1;
559                    for (ImapResponse response : responses) {
560                        if (response.isDataResponse(1, ImapConstants.EXISTS)) {
561                            messageCount = response.getStringOrEmpty(0).getNumberOrZero();
562
563                        } else if (response.isOk()) {
564                            final ImapString responseCode = response.getResponseCodeOrEmpty();
565                            if (responseCode.is(ImapConstants.READ_ONLY)) {
566                                mMode = OpenMode.READ_ONLY;
567                            } else if (responseCode.is(ImapConstants.READ_WRITE)) {
568                                mMode = OpenMode.READ_WRITE;
569                            }
570                        } else if (response.isTagged()) { // Not OK
571                            throw new MessagingException("Can't open mailbox: "
572                                    + response.getStatusResponseTextOrEmpty());
573                        }
574                    }
575
576                    if (messageCount == -1) {
577                        throw new MessagingException("Did not find message count during select");
578                    }
579                    mMessageCount = messageCount;
580                    mExists = true;
581
582                } catch (IOException ioe) {
583                    throw ioExceptionHandler(mConnection, ioe);
584                } finally {
585                    mConnection.destroyResponses();
586                }
587            } catch (MessagingException e) {
588                mExists = false;
589                close(false);
590                throw e;
591            }
592        }
593
594        @Override
595        public boolean isOpen() {
596            return mExists && mConnection != null;
597        }
598
599        @Override
600        public OpenMode getMode() throws MessagingException {
601            return mMode;
602        }
603
604        @Override
605        public void close(boolean expunge) {
606            // TODO implement expunge
607            mMessageCount = -1;
608            synchronized (this) {
609                mStore.poolConnection(mConnection);
610                mConnection = null;
611            }
612        }
613
614        @Override
615        public String getName() {
616            return mName;
617        }
618
619        @Override
620        public boolean exists() throws MessagingException {
621            if (mExists) {
622                return true;
623            }
624            /*
625             * This method needs to operate in the unselected mode as well as the selected mode
626             * so we must get the connection ourselves if it's not there. We are specifically
627             * not calling checkOpen() since we don't care if the folder is open.
628             */
629            ImapConnection connection = null;
630            synchronized(this) {
631                if (mConnection == null) {
632                    connection = mStore.getConnection();
633                } else {
634                    connection = mConnection;
635                }
636            }
637            try {
638                connection.executeSimpleCommand(String.format(
639                        ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")",
640                        encodeFolderName(mName)));
641                mExists = true;
642                return true;
643
644            } catch (MessagingException me) {
645                return false;
646
647            } catch (IOException ioe) {
648                throw ioExceptionHandler(connection, ioe);
649
650            } finally {
651                connection.destroyResponses();
652                if (mConnection == null) {
653                    mStore.poolConnection(connection);
654                }
655            }
656        }
657
658        // IMAP supports folder creation
659        @Override
660        public boolean canCreate(FolderType type) {
661            return true;
662        }
663
664        @Override
665        public boolean create(FolderType type) throws MessagingException {
666            /*
667             * This method needs to operate in the unselected mode as well as the selected mode
668             * so we must get the connection ourselves if it's not there. We are specifically
669             * not calling checkOpen() since we don't care if the folder is open.
670             */
671            ImapConnection connection = null;
672            synchronized(this) {
673                if (mConnection == null) {
674                    connection = mStore.getConnection();
675                } else {
676                    connection = mConnection;
677                }
678            }
679            try {
680                connection.executeSimpleCommand(String.format(ImapConstants.CREATE + " \"%s\"",
681                        encodeFolderName(mName)));
682                return true;
683
684            } catch (MessagingException me) {
685                return false;
686
687            } catch (IOException ioe) {
688                throw ioExceptionHandler(connection, ioe);
689
690            } finally {
691                connection.destroyResponses();
692                if (mConnection == null) {
693                    mStore.poolConnection(connection);
694                }
695            }
696        }
697
698        @Override
699        public void copyMessages(Message[] messages, Folder folder,
700                MessageUpdateCallbacks callbacks) throws MessagingException {
701            checkOpen();
702            try {
703                mConnection.executeSimpleCommand(
704                        String.format(ImapConstants.UID_COPY + " %s \"%s\"",
705                                joinMessageUids(messages),
706                                encodeFolderName(folder.getName())));
707            } catch (IOException ioe) {
708                throw ioExceptionHandler(mConnection, ioe);
709            } finally {
710                mConnection.destroyResponses();
711            }
712        }
713
714        @Override
715        public int getMessageCount() {
716            return mMessageCount;
717        }
718
719        @Override
720        public int getUnreadMessageCount() throws MessagingException {
721            checkOpen();
722            try {
723                int unreadMessageCount = 0;
724                List<ImapResponse> responses = mConnection.executeSimpleCommand(String.format(
725                        ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")",
726                        encodeFolderName(mName)));
727                // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292)
728                for (ImapResponse response : responses) {
729                    if (response.isDataResponse(0, ImapConstants.STATUS)) {
730                        unreadMessageCount = response.getListOrEmpty(2)
731                                .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero();
732                    }
733                }
734                return unreadMessageCount;
735            } catch (IOException ioe) {
736                throw ioExceptionHandler(mConnection, ioe);
737            } finally {
738                mConnection.destroyResponses();
739            }
740        }
741
742        @Override
743        public void delete(boolean recurse) throws MessagingException {
744            throw new Error("ImapStore.delete() not yet implemented");
745        }
746
747        private String[] searchForUids(String searchCriteria)
748                throws MessagingException {
749            checkOpen();
750            List<ImapResponse> responses;
751            try {
752                try {
753                    responses = mConnection.executeSimpleCommand(
754                            ImapConstants.UID_SEARCH + " " + searchCriteria);
755                } catch (ImapException e) {
756                    return Utility.EMPTY_STRINGS; // not found;
757                } catch (IOException ioe) {
758                    throw ioExceptionHandler(mConnection, ioe);
759                }
760                // S: * SEARCH 2 3 6
761                for (ImapResponse response : responses) {
762                    if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
763                        continue;
764                    }
765                    // Found SEARCH response data
766                    final int count = response.size() - 1;
767                    if (count <= 0) {
768                        return Utility.EMPTY_STRINGS; // ... but no UIDs in it!  Return empty array.
769                    }
770
771                    ArrayList<String> ret = new ArrayList<String>(count);
772                    for (int i = 1; i < response.size(); i++) {
773                        ImapString s = response.getStringOrEmpty(i);
774                        if (s.isString()) {
775                            ret.add(s.getString());
776                        }
777                    }
778                    return ret.toArray(Utility.EMPTY_STRINGS);
779                }
780            } finally {
781                mConnection.destroyResponses();
782            }
783            return Utility.EMPTY_STRINGS;
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                mConnection.logLastDiscourse();
845                throw e;
846            } finally {
847                mConnection.destroyResponses();
848            }
849        }
850
851        public void fetchInternal(Message[] messages, FetchProfile fp,
852                MessageRetrievalListener listener) throws MessagingException {
853            if (messages == null || 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                String tag = 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                        mConnection.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                mConnection.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                mConnection.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                mConnection.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            connection.close();
1354            close(false);
1355            return new MessagingException("IO Error", ioe);
1356        }
1357
1358        @Override
1359        public boolean equals(Object o) {
1360            if (o instanceof ImapFolder) {
1361                return ((ImapFolder)o).mName.equals(mName);
1362            }
1363            return super.equals(o);
1364        }
1365
1366        @Override
1367        public Message createMessage(String uid) throws MessagingException {
1368            return new ImapMessage(uid, this);
1369        }
1370    }
1371
1372    /**
1373     * A cacheable class that stores the details for a single IMAP connection.
1374     */
1375    class ImapConnection {
1376        private static final String IMAP_DEDACTED_LOG = "[IMAP command redacted]";
1377        private Transport mTransport;
1378        private ImapResponseParser mParser;
1379        /** # of command/response lines to log upon crash. */
1380        private static final int DISCOURSE_LOGGER_SIZE = 64;
1381        private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
1382
1383        public void open() throws IOException, MessagingException {
1384            if (mTransport != null && mTransport.isOpen()) {
1385                return;
1386            }
1387
1388            try {
1389                // copy configuration into a clean transport, if necessary
1390                if (mTransport == null) {
1391                    mTransport = mRootTransport.newInstanceWithConfiguration();
1392                }
1393
1394                mTransport.open();
1395                mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
1396
1397                createParser();
1398
1399                // BANNER
1400                mParser.readResponse();
1401
1402                // CAPABILITY
1403                ImapResponse capabilityResponse = null;
1404                for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
1405                    if (r.is(0, ImapConstants.CAPABILITY)) {
1406                        capabilityResponse = r;
1407                        break;
1408                    }
1409                }
1410                if (capabilityResponse == null) {
1411                    throw new MessagingException("Invalid CAPABILITY response received");
1412                }
1413
1414                if (mTransport.canTryTlsSecurity()) {
1415                    if (capabilityResponse.contains(ImapConstants.STARTTLS)) {
1416                        // STARTTLS
1417                        executeSimpleCommand(ImapConstants.STARTTLS);
1418
1419                        mTransport.reopenTls();
1420                        mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
1421                        createParser();
1422                    } else {
1423                        if (Config.LOGD && Email.DEBUG) {
1424                            Log.d(Email.LOG_TAG, "TLS not supported but required");
1425                        }
1426                        throw new MessagingException(MessagingException.TLS_REQUIRED);
1427                    }
1428                }
1429
1430                // Assign user-agent string (for RFC2971 ID command)
1431                String mUserAgent = getImapId(mContext, mUsername, mRootTransport.getHost(),
1432                        capabilityResponse);
1433                if (mUserAgent != null) {
1434                    mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
1435                } else if (DEBUG_FORCE_SEND_ID) {
1436                    mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
1437                }
1438                // else: mIdPhrase = null, no ID will be emitted
1439
1440                // Send user-agent in an RFC2971 ID command
1441                if (mIdPhrase != null) {
1442                    try {
1443                        executeSimpleCommand(mIdPhrase);
1444                    } catch (ImapException ie) {
1445                        // Log for debugging, but this is not a fatal problem.
1446                        if (Config.LOGD && Email.DEBUG) {
1447                            Log.d(Email.LOG_TAG, ie.toString());
1448                        }
1449                    } catch (IOException ioe) {
1450                        // Special case to handle malformed OK responses and ignore them.
1451                        // A true IOException will recur on the following login steps
1452                        // This can go away after the parser is fixed - see bug 2138981 for details
1453                    }
1454                }
1455
1456                try {
1457                    // TODO eventually we need to add additional authentication
1458                    // options such as SASL
1459                    executeSimpleCommand(mLoginPhrase, true);
1460                } catch (ImapException ie) {
1461                    if (Config.LOGD && Email.DEBUG) {
1462                        Log.d(Email.LOG_TAG, ie.toString());
1463                    }
1464                    throw new AuthenticationFailedException(ie.getAlertText(), ie);
1465
1466                } catch (MessagingException me) {
1467                    throw new AuthenticationFailedException(null, me);
1468                }
1469            } catch (SSLException e) {
1470                if (Config.LOGD && Email.DEBUG) {
1471                    Log.d(Email.LOG_TAG, e.toString());
1472                }
1473                throw new CertificateValidationException(e.getMessage(), e);
1474            } catch (IOException ioe) {
1475                // NOTE:  Unlike similar code in POP3, I'm going to rethrow as-is.  There is a lot
1476                // of other code here that catches IOException and I don't want to break it.
1477                // This catch is only here to enhance logging of connection-time issues.
1478                if (Config.LOGD && Email.DEBUG) {
1479                    Log.d(Email.LOG_TAG, ioe.toString());
1480                }
1481                throw ioe;
1482            } finally {
1483                destroyResponses();
1484            }
1485        }
1486
1487        public void close() {
1488            if (mTransport != null) {
1489                mTransport.close();
1490                mTransport = null;
1491            }
1492        }
1493
1494        /**
1495         * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
1496         * set it to {@link #mParser}.
1497         *
1498         * If we already have an {@link ImapResponseParser}, we
1499         * {@link #destroyResponses()} and throw it away.
1500         */
1501        private void createParser() {
1502            destroyResponses();
1503            mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse);
1504        }
1505
1506        public void destroyResponses() {
1507            if (mParser != null) {
1508                mParser.destroyResponses();
1509            }
1510        }
1511
1512        /* package */ boolean isTransportOpenForTest() {
1513            return mTransport != null ? mTransport.isOpen() : false;
1514        }
1515
1516        public ImapResponse readResponse() throws IOException, MessagingException {
1517            return mParser.readResponse();
1518        }
1519
1520        /**
1521         * Send a single command to the server.  The command will be preceded by an IMAP command
1522         * tag and followed by \r\n (caller need not supply them).
1523         *
1524         * @param command The command to send to the server
1525         * @param sensitive If true, the command will not be logged
1526         * @return Returns the command tag that was sent
1527         */
1528        public String sendCommand(String command, boolean sensitive)
1529            throws MessagingException, IOException {
1530            open();
1531            String tag = Integer.toString(mNextCommandTag.incrementAndGet());
1532            String commandToSend = tag + " " + command;
1533            mTransport.writeLine(commandToSend, sensitive ? IMAP_DEDACTED_LOG : null);
1534            mDiscourse.addSentCommand(sensitive ? IMAP_DEDACTED_LOG : commandToSend);
1535            return tag;
1536        }
1537
1538        public List<ImapResponse> executeSimpleCommand(String command) throws IOException,
1539                MessagingException {
1540            return executeSimpleCommand(command, false);
1541        }
1542
1543        public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
1544                throws IOException, MessagingException {
1545            String tag = sendCommand(command, sensitive);
1546            ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
1547            ImapResponse response;
1548            do {
1549                response = mParser.readResponse();
1550                responses.add(response);
1551            } while (!response.isTagged());
1552            if (!response.isOk()) {
1553                final String toString = response.toString();
1554                final String alert = response.getAlertTextOrEmpty().getString();
1555                destroyResponses();
1556                throw new ImapException(toString, alert);
1557            }
1558            return responses;
1559        }
1560
1561        /** @see ImapResponseParser#logLastDiscourse() */
1562        public void logLastDiscourse() {
1563            mDiscourse.logLastDiscourse();
1564        }
1565    }
1566
1567    static class ImapMessage extends MimeMessage {
1568        ImapMessage(String uid, Folder folder) throws MessagingException {
1569            this.mUid = uid;
1570            this.mFolder = folder;
1571        }
1572
1573        public void setSize(int size) {
1574            this.mSize = size;
1575        }
1576
1577        @Override
1578        public void parse(InputStream in) throws IOException, MessagingException {
1579            super.parse(in);
1580        }
1581
1582        public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
1583            super.setFlag(flag, set);
1584        }
1585
1586        @Override
1587        public void setFlag(Flag flag, boolean set) throws MessagingException {
1588            super.setFlag(flag, set);
1589            mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
1590        }
1591    }
1592
1593    static class ImapException extends MessagingException {
1594        String mAlertText;
1595
1596        public ImapException(String message, String alertText, Throwable throwable) {
1597            super(message, throwable);
1598            this.mAlertText = alertText;
1599        }
1600
1601        public ImapException(String message, String alertText) {
1602            super(message);
1603            this.mAlertText = alertText;
1604        }
1605
1606        public String getAlertText() {
1607            return mAlertText;
1608        }
1609
1610        public void setAlertText(String alertText) {
1611            mAlertText = alertText;
1612        }
1613    }
1614}
1615