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