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