• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.email.mail.store;
18 
19 import android.text.TextUtils;
20 import android.util.Log;
21 
22 import com.android.email.Email;
23 import com.android.email.mail.Transport;
24 import com.android.email.mail.store.ImapStore.ImapException;
25 import com.android.email.mail.store.imap.ImapConstants;
26 import com.android.email.mail.store.imap.ImapList;
27 import com.android.email.mail.store.imap.ImapResponse;
28 import com.android.email.mail.store.imap.ImapResponseParser;
29 import com.android.email.mail.store.imap.ImapUtility;
30 import com.android.email.mail.transport.DiscourseLogger;
31 import com.android.email.mail.transport.MailTransport;
32 import com.android.emailcommon.Logging;
33 import com.android.emailcommon.mail.AuthenticationFailedException;
34 import com.android.emailcommon.mail.CertificateValidationException;
35 import com.android.emailcommon.mail.MessagingException;
36 
37 import java.io.IOException;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.concurrent.atomic.AtomicInteger;
42 
43 import javax.net.ssl.SSLException;
44 
45 /**
46  * A cacheable class that stores the details for a single IMAP connection.
47  */
48 class 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
ImapConnection(ImapStore store, String username, String password)83     ImapConnection(ImapStore store, String username, String password) {
84         setStore(store, username, password);
85     }
86 
setStore(ImapStore store, String username, String password)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     }
open()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      */
close()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      */
isCapable(int capability)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      */
setCapabilities(ImapResponse capabilities)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      */
createParser()214     private void createParser() {
215         destroyResponses();
216         mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse);
217     }
218 
destroyResponses()219     void destroyResponses() {
220         if (mParser != null) {
221             mParser.destroyResponses();
222         }
223     }
224 
isTransportOpenForTest()225     boolean isTransportOpenForTest() {
226         return mTransport != null ? mTransport.isOpen() : false;
227     }
228 
readResponse()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      */
sendCommand(String command, boolean sensitive)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 
252     /**
253      * Send a single, complex command to the server.  The command will be preceded by an IMAP
254      * command tag and followed by \r\n (caller need not supply them).  After each piece of the
255      * command, a response will be read which MUST be a continuation request.
256      *
257      * @param commands An array of Strings comprising the command to be sent to the server
258      * @return Returns the command tag that was sent
259      */
sendComplexCommand(List<String> commands, boolean sensitive)260     String sendComplexCommand(List<String> commands, boolean sensitive) throws MessagingException,
261             IOException {
262         open();
263         String tag = Integer.toString(mNextCommandTag.incrementAndGet());
264         int len = commands.size();
265         for (int i = 0; i < len; i++) {
266             String commandToSend = commands.get(i);
267             // The first part of the command gets the tag
268             if (i == 0) {
269                 commandToSend = tag + " " + commandToSend;
270             } else {
271                 // Otherwise, read the response from the previous part of the command
272                 ImapResponse response = readResponse();
273                 // If it isn't a continuation request, that's an error
274                 if (!response.isContinuationRequest()) {
275                     throw new MessagingException("Expected continuation request");
276                 }
277             }
278             // Send the command
279             mTransport.writeLine(commandToSend, null);
280             mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
281         }
282         return tag;
283     }
284 
executeSimpleCommand(String command)285     List<ImapResponse> executeSimpleCommand(String command) throws IOException,
286             MessagingException {
287         return executeSimpleCommand(command, false);
288     }
289 
290     /**
291      * Read and return all of the responses from the most recent command sent to the server
292      *
293      * @return a list of ImapResponses
294      * @throws IOException
295      * @throws MessagingException
296      */
getCommandResponses()297     List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
298         ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
299         ImapResponse response;
300         do {
301             response = mParser.readResponse();
302             responses.add(response);
303         } while (!response.isTagged());
304         if (!response.isOk()) {
305             final String toString = response.toString();
306             final String alert = response.getAlertTextOrEmpty().getString();
307             destroyResponses();
308             throw new ImapException(toString, alert);
309         }
310         return responses;
311     }
312 
313     /**
314      * Execute a simple command at the server, a simple command being one that is sent in a single
315      * line of text
316      *
317      * @param command the command to send to the server
318      * @param sensitive whether the command should be redacted in logs (used for login)
319      * @return a list of ImapResponses
320      * @throws IOException
321      * @throws MessagingException
322      */
executeSimpleCommand(String command, boolean sensitive)323      List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
324             throws IOException, MessagingException {
325         sendCommand(command, sensitive);
326         return getCommandResponses();
327     }
328 
329      /**
330       * Execute a complex command at the server, a complex command being one that must be sent in
331       * multiple lines due to the use of string literals
332       *
333       * @param commands a list of strings that comprise the command to be sent to the server
334       * @param sensitive whether the command should be redacted in logs (used for login)
335       * @return a list of ImapResponses
336       * @throws IOException
337       * @throws MessagingException
338       */
executeComplexCommand(List<String> commands, boolean sensitive)339       List<ImapResponse> executeComplexCommand(List<String> commands, boolean sensitive)
340             throws IOException, MessagingException {
341         sendComplexCommand(commands, sensitive);
342         return getCommandResponses();
343     }
344 
345     /**
346      * Query server for capabilities.
347      */
queryCapabilities()348     private ImapResponse queryCapabilities() throws IOException, MessagingException {
349         ImapResponse capabilityResponse = null;
350         for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
351             if (r.is(0, ImapConstants.CAPABILITY)) {
352                 capabilityResponse = r;
353                 break;
354             }
355         }
356         if (capabilityResponse == null) {
357             throw new MessagingException("Invalid CAPABILITY response received");
358         }
359         return capabilityResponse;
360     }
361 
362     /**
363      * Sends client identification information to the IMAP server per RFC 2971. If
364      * the server does not support the ID command, this will perform no operation.
365      *
366      * Interoperability hack:  Never send ID to *.secureserver.net, which sends back a
367      * malformed response that our parser can't deal with.
368      */
doSendId(boolean hasIdCapability, String capabilities)369     private void doSendId(boolean hasIdCapability, String capabilities)
370             throws MessagingException {
371         if (!hasIdCapability) return;
372 
373         // Never send ID to *.secureserver.net
374         String host = mTransport.getHost();
375         if (host.toLowerCase().endsWith(".secureserver.net")) return;
376 
377         // Assign user-agent string (for RFC2971 ID command)
378         String mUserAgent =
379                 ImapStore.getImapId(mImapStore.getContext(), mUsername, host, capabilities);
380 
381         if (mUserAgent != null) {
382             mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
383         } else if (DEBUG_FORCE_SEND_ID) {
384             mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
385         }
386         // else: mIdPhrase = null, no ID will be emitted
387 
388         // Send user-agent in an RFC2971 ID command
389         if (mIdPhrase != null) {
390             try {
391                 executeSimpleCommand(mIdPhrase);
392             } catch (ImapException ie) {
393                 // Log for debugging, but this is not a fatal problem.
394                 if (Email.DEBUG) {
395                     Log.d(Logging.LOG_TAG, ie.toString());
396                 }
397             } catch (IOException ioe) {
398                 // Special case to handle malformed OK responses and ignore them.
399                 // A true IOException will recur on the following login steps
400                 // This can go away after the parser is fixed - see bug 2138981
401             }
402         }
403     }
404 
405     /**
406      * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
407      * explicitly sets a namespace (using setup UI) or if the server does not support the
408      * namespace command, this will perform no operation.
409      */
doGetNamespace(boolean hasNamespaceCapability)410     private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
411         // user did not specify a hard-coded prefix; try to get it from the server
412         if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) {
413             List<ImapResponse> responseList = Collections.emptyList();
414 
415             try {
416                 responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
417             } catch (ImapException ie) {
418                 // Log for debugging, but this is not a fatal problem.
419                 if (Email.DEBUG) {
420                     Log.d(Logging.LOG_TAG, ie.toString());
421                 }
422             } catch (IOException ioe) {
423                 // Special case to handle malformed OK responses and ignore them.
424             }
425 
426             for (ImapResponse response: responseList) {
427                 if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
428                     ImapList namespaceList = response.getListOrEmpty(1);
429                     ImapList namespace = namespaceList.getListOrEmpty(0);
430                     String namespaceString = namespace.getStringOrEmpty(0).getString();
431                     if (!TextUtils.isEmpty(namespaceString)) {
432                         mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null));
433                         mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString());
434                     }
435                 }
436             }
437         }
438     }
439 
440     /**
441      * Logs into the IMAP server
442      */
doLogin()443     private void doLogin()
444             throws IOException, MessagingException, AuthenticationFailedException {
445         try {
446             // TODO eventually we need to add additional authentication
447             // options such as SASL
448             executeSimpleCommand(mLoginPhrase, true);
449         } catch (ImapException ie) {
450             if (Email.DEBUG) {
451                 Log.d(Logging.LOG_TAG, ie.toString());
452             }
453             throw new AuthenticationFailedException(ie.getAlertText(), ie);
454 
455         } catch (MessagingException me) {
456             throw new AuthenticationFailedException(null, me);
457         }
458     }
459 
460     /**
461      * Gets the path separator per the LIST command in RFC 3501. If the path separator
462      * was obtained while obtaining the namespace or there is no prefix defined, this
463      * will perform no operation.
464      */
doGetPathSeparator()465     private void doGetPathSeparator() throws MessagingException {
466         // user did not specify a hard-coded prefix; try to get it from the server
467         if (mImapStore.isUserPrefixSet()) {
468             List<ImapResponse> responseList = Collections.emptyList();
469 
470             try {
471                 responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
472             } catch (ImapException ie) {
473                 // Log for debugging, but this is not a fatal problem.
474                 if (Email.DEBUG) {
475                     Log.d(Logging.LOG_TAG, ie.toString());
476                 }
477             } catch (IOException ioe) {
478                 // Special case to handle malformed OK responses and ignore them.
479             }
480 
481             for (ImapResponse response: responseList) {
482                 if (response.isDataResponse(0, ImapConstants.LIST)) {
483                     mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString());
484                 }
485             }
486         }
487     }
488 
489     /**
490      * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
491      * to use TLS or the server does not support the TLS capability, this will perform
492      * no operation.
493      */
doStartTls(boolean hasStartTlsCapability)494     private ImapResponse doStartTls(boolean hasStartTlsCapability)
495             throws IOException, MessagingException {
496         if (mTransport.canTryTlsSecurity()) {
497             if (hasStartTlsCapability) {
498                 // STARTTLS
499                 executeSimpleCommand(ImapConstants.STARTTLS);
500 
501                 mTransport.reopenTls();
502                 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
503                 createParser();
504                 // Per RFC requirement (3501-6.2.1) gather new capabilities
505                 return(queryCapabilities());
506             } else {
507                 if (Email.DEBUG) {
508                     Log.d(Logging.LOG_TAG, "TLS not supported but required");
509                 }
510                 throw new MessagingException(MessagingException.TLS_REQUIRED);
511             }
512         }
513         return null;
514     }
515 
516     /** @see DiscourseLogger#logLastDiscourse() */
logLastDiscourse()517     void logLastDiscourse() {
518         mDiscourse.logLastDiscourse();
519     }
520 }