ImapStore.java revision ba7652cda04d428ae7959f8bf6cbc6efe028bfce
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.ImapList;
26import com.android.email.mail.store.imap.ImapResponse;
27import com.android.email.mail.store.imap.ImapResponseParser;
28import com.android.email.mail.store.imap.ImapString;
29import com.android.email.mail.store.imap.ImapUtility;
30import com.android.email.mail.transport.DiscourseLogger;
31import com.android.email.mail.transport.MailTransport;
32import com.android.emailcommon.Logging;
33import com.android.emailcommon.internet.MimeMessage;
34import com.android.emailcommon.mail.AuthenticationFailedException;
35import com.android.emailcommon.mail.CertificateValidationException;
36import com.android.emailcommon.mail.Flag;
37import com.android.emailcommon.mail.Folder;
38import com.android.emailcommon.mail.Message;
39import com.android.emailcommon.mail.MessagingException;
40import com.android.emailcommon.service.EmailServiceProxy;
41import com.android.emailcommon.utility.Utility;
42import com.beetstra.jutf7.CharsetProvider;
43
44import android.content.Context;
45import android.os.Build;
46import android.os.Bundle;
47import android.telephony.TelephonyManager;
48import android.text.TextUtils;
49import android.util.Base64;
50import android.util.Log;
51
52import java.io.IOException;
53import java.io.InputStream;
54import java.net.URI;
55import java.net.URISyntaxException;
56import java.nio.ByteBuffer;
57import java.nio.charset.Charset;
58import java.security.MessageDigest;
59import java.security.NoSuchAlgorithmException;
60import java.util.ArrayList;
61import java.util.Collection;
62import java.util.Collections;
63import java.util.HashMap;
64import java.util.List;
65import java.util.concurrent.ConcurrentLinkedQueue;
66import java.util.concurrent.atomic.AtomicInteger;
67import java.util.regex.Pattern;
68
69import javax.net.ssl.SSLException;
70
71/**
72 * <pre>
73 * TODO Need to start keeping track of UIDVALIDITY
74 * TODO Need a default response handler for things like folder updates
75 * TODO In fetch(), if we need a ImapMessage and were given
76 * TODO Collect ALERT messages and show them to users.
77 * something else we can try to do a pre-fetch first.
78 *
79 * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for
80 * certain information in a FETCH command, the server may return the requested
81 * information in any order, not necessarily in the order that it was requested.
82 * Further, the server may return the information in separate FETCH responses
83 * and may also return information that was not explicitly requested (to reflect
84 * to the client changes in the state of the subject message).
85 * </pre>
86 */
87public class ImapStore extends Store {
88
89    // Always check in FALSE
90    private static final boolean DEBUG_FORCE_SEND_ID = false;
91
92    static final int COPY_BUFFER_SIZE = 16*1024;
93
94    static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED };
95    private final Context mContext;
96    private Transport mRootTransport;
97    private String mUsername;
98    private String mPassword;
99    private String mLoginPhrase;
100    private String mIdPhrase = null;
101    private static String sImapId = null;
102    /*package*/ String mPathPrefix;
103    /*package*/ String mPathSeparator;
104
105    private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool =
106            new ConcurrentLinkedQueue<ImapConnection>();
107
108    /**
109     * Charset used for converting folder names to and from UTF-7 as defined by RFC 3501.
110     */
111    private static final Charset MODIFIED_UTF_7_CHARSET =
112            new CharsetProvider().charsetForName("X-RFC-3501");
113
114    /**
115     * Cache of ImapFolder objects. ImapFolders are attached to a given folder on the server
116     * and as long as their associated connection remains open they are reusable between
117     * requests. This cache lets us make sure we always reuse, if possible, for a given
118     * folder name.
119     */
120    private HashMap<String, ImapFolder> mFolderCache = new HashMap<String, ImapFolder>();
121
122    /**
123     * Next tag to use.  All connections associated to the same ImapStore instance share the same
124     * counter to make tests simpler.
125     * (Some of the tests involve multiple connections but only have a single counter to track the
126     * tag.)
127     */
128    private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
129
130    /**
131     * Static named constructor.
132     */
133    public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks)
134            throws MessagingException {
135        return new ImapStore(context, uri);
136    }
137
138    /**
139     * Allowed formats for the Uri:
140     * imap://user:password@server:port
141     * imap+tls+://user:password@server:port
142     * imap+tls+trustallcerts://user:password@server:port
143     * imap+ssl+://user:password@server:port
144     * imap+ssl+trustallcerts://user:password@server:port
145     *
146     * @param uriString the Uri containing information to configure this store
147     */
148    private ImapStore(Context context, String uriString) throws MessagingException {
149        mContext = context;
150        URI uri;
151        try {
152            uri = new URI(uriString);
153        } catch (URISyntaxException use) {
154            throw new MessagingException("Invalid ImapStore URI", use);
155        }
156
157        String scheme = uri.getScheme();
158        if (scheme == null || !scheme.startsWith(STORE_SCHEME_IMAP)) {
159            throw new MessagingException("Unsupported protocol");
160        }
161        // defaults, which can be changed by security modifiers
162        int connectionSecurity = Transport.CONNECTION_SECURITY_NONE;
163        int defaultPort = 143;
164        // check for security modifiers and apply changes
165        if (scheme.contains("+ssl")) {
166            connectionSecurity = Transport.CONNECTION_SECURITY_SSL;
167            defaultPort = 993;
168        } else if (scheme.contains("+tls")) {
169            connectionSecurity = Transport.CONNECTION_SECURITY_TLS;
170        }
171        boolean trustCertificates = scheme.contains(STORE_SECURITY_TRUST_CERTIFICATES);
172
173        mRootTransport = new MailTransport("IMAP");
174        mRootTransport.setUri(uri, defaultPort);
175        mRootTransport.setSecurity(connectionSecurity, trustCertificates);
176
177        String[] userInfoParts = mRootTransport.getUserInfoParts();
178        if (userInfoParts != null) {
179            mUsername = userInfoParts[0];
180            if (userInfoParts.length > 1) {
181                mPassword = userInfoParts[1];
182
183                // build the LOGIN string once (instead of over-and-over again.)
184                // apply the quoting here around the built-up password
185                mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " "
186                        + ImapUtility.imapQuoted(mPassword);
187            }
188        }
189
190        if ((uri.getPath() != null) && (uri.getPath().length() > 0)) {
191            mPathPrefix = uri.getPath().substring(1);
192        }
193    }
194
195    /* package */ Collection<ImapConnection> getConnectionPoolForTest() {
196        return mConnectionPool;
197    }
198
199    /**
200     * For testing only.  Injects a different root transport (it will be copied using
201     * newInstanceWithConfiguration() each time IMAP sets up a new channel).  The transport
202     * should already be set up and ready to use.  Do not use for real code.
203     * @param testTransport The Transport to inject and use for all future communication.
204     */
205    /* package */ void setTransport(Transport testTransport) {
206        mRootTransport = testTransport;
207    }
208
209    /**
210     * Return, or create and return, an string suitable for use in an IMAP ID message.
211     * This is constructed similarly to the way the browser sets up its user-agent strings.
212     * See RFC 2971 for more details.  The output of this command will be a series of key-value
213     * pairs delimited by spaces (there is no point in returning a structured result because
214     * this will be sent as-is to the IMAP server).  No tokens, parenthesis or "ID" are included,
215     * because some connections may append additional values.
216     *
217     * The following IMAP ID keys may be included:
218     *   name                   Android package name of the program
219     *   os                     "android"
220     *   os-version             "version; model; build-id"
221     *   vendor                 Vendor of the client/server
222     *   x-android-device-model Model (only revealed if release build)
223     *   x-android-net-operator Mobile network operator (if known)
224     *   AGUID                  A device+account UID
225     *
226     * In addition, a vendor policy .apk can append key/value pairs.
227     *
228     * @param userName the username of the account
229     * @param host the host (server) of the account
230     * @param capabilities a list of the capabilities from the server
231     * @return a String for use in an IMAP ID message.
232     */
233    /* package */ static String getImapId(Context context, String userName, String host,
234            String capabilities) {
235        // The first section is global to all IMAP connections, and generates the fixed
236        // values in any IMAP ID message
237        synchronized (ImapStore.class) {
238            if (sImapId == null) {
239                TelephonyManager tm =
240                        (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
241                String networkOperator = tm.getNetworkOperatorName();
242                if (networkOperator == null) networkOperator = "";
243
244                sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE,
245                        Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER,
246                        networkOperator);
247            }
248        }
249
250        // This section is per Store, and adds in a dynamic elements like UID's.
251        // We don't cache the result of this work, because the caller does anyway.
252        StringBuilder id = new StringBuilder(sImapId);
253
254        // Optionally add any vendor-supplied id keys
255        String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host,
256                capabilities);
257        if (vendorId != null) {
258            id.append(' ');
259            id.append(vendorId);
260        }
261
262        // Generate a UID that mixes a "stable" device UID with the email address
263        try {
264            String devUID = Preferences.getPreferences(context).getDeviceUID();
265            MessageDigest messageDigest;
266            messageDigest = MessageDigest.getInstance("SHA-1");
267            messageDigest.update(userName.getBytes());
268            messageDigest.update(devUID.getBytes());
269            byte[] uid = messageDigest.digest();
270            String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP);
271            id.append(" \"AGUID\" \"");
272            id.append(hexUid);
273            id.append('\"');
274        } catch (NoSuchAlgorithmException e) {
275            Log.d(Logging.LOG_TAG, "couldn't obtain SHA-1 hash for device UID");
276        }
277        return id.toString();
278    }
279
280    /**
281     * Helper function that actually builds the static part of the IMAP ID string.  This is
282     * separated from getImapId for testability.  There is no escaping or encoding in IMAP ID so
283     * any rogue chars must be filtered here.
284     *
285     * @param packageName context.getPackageName()
286     * @param version Build.VERSION.RELEASE
287     * @param codeName Build.VERSION.CODENAME
288     * @param model Build.MODEL
289     * @param id Build.ID
290     * @param vendor Build.MANUFACTURER
291     * @param networkOperator TelephonyManager.getNetworkOperatorName()
292     * @return the static (never changes) portion of the IMAP ID
293     */
294    /* package */ static String makeCommonImapId(String packageName, String version,
295            String codeName, String model, String id, String vendor, String networkOperator) {
296
297        // Before building up IMAP ID string, pre-filter the input strings for "legal" chars
298        // This is using a fairly arbitrary char set intended to pass through most reasonable
299        // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space>
300        // The most important thing is *not* to pass parens, quotes, or CRLF, which would break
301        // the format of the IMAP ID list.
302        Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]");
303        packageName = p.matcher(packageName).replaceAll("");
304        version = p.matcher(version).replaceAll("");
305        codeName = p.matcher(codeName).replaceAll("");
306        model = p.matcher(model).replaceAll("");
307        id = p.matcher(id).replaceAll("");
308        vendor = p.matcher(vendor).replaceAll("");
309        networkOperator = p.matcher(networkOperator).replaceAll("");
310
311        // "name" "com.android.email"
312        StringBuffer sb = new StringBuffer("\"name\" \"");
313        sb.append(packageName);
314        sb.append("\"");
315
316        // "os" "android"
317        sb.append(" \"os\" \"android\"");
318
319        // "os-version" "version; build-id"
320        sb.append(" \"os-version\" \"");
321        if (version.length() > 0) {
322            sb.append(version);
323        } else {
324            // default to "1.0"
325            sb.append("1.0");
326        }
327        // add the build ID or build #
328        if (id.length() > 0) {
329            sb.append("; ");
330            sb.append(id);
331        }
332        sb.append("\"");
333
334        // "vendor" "the vendor"
335        if (vendor.length() > 0) {
336            sb.append(" \"vendor\" \"");
337            sb.append(vendor);
338            sb.append("\"");
339        }
340
341        // "x-android-device-model" the device model (on release builds only)
342        if ("REL".equals(codeName)) {
343            if (model.length() > 0) {
344                sb.append(" \"x-android-device-model\" \"");
345                sb.append(model);
346                sb.append("\"");
347            }
348        }
349
350        // "x-android-mobile-net-operator" "name of network operator"
351        if (networkOperator.length() > 0) {
352            sb.append(" \"x-android-mobile-net-operator\" \"");
353            sb.append(networkOperator);
354            sb.append("\"");
355        }
356
357        return sb.toString();
358    }
359
360
361    @Override
362    public Folder getFolder(String name) {
363        ImapFolder folder;
364        synchronized (mFolderCache) {
365            folder = mFolderCache.get(name);
366            if (folder == null) {
367                folder = new ImapFolder(this, name);
368                mFolderCache.put(name, folder);
369            }
370        }
371        return folder;
372    }
373
374    @Override
375    public Folder[] getAllFolders() throws MessagingException {
376        ImapConnection connection = getConnection();
377        try {
378            ArrayList<Folder> folders = new ArrayList<Folder>();
379            // Establish a connection to the IMAP server; if necessary
380            // This ensures a valid prefix if the prefix is automatically set by the server
381            connection.executeSimpleCommand(ImapConstants.NOOP);
382            String imapCommand = ImapConstants.LIST + " \"\" \"*\"";
383            if (mPathPrefix != null) {
384                imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\"";
385            }
386            List<ImapResponse> responses = connection.executeSimpleCommand(imapCommand);
387            for (ImapResponse response : responses) {
388                // S: * LIST (\Noselect) "/" ~/Mail/foo
389                if (response.isDataResponse(0, ImapConstants.LIST)) {
390                    boolean includeFolder = true;
391
392                    // Get folder name.
393                    ImapString encodedFolder = response.getStringOrEmpty(3);
394                    if (encodedFolder.isEmpty()) continue;
395                    String folder = decodeFolderName(encodedFolder.getString(), mPathPrefix);
396                    if (ImapConstants.INBOX.equalsIgnoreCase(folder)) {
397                        continue;
398                    }
399
400                    // Parse attributes.
401                    if (response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT)) {
402                        includeFolder = false;
403                    }
404                    if (includeFolder) {
405                        folders.add(getFolder(folder));
406                    }
407                }
408            }
409            folders.add(getFolder(ImapConstants.INBOX));
410            return folders.toArray(new Folder[] {});
411        } catch (IOException ioe) {
412            connection.close();
413            throw new MessagingException("Unable to get folder list.", ioe);
414        } catch (AuthenticationFailedException afe) {
415            // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT
416            // commands to the server
417            connection.destroyResponses();
418            connection = null;
419            throw afe;
420        } finally {
421            if (connection != null) {
422                connection.destroyResponses();
423                poolConnection(connection);
424            }
425        }
426    }
427
428    @Override
429    public Bundle checkSettings() throws MessagingException {
430        int result = MessagingException.NO_ERROR;
431        Bundle bundle = new Bundle();
432        ImapConnection connection = new ImapConnection();
433        try {
434            connection.open();
435            connection.close();
436        } catch (IOException ioe) {
437            bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage());
438            result = MessagingException.IOERROR;
439        } finally {
440            connection.destroyResponses();
441        }
442        bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
443        return bundle;
444    }
445
446    /**
447     * Fixes the path prefix, if necessary. The path prefix must always end with the
448     * path separator.
449     */
450    /*package*/ void ensurePrefixIsValid() {
451        // Make sure the path prefix ends with the path separator
452        if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) {
453            if (!mPathPrefix.endsWith(mPathSeparator)) {
454                mPathPrefix = mPathPrefix + mPathSeparator;
455            }
456        }
457    }
458
459    /**
460     * Gets a connection if one is available from the pool, or creates a new one if not.
461     */
462    /* package */ ImapConnection getConnection() {
463        ImapConnection connection = null;
464        while ((connection = mConnectionPool.poll()) != null) {
465            try {
466                connection.executeSimpleCommand(ImapConstants.NOOP);
467                break;
468            } catch (MessagingException e) {
469                // Fall through
470            } catch (IOException e) {
471                // Fall through
472            } finally {
473                connection.destroyResponses();
474            }
475            connection.close();
476            connection = null;
477        }
478        if (connection == null) {
479            connection = new ImapConnection();
480        }
481        return connection;
482    }
483
484    /**
485     * Save a {@link ImapConnection} in the pool for reuse.
486     */
487    /* package */ void poolConnection(ImapConnection connection) {
488        if (connection != null) {
489            mConnectionPool.add(connection);
490        }
491    }
492
493    /**
494     * Prepends the folder name with the given prefix and UTF-7 encodes it.
495     */
496    /* package */ static String encodeFolderName(String name, String prefix) {
497        // do NOT add the prefix to the special name "INBOX"
498        if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name;
499
500        // Prepend prefix
501        if (prefix != null) {
502            name = prefix + name;
503        }
504
505        // TODO bypass the conversion if name doesn't have special char.
506        ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name);
507        byte[] b = new byte[bb.limit()];
508        bb.get(b);
509
510        return Utility.fromAscii(b);
511    }
512
513    /**
514     * UTF-7 decodes the folder name and removes the given path prefix.
515     */
516    /* package */ static String decodeFolderName(String name, String prefix) {
517        // TODO bypass the conversion if name doesn't have special char.
518        String folder;
519        folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString();
520        if ((prefix != null) && folder.startsWith(prefix)) {
521            folder = folder.substring(prefix.length());
522        }
523        return folder;
524    }
525
526    /**
527     * Returns UIDs of Messages joined with "," as the separator.
528     */
529    /* package */ static String joinMessageUids(Message[] messages) {
530        StringBuilder sb = new StringBuilder();
531        boolean notFirst = false;
532        for (Message m : messages) {
533            if (notFirst) {
534                sb.append(',');
535            }
536            sb.append(m.getUid());
537            notFirst = true;
538        }
539        return sb.toString();
540    }
541
542    /**
543     * A cacheable class that stores the details for a single IMAP connection.
544     */
545    class ImapConnection {
546        /** ID capability per RFC 2971*/
547        public static final int CAPABILITY_ID        = 1 << 0;
548        /** NAMESPACE capability per RFC 2342 */
549        public static final int CAPABILITY_NAMESPACE = 1 << 1;
550        /** STARTTLS capability per RFC 3501 */
551        public static final int CAPABILITY_STARTTLS  = 1 << 2;
552        /** UIDPLUS capability per RFC 4315 */
553        public static final int CAPABILITY_UIDPLUS   = 1 << 3;
554
555        /** The capabilities supported; a set of CAPABILITY_* values. */
556        private int mCapabilities;
557        private static final String IMAP_DEDACTED_LOG = "[IMAP command redacted]";
558        Transport mTransport;
559        private ImapResponseParser mParser;
560        /** # of command/response lines to log upon crash. */
561        private static final int DISCOURSE_LOGGER_SIZE = 64;
562        private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
563
564        public void open() throws IOException, MessagingException {
565            if (mTransport != null && mTransport.isOpen()) {
566                return;
567            }
568
569            try {
570                // copy configuration into a clean transport, if necessary
571                if (mTransport == null) {
572                    mTransport = mRootTransport.newInstanceWithConfiguration();
573                }
574
575                mTransport.open();
576                mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
577
578                createParser();
579
580                // BANNER
581                mParser.readResponse();
582
583                // CAPABILITY
584                ImapResponse capabilities = queryCapabilities();
585
586                boolean hasStartTlsCapability =
587                    capabilities.contains(ImapConstants.STARTTLS);
588
589                // TLS
590                ImapResponse newCapabilities = doStartTls(hasStartTlsCapability);
591                if (newCapabilities != null) {
592                    capabilities = newCapabilities;
593                }
594
595                // NOTE: An IMAP response MUST be processed before issuing any new IMAP
596                // requests. Subsequent requests may destroy previous response data. As
597                // such, we save away capability information here for future use.
598                setCapabilities(capabilities);
599                String capabilityString = capabilities.flatten();
600
601                // ID
602                doSendId(isCapable(CAPABILITY_ID), capabilityString);
603
604                // LOGIN
605                doLogin();
606
607                // NAMESPACE (only valid in the Authenticated state)
608                doGetNamespace(isCapable(CAPABILITY_NAMESPACE));
609
610                // Gets the path separator from the server
611                doGetPathSeparator();
612
613                ensurePrefixIsValid();
614            } catch (SSLException e) {
615                if (Email.DEBUG) {
616                    Log.d(Logging.LOG_TAG, e.toString());
617                }
618                throw new CertificateValidationException(e.getMessage(), e);
619            } catch (IOException ioe) {
620                // NOTE:  Unlike similar code in POP3, I'm going to rethrow as-is.  There is a lot
621                // of other code here that catches IOException and I don't want to break it.
622                // This catch is only here to enhance logging of connection-time issues.
623                if (Email.DEBUG) {
624                    Log.d(Logging.LOG_TAG, ioe.toString());
625                }
626                throw ioe;
627            } finally {
628                destroyResponses();
629            }
630        }
631
632        public void close() {
633            if (mTransport != null) {
634                mTransport.close();
635                mTransport = null;
636            }
637        }
638
639        /**
640         * Returns whether or not the specified capability is supported by the server.
641         */
642        public boolean isCapable(int capability) {
643            return (mCapabilities & capability) != 0;
644        }
645
646        /**
647         * Sets the capability flags according to the response provided by the server.
648         * Note: We only set the capability flags that we are interested in. There are many IMAP
649         * capabilities that we do not track.
650         */
651        private void setCapabilities(ImapResponse capabilities) {
652            if (capabilities.contains(ImapConstants.ID)) {
653                mCapabilities |= CAPABILITY_ID;
654            }
655            if (capabilities.contains(ImapConstants.NAMESPACE)) {
656                mCapabilities |= CAPABILITY_NAMESPACE;
657            }
658            if (capabilities.contains(ImapConstants.UIDPLUS)) {
659                mCapabilities |= CAPABILITY_UIDPLUS;
660            }
661            if (capabilities.contains(ImapConstants.STARTTLS)) {
662                mCapabilities |= CAPABILITY_STARTTLS;
663            }
664        }
665
666        /**
667         * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
668         * set it to {@link #mParser}.
669         *
670         * If we already have an {@link ImapResponseParser}, we
671         * {@link #destroyResponses()} and throw it away.
672         */
673        private void createParser() {
674            destroyResponses();
675            mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse);
676        }
677
678        public void destroyResponses() {
679            if (mParser != null) {
680                mParser.destroyResponses();
681            }
682        }
683
684        /* package */ boolean isTransportOpenForTest() {
685            return mTransport != null ? mTransport.isOpen() : false;
686        }
687
688        public ImapResponse readResponse() throws IOException, MessagingException {
689            return mParser.readResponse();
690        }
691
692        /**
693         * Send a single command to the server.  The command will be preceded by an IMAP command
694         * tag and followed by \r\n (caller need not supply them).
695         *
696         * @param command The command to send to the server
697         * @param sensitive If true, the command will not be logged
698         * @return Returns the command tag that was sent
699         */
700        public String sendCommand(String command, boolean sensitive)
701            throws MessagingException, IOException {
702            open();
703            String tag = Integer.toString(mNextCommandTag.incrementAndGet());
704            String commandToSend = tag + " " + command;
705            mTransport.writeLine(commandToSend, sensitive ? IMAP_DEDACTED_LOG : null);
706            mDiscourse.addSentCommand(sensitive ? IMAP_DEDACTED_LOG : commandToSend);
707            return tag;
708        }
709
710        /*package*/ List<ImapResponse> executeSimpleCommand(String command) throws IOException,
711                MessagingException {
712            return executeSimpleCommand(command, false);
713        }
714
715        /*package*/ List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
716                throws IOException, MessagingException {
717            String tag = sendCommand(command, sensitive);
718            ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
719            ImapResponse response;
720            do {
721                response = mParser.readResponse();
722                responses.add(response);
723            } while (!response.isTagged());
724            if (!response.isOk()) {
725                final String toString = response.toString();
726                final String alert = response.getAlertTextOrEmpty().getString();
727                destroyResponses();
728                throw new ImapException(toString, alert);
729            }
730            return responses;
731        }
732
733        /**
734         * Query server for capabilities.
735         */
736        private ImapResponse queryCapabilities() throws IOException, MessagingException {
737            ImapResponse capabilityResponse = null;
738            for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
739                if (r.is(0, ImapConstants.CAPABILITY)) {
740                    capabilityResponse = r;
741                    break;
742                }
743            }
744            if (capabilityResponse == null) {
745                throw new MessagingException("Invalid CAPABILITY response received");
746            }
747            return capabilityResponse;
748        }
749
750        /**
751         * Sends client identification information to the IMAP server per RFC 2971. If
752         * the server does not support the ID command, this will perform no operation.
753         *
754         * Interoperability hack:  Never send ID to *.secureserver.net, which sends back a
755         * malformed response that our parser can't deal with.
756         */
757        private void doSendId(boolean hasIdCapability, String capabilities)
758                throws MessagingException {
759            if (!hasIdCapability) return;
760
761            // Never send ID to *.secureserver.net
762            String host = mRootTransport.getHost();
763            if (host.toLowerCase().endsWith(".secureserver.net")) return;
764
765            // Assign user-agent string (for RFC2971 ID command)
766            String mUserAgent = getImapId(mContext, mUsername, host, capabilities);
767
768            if (mUserAgent != null) {
769                mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
770            } else if (DEBUG_FORCE_SEND_ID) {
771                mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
772            }
773            // else: mIdPhrase = null, no ID will be emitted
774
775            // Send user-agent in an RFC2971 ID command
776            if (mIdPhrase != null) {
777                try {
778                    executeSimpleCommand(mIdPhrase);
779                } catch (ImapException ie) {
780                    // Log for debugging, but this is not a fatal problem.
781                    if (Email.DEBUG) {
782                        Log.d(Logging.LOG_TAG, ie.toString());
783                    }
784                } catch (IOException ioe) {
785                    // Special case to handle malformed OK responses and ignore them.
786                    // A true IOException will recur on the following login steps
787                    // This can go away after the parser is fixed - see bug 2138981
788                }
789            }
790        }
791
792        /**
793         * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
794         * explicitly sets a namespace (using setup UI) or if the server does not support the
795         * namespace command, this will perform no operation.
796         */
797        private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
798            // user did not specify a hard-coded prefix; try to get it from the server
799            if (hasNamespaceCapability && TextUtils.isEmpty(mPathPrefix)) {
800                List<ImapResponse> responseList = Collections.emptyList();
801
802                try {
803                    responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
804                } catch (ImapException ie) {
805                    // Log for debugging, but this is not a fatal problem.
806                    if (Email.DEBUG) {
807                        Log.d(Logging.LOG_TAG, ie.toString());
808                    }
809                } catch (IOException ioe) {
810                    // Special case to handle malformed OK responses and ignore them.
811                }
812
813                for (ImapResponse response: responseList) {
814                    if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
815                        ImapList namespaceList = response.getListOrEmpty(1);
816                        ImapList namespace = namespaceList.getListOrEmpty(0);
817                        String namespaceString = namespace.getStringOrEmpty(0).getString();
818                        if (!TextUtils.isEmpty(namespaceString)) {
819                            mPathPrefix = decodeFolderName(namespaceString, null);
820                            mPathSeparator = namespace.getStringOrEmpty(1).getString();
821                        }
822                    }
823                }
824            }
825        }
826
827        /**
828         * Logs into the IMAP server
829         */
830        private void doLogin()
831                throws IOException, MessagingException, AuthenticationFailedException {
832            try {
833                // TODO eventually we need to add additional authentication
834                // options such as SASL
835                executeSimpleCommand(mLoginPhrase, true);
836            } catch (ImapException ie) {
837                if (Email.DEBUG) {
838                    Log.d(Logging.LOG_TAG, ie.toString());
839                }
840                throw new AuthenticationFailedException(ie.getAlertText(), ie);
841
842            } catch (MessagingException me) {
843                throw new AuthenticationFailedException(null, me);
844            }
845        }
846
847        /**
848         * Gets the path separator per the LIST command in RFC 3501. If the path separator
849         * was obtained while obtaining the namespace or there is no prefix defined, this
850         * will perform no operation.
851         */
852        private void doGetPathSeparator() throws MessagingException {
853            // user did not specify a hard-coded prefix; try to get it from the server
854            if (TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix)) {
855                List<ImapResponse> responseList = Collections.emptyList();
856
857                try {
858                    responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
859                } catch (ImapException ie) {
860                    // Log for debugging, but this is not a fatal problem.
861                    if (Email.DEBUG) {
862                        Log.d(Logging.LOG_TAG, ie.toString());
863                    }
864                } catch (IOException ioe) {
865                    // Special case to handle malformed OK responses and ignore them.
866                }
867
868                for (ImapResponse response: responseList) {
869                    if (response.isDataResponse(0, ImapConstants.LIST)) {
870                        mPathSeparator = response.getStringOrEmpty(2).getString();
871                    }
872                }
873            }
874        }
875
876        /**
877         * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
878         * to use TLS or the server does not support the TLS capability, this will perform
879         * no operation.
880         */
881        private ImapResponse doStartTls(boolean hasStartTlsCapability)
882                throws IOException, MessagingException {
883            if (mTransport.canTryTlsSecurity()) {
884                if (hasStartTlsCapability) {
885                    // STARTTLS
886                    executeSimpleCommand(ImapConstants.STARTTLS);
887
888                    mTransport.reopenTls();
889                    mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
890                    createParser();
891                    // Per RFC requirement (3501-6.2.1) gather new capabilities
892                    return(queryCapabilities());
893                } else {
894                    if (Email.DEBUG) {
895                        Log.d(Logging.LOG_TAG, "TLS not supported but required");
896                    }
897                    throw new MessagingException(MessagingException.TLS_REQUIRED);
898                }
899            }
900            return null;
901        }
902
903        /** @see DiscourseLogger#logLastDiscourse() */
904        public void logLastDiscourse() {
905            mDiscourse.logLastDiscourse();
906        }
907    }
908
909    static class ImapMessage extends MimeMessage {
910        ImapMessage(String uid, ImapFolder folder) {
911            this.mUid = uid;
912            this.mFolder = folder;
913        }
914
915        public void setSize(int size) {
916            this.mSize = size;
917        }
918
919        @Override
920        public void parse(InputStream in) throws IOException, MessagingException {
921            super.parse(in);
922        }
923
924        public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
925            super.setFlag(flag, set);
926        }
927
928        @Override
929        public void setFlag(Flag flag, boolean set) throws MessagingException {
930            super.setFlag(flag, set);
931            mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
932        }
933    }
934
935    static class ImapException extends MessagingException {
936        private static final long serialVersionUID = 1L;
937
938        String mAlertText;
939
940        public ImapException(String message, String alertText, Throwable throwable) {
941            super(message, throwable);
942            this.mAlertText = alertText;
943        }
944
945        public ImapException(String message, String alertText) {
946            super(message);
947            this.mAlertText = alertText;
948        }
949
950        public String getAlertText() {
951            return mAlertText;
952        }
953
954        public void setAlertText(String alertText) {
955            mAlertText = alertText;
956        }
957    }
958}
959