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