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