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