ImapConnection.java revision 171c3f2273223652b9999977d530a715420c0f64
1/*
2 * Copyright (C) 2011 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.mail.Transport;
21import com.android.email.mail.store.ImapStore.ImapException;
22import com.android.email.mail.store.imap.ImapConstants;
23import com.android.email.mail.store.imap.ImapList;
24import com.android.email.mail.store.imap.ImapResponse;
25import com.android.email.mail.store.imap.ImapResponseParser;
26import com.android.email.mail.store.imap.ImapUtility;
27import com.android.email.mail.transport.DiscourseLogger;
28import com.android.email.mail.transport.MailTransport;
29import com.android.emailcommon.Logging;
30import com.android.emailcommon.mail.AuthenticationFailedException;
31import com.android.emailcommon.mail.CertificateValidationException;
32import com.android.emailcommon.mail.MessagingException;
33
34import android.text.TextUtils;
35import android.util.Log;
36
37import java.io.IOException;
38import java.util.ArrayList;
39import java.util.Collections;
40import java.util.List;
41import java.util.concurrent.atomic.AtomicInteger;
42
43import javax.net.ssl.SSLException;
44
45/**
46 * A cacheable class that stores the details for a single IMAP connection.
47 */
48class ImapConnection {
49    // Always check in FALSE
50    private static final boolean DEBUG_FORCE_SEND_ID = false;
51
52    /** ID capability per RFC 2971*/
53    public static final int CAPABILITY_ID        = 1 << 0;
54    /** NAMESPACE capability per RFC 2342 */
55    public static final int CAPABILITY_NAMESPACE = 1 << 1;
56    /** STARTTLS capability per RFC 3501 */
57    public static final int CAPABILITY_STARTTLS  = 1 << 2;
58    /** UIDPLUS capability per RFC 4315 */
59    public static final int CAPABILITY_UIDPLUS   = 1 << 3;
60
61    /** The capabilities supported; a set of CAPABILITY_* values. */
62    private int mCapabilities;
63    private static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
64    Transport mTransport;
65    private ImapResponseParser mParser;
66    private ImapStore mImapStore;
67    private String mUsername;
68    private String mLoginPhrase;
69    private String mIdPhrase = null;
70    /** # of command/response lines to log upon crash. */
71    private static final int DISCOURSE_LOGGER_SIZE = 64;
72    private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
73    /**
74     * Next tag to use.  All connections associated to the same ImapStore instance share the same
75     * counter to make tests simpler.
76     * (Some of the tests involve multiple connections but only have a single counter to track the
77     * tag.)
78     */
79    private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
80
81
82    // Keep others from instantiating directly
83    ImapConnection(ImapStore store, String username, String password) {
84        setStore(store, username, password);
85    }
86
87    void setStore(ImapStore store, String username, String password) {
88        if (username != null && password != null) {
89            mUsername = username;
90
91            // build the LOGIN string once (instead of over-and-over again.)
92            // apply the quoting here around the built-up password
93            mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " "
94                    + ImapUtility.imapQuoted(password);
95        }
96        mImapStore = store;
97    }
98    void open() throws IOException, MessagingException {
99        if (mTransport != null && mTransport.isOpen()) {
100            return;
101        }
102
103        try {
104            // copy configuration into a clean transport, if necessary
105            if (mTransport == null) {
106                mTransport = mImapStore.cloneTransport();
107            }
108
109            mTransport.open();
110            mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
111
112            createParser();
113
114            // BANNER
115            mParser.readResponse();
116
117            // CAPABILITY
118            ImapResponse capabilities = queryCapabilities();
119
120            boolean hasStartTlsCapability =
121                capabilities.contains(ImapConstants.STARTTLS);
122
123            // TLS
124            ImapResponse newCapabilities = doStartTls(hasStartTlsCapability);
125            if (newCapabilities != null) {
126                capabilities = newCapabilities;
127            }
128
129            // NOTE: An IMAP response MUST be processed before issuing any new IMAP
130            // requests. Subsequent requests may destroy previous response data. As
131            // such, we save away capability information here for future use.
132            setCapabilities(capabilities);
133            String capabilityString = capabilities.flatten();
134
135            // ID
136            doSendId(isCapable(CAPABILITY_ID), capabilityString);
137
138            // LOGIN
139            doLogin();
140
141            // NAMESPACE (only valid in the Authenticated state)
142            doGetNamespace(isCapable(CAPABILITY_NAMESPACE));
143
144            // Gets the path separator from the server
145            doGetPathSeparator();
146
147            mImapStore.ensurePrefixIsValid();
148        } catch (SSLException e) {
149            if (Email.DEBUG) {
150                Log.d(Logging.LOG_TAG, e.toString());
151            }
152            throw new CertificateValidationException(e.getMessage(), e);
153        } catch (IOException ioe) {
154            // NOTE:  Unlike similar code in POP3, I'm going to rethrow as-is.  There is a lot
155            // of other code here that catches IOException and I don't want to break it.
156            // This catch is only here to enhance logging of connection-time issues.
157            if (Email.DEBUG) {
158                Log.d(Logging.LOG_TAG, ioe.toString());
159            }
160            throw ioe;
161        } finally {
162            destroyResponses();
163        }
164    }
165
166    /**
167     * Closes the connection and releases all resources. This connection can not be used again
168     * until {@link #setStore(ImapStore, String, String)} is called.
169     */
170    void close() {
171        if (mTransport != null) {
172            mTransport.close();
173            mTransport = null;
174        }
175        destroyResponses();
176        mParser = null;
177        mImapStore = null;
178    }
179
180    /**
181     * Returns whether or not the specified capability is supported by the server.
182     */
183    private boolean isCapable(int capability) {
184        return (mCapabilities & capability) != 0;
185    }
186
187    /**
188     * Sets the capability flags according to the response provided by the server.
189     * Note: We only set the capability flags that we are interested in. There are many IMAP
190     * capabilities that we do not track.
191     */
192    private void setCapabilities(ImapResponse capabilities) {
193        if (capabilities.contains(ImapConstants.ID)) {
194            mCapabilities |= CAPABILITY_ID;
195        }
196        if (capabilities.contains(ImapConstants.NAMESPACE)) {
197            mCapabilities |= CAPABILITY_NAMESPACE;
198        }
199        if (capabilities.contains(ImapConstants.UIDPLUS)) {
200            mCapabilities |= CAPABILITY_UIDPLUS;
201        }
202        if (capabilities.contains(ImapConstants.STARTTLS)) {
203            mCapabilities |= CAPABILITY_STARTTLS;
204        }
205    }
206
207    /**
208     * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
209     * set it to {@link #mParser}.
210     *
211     * If we already have an {@link ImapResponseParser}, we
212     * {@link #destroyResponses()} and throw it away.
213     */
214    private void createParser() {
215        destroyResponses();
216        mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse);
217    }
218
219    void destroyResponses() {
220        if (mParser != null) {
221            mParser.destroyResponses();
222        }
223    }
224
225    boolean isTransportOpenForTest() {
226        return mTransport != null ? mTransport.isOpen() : false;
227    }
228
229    ImapResponse readResponse() throws IOException, MessagingException {
230        return mParser.readResponse();
231    }
232
233    /**
234     * Send a single command to the server.  The command will be preceded by an IMAP command
235     * tag and followed by \r\n (caller need not supply them).
236     *
237     * @param command The command to send to the server
238     * @param sensitive If true, the command will not be logged
239     * @return Returns the command tag that was sent
240     */
241    String sendCommand(String command, boolean sensitive)
242        throws MessagingException, IOException {
243        open();
244        String tag = Integer.toString(mNextCommandTag.incrementAndGet());
245        String commandToSend = tag + " " + command;
246        mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
247        mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
248        return tag;
249    }
250
251    List<ImapResponse> executeSimpleCommand(String command) throws IOException,
252            MessagingException {
253        return executeSimpleCommand(command, false);
254    }
255
256    List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
257            throws IOException, MessagingException {
258        String tag = sendCommand(command, sensitive);
259        ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
260        ImapResponse response;
261        do {
262            response = mParser.readResponse();
263            responses.add(response);
264        } while (!response.isTagged());
265        if (!response.isOk()) {
266            final String toString = response.toString();
267            final String alert = response.getAlertTextOrEmpty().getString();
268            destroyResponses();
269            throw new ImapException(toString, alert);
270        }
271        return responses;
272    }
273
274    /**
275     * Query server for capabilities.
276     */
277    private ImapResponse queryCapabilities() throws IOException, MessagingException {
278        ImapResponse capabilityResponse = null;
279        for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
280            if (r.is(0, ImapConstants.CAPABILITY)) {
281                capabilityResponse = r;
282                break;
283            }
284        }
285        if (capabilityResponse == null) {
286            throw new MessagingException("Invalid CAPABILITY response received");
287        }
288        return capabilityResponse;
289    }
290
291    /**
292     * Sends client identification information to the IMAP server per RFC 2971. If
293     * the server does not support the ID command, this will perform no operation.
294     *
295     * Interoperability hack:  Never send ID to *.secureserver.net, which sends back a
296     * malformed response that our parser can't deal with.
297     */
298    private void doSendId(boolean hasIdCapability, String capabilities)
299            throws MessagingException {
300        if (!hasIdCapability) return;
301
302        // Never send ID to *.secureserver.net
303        String host = mTransport.getHost();
304        if (host.toLowerCase().endsWith(".secureserver.net")) return;
305
306        // Assign user-agent string (for RFC2971 ID command)
307        String mUserAgent =
308                ImapStore.getImapId(mImapStore.getContext(), mUsername, host, capabilities);
309
310        if (mUserAgent != null) {
311            mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
312        } else if (DEBUG_FORCE_SEND_ID) {
313            mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
314        }
315        // else: mIdPhrase = null, no ID will be emitted
316
317        // Send user-agent in an RFC2971 ID command
318        if (mIdPhrase != null) {
319            try {
320                executeSimpleCommand(mIdPhrase);
321            } catch (ImapException ie) {
322                // Log for debugging, but this is not a fatal problem.
323                if (Email.DEBUG) {
324                    Log.d(Logging.LOG_TAG, ie.toString());
325                }
326            } catch (IOException ioe) {
327                // Special case to handle malformed OK responses and ignore them.
328                // A true IOException will recur on the following login steps
329                // This can go away after the parser is fixed - see bug 2138981
330            }
331        }
332    }
333
334    /**
335     * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
336     * explicitly sets a namespace (using setup UI) or if the server does not support the
337     * namespace command, this will perform no operation.
338     */
339    private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
340        // user did not specify a hard-coded prefix; try to get it from the server
341        if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) {
342            List<ImapResponse> responseList = Collections.emptyList();
343
344            try {
345                responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
346            } catch (ImapException ie) {
347                // Log for debugging, but this is not a fatal problem.
348                if (Email.DEBUG) {
349                    Log.d(Logging.LOG_TAG, ie.toString());
350                }
351            } catch (IOException ioe) {
352                // Special case to handle malformed OK responses and ignore them.
353            }
354
355            for (ImapResponse response: responseList) {
356                if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
357                    ImapList namespaceList = response.getListOrEmpty(1);
358                    ImapList namespace = namespaceList.getListOrEmpty(0);
359                    String namespaceString = namespace.getStringOrEmpty(0).getString();
360                    if (!TextUtils.isEmpty(namespaceString)) {
361                        mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null));
362                        mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString());
363                    }
364                }
365            }
366        }
367    }
368
369    /**
370     * Logs into the IMAP server
371     */
372    private void doLogin()
373            throws IOException, MessagingException, AuthenticationFailedException {
374        try {
375            // TODO eventually we need to add additional authentication
376            // options such as SASL
377            executeSimpleCommand(mLoginPhrase, true);
378        } catch (ImapException ie) {
379            if (Email.DEBUG) {
380                Log.d(Logging.LOG_TAG, ie.toString());
381            }
382            throw new AuthenticationFailedException(ie.getAlertText(), ie);
383
384        } catch (MessagingException me) {
385            throw new AuthenticationFailedException(null, me);
386        }
387    }
388
389    /**
390     * Gets the path separator per the LIST command in RFC 3501. If the path separator
391     * was obtained while obtaining the namespace or there is no prefix defined, this
392     * will perform no operation.
393     */
394    private void doGetPathSeparator() throws MessagingException {
395        // user did not specify a hard-coded prefix; try to get it from the server
396        if (mImapStore.isUserPrefixSet()) {
397            List<ImapResponse> responseList = Collections.emptyList();
398
399            try {
400                responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
401            } catch (ImapException ie) {
402                // Log for debugging, but this is not a fatal problem.
403                if (Email.DEBUG) {
404                    Log.d(Logging.LOG_TAG, ie.toString());
405                }
406            } catch (IOException ioe) {
407                // Special case to handle malformed OK responses and ignore them.
408            }
409
410            for (ImapResponse response: responseList) {
411                if (response.isDataResponse(0, ImapConstants.LIST)) {
412                    mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString());
413                }
414            }
415        }
416    }
417
418    /**
419     * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
420     * to use TLS or the server does not support the TLS capability, this will perform
421     * no operation.
422     */
423    private ImapResponse doStartTls(boolean hasStartTlsCapability)
424            throws IOException, MessagingException {
425        if (mTransport.canTryTlsSecurity()) {
426            if (hasStartTlsCapability) {
427                // STARTTLS
428                executeSimpleCommand(ImapConstants.STARTTLS);
429
430                mTransport.reopenTls();
431                mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
432                createParser();
433                // Per RFC requirement (3501-6.2.1) gather new capabilities
434                return(queryCapabilities());
435            } else {
436                if (Email.DEBUG) {
437                    Log.d(Logging.LOG_TAG, "TLS not supported but required");
438                }
439                throw new MessagingException(MessagingException.TLS_REQUIRED);
440            }
441        }
442        return null;
443    }
444
445    /** @see DiscourseLogger#logLastDiscourse() */
446    void logLastDiscourse() {
447        mDiscourse.logLastDiscourse();
448    }
449}