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 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 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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() */ 517 void logLastDiscourse() { 518 mDiscourse.logLastDiscourse(); 519 } 520 }