ImapStore.java revision 32311cce0153fbb2708d871626a0797cc93b7e4e
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.Utility; 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.ImapElement; 27import com.android.email.mail.store.imap.ImapList; 28import com.android.email.mail.store.imap.ImapResponse; 29import com.android.email.mail.store.imap.ImapResponseParser; 30import com.android.email.mail.store.imap.ImapString; 31import com.android.email.mail.transport.CountingOutputStream; 32import com.android.email.mail.transport.DiscourseLogger; 33import com.android.email.mail.transport.EOLConvertingOutputStream; 34import com.android.email.mail.transport.MailTransport; 35import com.android.emailcommon.internet.BinaryTempFileBody; 36import com.android.emailcommon.internet.MimeBodyPart; 37import com.android.emailcommon.internet.MimeHeader; 38import com.android.emailcommon.internet.MimeMessage; 39import com.android.emailcommon.internet.MimeMultipart; 40import com.android.emailcommon.internet.MimeUtility; 41import com.android.emailcommon.mail.AuthenticationFailedException; 42import com.android.emailcommon.mail.Body; 43import com.android.emailcommon.mail.CertificateValidationException; 44import com.android.emailcommon.mail.FetchProfile; 45import com.android.emailcommon.mail.Flag; 46import com.android.emailcommon.mail.Folder; 47import com.android.emailcommon.mail.Message; 48import com.android.emailcommon.mail.MessagingException; 49import com.android.emailcommon.mail.Part; 50import com.android.emailcommon.service.EmailServiceProxy; 51import com.beetstra.jutf7.CharsetProvider; 52 53import android.content.Context; 54import android.os.Build; 55import android.os.Bundle; 56import android.telephony.TelephonyManager; 57import android.text.TextUtils; 58import android.util.Base64; 59import android.util.Base64DataException; 60import android.util.Log; 61 62import java.io.IOException; 63import java.io.InputStream; 64import java.io.OutputStream; 65import java.net.URI; 66import java.net.URISyntaxException; 67import java.nio.ByteBuffer; 68import java.nio.charset.Charset; 69import java.security.MessageDigest; 70import java.security.NoSuchAlgorithmException; 71import java.util.ArrayList; 72import java.util.Collection; 73import java.util.Collections; 74import java.util.Date; 75import java.util.HashMap; 76import java.util.LinkedHashSet; 77import java.util.List; 78import java.util.concurrent.ConcurrentLinkedQueue; 79import java.util.concurrent.atomic.AtomicInteger; 80import java.util.regex.Pattern; 81 82import javax.net.ssl.SSLException; 83 84/** 85 * <pre> 86 * TODO Need to start keeping track of UIDVALIDITY 87 * TODO Need a default response handler for things like folder updates 88 * TODO In fetch(), if we need a ImapMessage and were given 89 * TODO Collect ALERT messages and show them to users. 90 * something else we can try to do a pre-fetch first. 91 * 92 * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for 93 * certain information in a FETCH command, the server may return the requested 94 * information in any order, not necessarily in the order that it was requested. 95 * Further, the server may return the information in separate FETCH responses 96 * and may also return information that was not explicitly requested (to reflect 97 * to the client changes in the state of the subject message). 98 * </pre> 99 */ 100public class ImapStore extends Store { 101 102 // Always check in FALSE 103 private static final boolean DEBUG_FORCE_SEND_ID = false; 104 105 private static final int COPY_BUFFER_SIZE = 16*1024; 106 107 private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED }; 108 109 private final Context mContext; 110 private Transport mRootTransport; 111 private String mUsername; 112 private String mPassword; 113 private String mLoginPhrase; 114 private String mIdPhrase = null; 115 private static String sImapId = null; 116 /*package*/ String mPathPrefix; 117 /*package*/ String mPathSeparator; 118 119 private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool = 120 new ConcurrentLinkedQueue<ImapConnection>(); 121 122 /** 123 * Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. 124 */ 125 private static final Charset MODIFIED_UTF_7_CHARSET = 126 new CharsetProvider().charsetForName("X-RFC-3501"); 127 128 /** 129 * Cache of ImapFolder objects. ImapFolders are attached to a given folder on the server 130 * and as long as their associated connection remains open they are reusable between 131 * requests. This cache lets us make sure we always reuse, if possible, for a given 132 * folder name. 133 */ 134 private HashMap<String, ImapFolder> mFolderCache = new HashMap<String, ImapFolder>(); 135 136 /** 137 * Next tag to use. All connections associated to the same ImapStore instance share the same 138 * counter to make tests simpler. 139 * (Some of the tests involve multiple connections but only have a single counter to track the 140 * tag.) 141 */ 142 private final AtomicInteger mNextCommandTag = new AtomicInteger(0); 143 144 /** 145 * Static named constructor. 146 */ 147 public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks) 148 throws MessagingException { 149 return new ImapStore(context, uri); 150 } 151 152 /** 153 * Allowed formats for the Uri: 154 * imap://user:password@server:port 155 * imap+tls+://user:password@server:port 156 * imap+tls+trustallcerts://user:password@server:port 157 * imap+ssl+://user:password@server:port 158 * imap+ssl+trustallcerts://user:password@server:port 159 * 160 * @param uriString the Uri containing information to configure this store 161 */ 162 private ImapStore(Context context, String uriString) throws MessagingException { 163 mContext = context; 164 URI uri; 165 try { 166 uri = new URI(uriString); 167 } catch (URISyntaxException use) { 168 throw new MessagingException("Invalid ImapStore URI", use); 169 } 170 171 String scheme = uri.getScheme(); 172 if (scheme == null || !scheme.startsWith(STORE_SCHEME_IMAP)) { 173 throw new MessagingException("Unsupported protocol"); 174 } 175 // defaults, which can be changed by security modifiers 176 int connectionSecurity = Transport.CONNECTION_SECURITY_NONE; 177 int defaultPort = 143; 178 // check for security modifiers and apply changes 179 if (scheme.contains("+ssl")) { 180 connectionSecurity = Transport.CONNECTION_SECURITY_SSL; 181 defaultPort = 993; 182 } else if (scheme.contains("+tls")) { 183 connectionSecurity = Transport.CONNECTION_SECURITY_TLS; 184 } 185 boolean trustCertificates = scheme.contains(STORE_SECURITY_TRUST_CERTIFICATES); 186 187 mRootTransport = new MailTransport("IMAP"); 188 mRootTransport.setUri(uri, defaultPort); 189 mRootTransport.setSecurity(connectionSecurity, trustCertificates); 190 191 String[] userInfoParts = mRootTransport.getUserInfoParts(); 192 if (userInfoParts != null) { 193 mUsername = userInfoParts[0]; 194 if (userInfoParts.length > 1) { 195 mPassword = userInfoParts[1]; 196 197 // build the LOGIN string once (instead of over-and-over again.) 198 // apply the quoting here around the built-up password 199 mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " " 200 + Utility.imapQuoted(mPassword); 201 } 202 } 203 204 if ((uri.getPath() != null) && (uri.getPath().length() > 0)) { 205 mPathPrefix = uri.getPath().substring(1); 206 } 207 } 208 209 /* package */ Collection<ImapConnection> getConnectionPoolForTest() { 210 return mConnectionPool; 211 } 212 213 /** 214 * For testing only. Injects a different root transport (it will be copied using 215 * newInstanceWithConfiguration() each time IMAP sets up a new channel). The transport 216 * should already be set up and ready to use. Do not use for real code. 217 * @param testTransport The Transport to inject and use for all future communication. 218 */ 219 /* package */ void setTransport(Transport testTransport) { 220 mRootTransport = testTransport; 221 } 222 223 /** 224 * Return, or create and return, an string suitable for use in an IMAP ID message. 225 * This is constructed similarly to the way the browser sets up its user-agent strings. 226 * See RFC 2971 for more details. The output of this command will be a series of key-value 227 * pairs delimited by spaces (there is no point in returning a structured result because 228 * this will be sent as-is to the IMAP server). No tokens, parenthesis or "ID" are included, 229 * because some connections may append additional values. 230 * 231 * The following IMAP ID keys may be included: 232 * name Android package name of the program 233 * os "android" 234 * os-version "version; model; build-id" 235 * vendor Vendor of the client/server 236 * x-android-device-model Model (only revealed if release build) 237 * x-android-net-operator Mobile network operator (if known) 238 * AGUID A device+account UID 239 * 240 * In addition, a vendor policy .apk can append key/value pairs. 241 * 242 * @param userName the username of the account 243 * @param host the host (server) of the account 244 * @param capabilities a list of the capabilities from the server 245 * @return a String for use in an IMAP ID message. 246 */ 247 /* package */ static String getImapId(Context context, String userName, String host, 248 String capabilities) { 249 // The first section is global to all IMAP connections, and generates the fixed 250 // values in any IMAP ID message 251 synchronized (ImapStore.class) { 252 if (sImapId == null) { 253 TelephonyManager tm = 254 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 255 String networkOperator = tm.getNetworkOperatorName(); 256 if (networkOperator == null) networkOperator = ""; 257 258 sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE, 259 Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER, 260 networkOperator); 261 } 262 } 263 264 // This section is per Store, and adds in a dynamic elements like UID's. 265 // We don't cache the result of this work, because the caller does anyway. 266 StringBuilder id = new StringBuilder(sImapId); 267 268 // Optionally add any vendor-supplied id keys 269 String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host, 270 capabilities); 271 if (vendorId != null) { 272 id.append(' '); 273 id.append(vendorId); 274 } 275 276 // Generate a UID that mixes a "stable" device UID with the email address 277 try { 278 String devUID = Preferences.getPreferences(context).getDeviceUID(); 279 MessageDigest messageDigest; 280 messageDigest = MessageDigest.getInstance("SHA-1"); 281 messageDigest.update(userName.getBytes()); 282 messageDigest.update(devUID.getBytes()); 283 byte[] uid = messageDigest.digest(); 284 String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP); 285 id.append(" \"AGUID\" \""); 286 id.append(hexUid); 287 id.append('\"'); 288 } catch (NoSuchAlgorithmException e) { 289 Log.d(Email.LOG_TAG, "couldn't obtain SHA-1 hash for device UID"); 290 } 291 return id.toString(); 292 } 293 294 /** 295 * Helper function that actually builds the static part of the IMAP ID string. This is 296 * separated from getImapId for testability. There is no escaping or encoding in IMAP ID so 297 * any rogue chars must be filtered here. 298 * 299 * @param packageName context.getPackageName() 300 * @param version Build.VERSION.RELEASE 301 * @param codeName Build.VERSION.CODENAME 302 * @param model Build.MODEL 303 * @param id Build.ID 304 * @param vendor Build.MANUFACTURER 305 * @param networkOperator TelephonyManager.getNetworkOperatorName() 306 * @return the static (never changes) portion of the IMAP ID 307 */ 308 /* package */ static String makeCommonImapId(String packageName, String version, 309 String codeName, String model, String id, String vendor, String networkOperator) { 310 311 // Before building up IMAP ID string, pre-filter the input strings for "legal" chars 312 // This is using a fairly arbitrary char set intended to pass through most reasonable 313 // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space> 314 // The most important thing is *not* to pass parens, quotes, or CRLF, which would break 315 // the format of the IMAP ID list. 316 Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]"); 317 packageName = p.matcher(packageName).replaceAll(""); 318 version = p.matcher(version).replaceAll(""); 319 codeName = p.matcher(codeName).replaceAll(""); 320 model = p.matcher(model).replaceAll(""); 321 id = p.matcher(id).replaceAll(""); 322 vendor = p.matcher(vendor).replaceAll(""); 323 networkOperator = p.matcher(networkOperator).replaceAll(""); 324 325 // "name" "com.android.email" 326 StringBuffer sb = new StringBuffer("\"name\" \""); 327 sb.append(packageName); 328 sb.append("\""); 329 330 // "os" "android" 331 sb.append(" \"os\" \"android\""); 332 333 // "os-version" "version; build-id" 334 sb.append(" \"os-version\" \""); 335 if (version.length() > 0) { 336 sb.append(version); 337 } else { 338 // default to "1.0" 339 sb.append("1.0"); 340 } 341 // add the build ID or build # 342 if (id.length() > 0) { 343 sb.append("; "); 344 sb.append(id); 345 } 346 sb.append("\""); 347 348 // "vendor" "the vendor" 349 if (vendor.length() > 0) { 350 sb.append(" \"vendor\" \""); 351 sb.append(vendor); 352 sb.append("\""); 353 } 354 355 // "x-android-device-model" the device model (on release builds only) 356 if ("REL".equals(codeName)) { 357 if (model.length() > 0) { 358 sb.append(" \"x-android-device-model\" \""); 359 sb.append(model); 360 sb.append("\""); 361 } 362 } 363 364 // "x-android-mobile-net-operator" "name of network operator" 365 if (networkOperator.length() > 0) { 366 sb.append(" \"x-android-mobile-net-operator\" \""); 367 sb.append(networkOperator); 368 sb.append("\""); 369 } 370 371 return sb.toString(); 372 } 373 374 375 @Override 376 public Folder getFolder(String name) { 377 ImapFolder folder; 378 synchronized (mFolderCache) { 379 folder = mFolderCache.get(name); 380 if (folder == null) { 381 folder = new ImapFolder(this, name); 382 mFolderCache.put(name, folder); 383 } 384 } 385 return folder; 386 } 387 388 @Override 389 public Folder[] getAllFolders() throws MessagingException { 390 ImapConnection connection = getConnection(); 391 try { 392 ArrayList<Folder> folders = new ArrayList<Folder>(); 393 // Establish a connection to the IMAP server; if necessary 394 // This ensures a valid prefix if the prefix is automatically set by the server 395 connection.executeSimpleCommand(ImapConstants.NOOP); 396 String imapCommand = ImapConstants.LIST + " \"\" \"*\""; 397 if (mPathPrefix != null) { 398 imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\""; 399 } 400 List<ImapResponse> responses = connection.executeSimpleCommand(imapCommand); 401 for (ImapResponse response : responses) { 402 // S: * LIST (\Noselect) "/" ~/Mail/foo 403 if (response.isDataResponse(0, ImapConstants.LIST)) { 404 boolean includeFolder = true; 405 406 // Get folder name. 407 ImapString encodedFolder = response.getStringOrEmpty(3); 408 if (encodedFolder.isEmpty()) continue; 409 String folder = decodeFolderName(encodedFolder.getString(), mPathPrefix); 410 if (ImapConstants.INBOX.equalsIgnoreCase(folder)) { 411 continue; 412 } 413 414 // Parse attributes. 415 if (response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT)) { 416 includeFolder = false; 417 } 418 if (includeFolder) { 419 folders.add(getFolder(folder)); 420 } 421 } 422 } 423 folders.add(getFolder(ImapConstants.INBOX)); 424 return folders.toArray(new Folder[] {}); 425 } catch (IOException ioe) { 426 connection.close(); 427 throw new MessagingException("Unable to get folder list.", ioe); 428 } catch (AuthenticationFailedException afe) { 429 // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT 430 // commands to the server 431 connection.destroyResponses(); 432 connection = null; 433 throw afe; 434 } finally { 435 if (connection != null) { 436 connection.destroyResponses(); 437 poolConnection(connection); 438 } 439 } 440 } 441 442 @Override 443 public Bundle checkSettings() throws MessagingException { 444 int result = MessagingException.NO_ERROR; 445 Bundle bundle = new Bundle(); 446 ImapConnection connection = new ImapConnection(); 447 try { 448 connection.open(); 449 connection.close(); 450 } catch (IOException ioe) { 451 bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage()); 452 result = MessagingException.IOERROR; 453 } finally { 454 connection.destroyResponses(); 455 } 456 bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); 457 return bundle; 458 } 459 460 /** 461 * Fixes the path prefix, if necessary. The path prefix must always end with the 462 * path separator. 463 */ 464 /*package*/ void ensurePrefixIsValid() { 465 // Make sure the path prefix ends with the path separator 466 if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) { 467 if (!mPathPrefix.endsWith(mPathSeparator)) { 468 mPathPrefix = mPathPrefix + mPathSeparator; 469 } 470 } 471 } 472 473 /** 474 * Gets a connection if one is available from the pool, or creates a new one if not. 475 */ 476 /* package */ ImapConnection getConnection() { 477 ImapConnection connection = null; 478 while ((connection = mConnectionPool.poll()) != null) { 479 try { 480 connection.executeSimpleCommand(ImapConstants.NOOP); 481 break; 482 } catch (MessagingException e) { 483 // Fall through 484 } catch (IOException e) { 485 // Fall through 486 } finally { 487 connection.destroyResponses(); 488 } 489 connection.close(); 490 connection = null; 491 } 492 if (connection == null) { 493 connection = new ImapConnection(); 494 } 495 return connection; 496 } 497 498 /** 499 * Save a {@link ImapConnection} in the pool for reuse. 500 */ 501 /* package */ void poolConnection(ImapConnection connection) { 502 if (connection != null) { 503 mConnectionPool.add(connection); 504 } 505 } 506 507 /** 508 * Prepends the folder name with the given prefix and UTF-7 encodes it. 509 */ 510 /* package */ static String encodeFolderName(String name, String prefix) { 511 // do NOT add the prefix to the special name "INBOX" 512 if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name; 513 514 // Prepend prefix 515 if (prefix != null) { 516 name = prefix + name; 517 } 518 519 // TODO bypass the conversion if name doesn't have special char. 520 ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name); 521 byte[] b = new byte[bb.limit()]; 522 bb.get(b); 523 524 return Utility.fromAscii(b); 525 } 526 527 /** 528 * UTF-7 decodes the folder name and removes the given path prefix. 529 */ 530 /* package */ static String decodeFolderName(String name, String prefix) { 531 // TODO bypass the conversion if name doesn't have special char. 532 String folder; 533 folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString(); 534 if ((prefix != null) && folder.startsWith(prefix)) { 535 folder = folder.substring(prefix.length()); 536 } 537 return folder; 538 } 539 540 /** 541 * Returns UIDs of Messages joined with "," as the separator. 542 */ 543 /* package */ static String joinMessageUids(Message[] messages) { 544 StringBuilder sb = new StringBuilder(); 545 boolean notFirst = false; 546 for (Message m : messages) { 547 if (notFirst) { 548 sb.append(','); 549 } 550 sb.append(m.getUid()); 551 notFirst = true; 552 } 553 return sb.toString(); 554 } 555 556 static class ImapFolder extends Folder { 557 private final ImapStore mStore; 558 private final String mName; 559 private int mMessageCount = -1; 560 private ImapConnection mConnection; 561 private OpenMode mMode; 562 private boolean mExists; 563 564 /*package*/ ImapFolder(ImapStore store, String name) { 565 mStore = store; 566 mName = name; 567 } 568 569 private void destroyResponses() { 570 if (mConnection != null) { 571 mConnection.destroyResponses(); 572 } 573 } 574 575 @Override 576 public void open(OpenMode mode, PersistentDataCallbacks callbacks) 577 throws MessagingException { 578 try { 579 if (isOpen()) { 580 if (mMode == mode) { 581 // Make sure the connection is valid. 582 // If it's not we'll close it down and continue on to get a new one. 583 try { 584 mConnection.executeSimpleCommand(ImapConstants.NOOP); 585 return; 586 587 } catch (IOException ioe) { 588 ioExceptionHandler(mConnection, ioe); 589 } finally { 590 destroyResponses(); 591 } 592 } else { 593 // Return the connection to the pool, if exists. 594 close(false); 595 } 596 } 597 synchronized (this) { 598 mConnection = mStore.getConnection(); 599 } 600 // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk 601 // $MDNSent) 602 // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft 603 // NonJunk $MDNSent \*)] Flags permitted. 604 // * 23 EXISTS 605 // * 0 RECENT 606 // * OK [UIDVALIDITY 1125022061] UIDs valid 607 // * OK [UIDNEXT 57576] Predicted next UID 608 // 2 OK [READ-WRITE] Select completed. 609 try { 610 List<ImapResponse> responses = mConnection.executeSimpleCommand( 611 String.format(ImapConstants.SELECT + " \"%s\"", 612 encodeFolderName(mName, mStore.mPathPrefix))); 613 /* 614 * If the command succeeds we expect the folder has been opened read-write 615 * unless we are notified otherwise in the responses. 616 */ 617 mMode = OpenMode.READ_WRITE; 618 619 int messageCount = -1; 620 for (ImapResponse response : responses) { 621 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 622 messageCount = response.getStringOrEmpty(0).getNumberOrZero(); 623 624 } else if (response.isOk()) { 625 final ImapString responseCode = response.getResponseCodeOrEmpty(); 626 if (responseCode.is(ImapConstants.READ_ONLY)) { 627 mMode = OpenMode.READ_ONLY; 628 } else if (responseCode.is(ImapConstants.READ_WRITE)) { 629 mMode = OpenMode.READ_WRITE; 630 } 631 } else if (response.isTagged()) { // Not OK 632 throw new MessagingException("Can't open mailbox: " 633 + response.getStatusResponseTextOrEmpty()); 634 } 635 } 636 637 if (messageCount == -1) { 638 throw new MessagingException("Did not find message count during select"); 639 } 640 mMessageCount = messageCount; 641 mExists = true; 642 643 } catch (IOException ioe) { 644 throw ioExceptionHandler(mConnection, ioe); 645 } finally { 646 destroyResponses(); 647 } 648 } catch (AuthenticationFailedException e) { 649 // Don't cache this connection, so we're forced to try connecting/login again 650 mConnection = null; 651 close(false); 652 throw e; 653 } catch (MessagingException e) { 654 mExists = false; 655 close(false); 656 throw e; 657 } 658 } 659 660 @Override 661 public boolean isOpen() { 662 return mExists && mConnection != null; 663 } 664 665 @Override 666 public OpenMode getMode() { 667 return mMode; 668 } 669 670 @Override 671 public void close(boolean expunge) { 672 // TODO implement expunge 673 mMessageCount = -1; 674 synchronized (this) { 675 destroyResponses(); 676 mStore.poolConnection(mConnection); 677 mConnection = null; 678 } 679 } 680 681 @Override 682 public String getName() { 683 return mName; 684 } 685 686 @Override 687 public boolean exists() throws MessagingException { 688 if (mExists) { 689 return true; 690 } 691 /* 692 * This method needs to operate in the unselected mode as well as the selected mode 693 * so we must get the connection ourselves if it's not there. We are specifically 694 * not calling checkOpen() since we don't care if the folder is open. 695 */ 696 ImapConnection connection = null; 697 synchronized(this) { 698 if (mConnection == null) { 699 connection = mStore.getConnection(); 700 } else { 701 connection = mConnection; 702 } 703 } 704 try { 705 connection.executeSimpleCommand(String.format( 706 ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")", 707 encodeFolderName(mName, mStore.mPathPrefix))); 708 mExists = true; 709 return true; 710 711 } catch (MessagingException me) { 712 return false; 713 714 } catch (IOException ioe) { 715 throw ioExceptionHandler(connection, ioe); 716 717 } finally { 718 connection.destroyResponses(); 719 if (mConnection == null) { 720 mStore.poolConnection(connection); 721 } 722 } 723 } 724 725 // IMAP supports folder creation 726 @Override 727 public boolean canCreate(FolderType type) { 728 return true; 729 } 730 731 @Override 732 public boolean create(FolderType type) throws MessagingException { 733 /* 734 * This method needs to operate in the unselected mode as well as the selected mode 735 * so we must get the connection ourselves if it's not there. We are specifically 736 * not calling checkOpen() since we don't care if the folder is open. 737 */ 738 ImapConnection connection = null; 739 synchronized(this) { 740 if (mConnection == null) { 741 connection = mStore.getConnection(); 742 } else { 743 connection = mConnection; 744 } 745 } 746 try { 747 connection.executeSimpleCommand(String.format(ImapConstants.CREATE + " \"%s\"", 748 encodeFolderName(mName, mStore.mPathPrefix))); 749 return true; 750 751 } catch (MessagingException me) { 752 return false; 753 754 } catch (IOException ioe) { 755 throw ioExceptionHandler(connection, ioe); 756 757 } finally { 758 connection.destroyResponses(); 759 if (mConnection == null) { 760 mStore.poolConnection(connection); 761 } 762 } 763 } 764 765 @Override 766 public void copyMessages(Message[] messages, Folder folder, 767 MessageUpdateCallbacks callbacks) throws MessagingException { 768 checkOpen(); 769 try { 770 mConnection.executeSimpleCommand( 771 String.format(ImapConstants.UID_COPY + " %s \"%s\"", 772 joinMessageUids(messages), 773 encodeFolderName(folder.getName(), mStore.mPathPrefix))); 774 } catch (IOException ioe) { 775 throw ioExceptionHandler(mConnection, ioe); 776 } finally { 777 destroyResponses(); 778 } 779 } 780 781 @Override 782 public int getMessageCount() { 783 return mMessageCount; 784 } 785 786 @Override 787 public int getUnreadMessageCount() throws MessagingException { 788 checkOpen(); 789 try { 790 int unreadMessageCount = 0; 791 List<ImapResponse> responses = mConnection.executeSimpleCommand(String.format( 792 ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")", 793 encodeFolderName(mName, mStore.mPathPrefix))); 794 // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292) 795 for (ImapResponse response : responses) { 796 if (response.isDataResponse(0, ImapConstants.STATUS)) { 797 unreadMessageCount = response.getListOrEmpty(2) 798 .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero(); 799 } 800 } 801 return unreadMessageCount; 802 } catch (IOException ioe) { 803 throw ioExceptionHandler(mConnection, ioe); 804 } finally { 805 destroyResponses(); 806 } 807 } 808 809 @Override 810 public void delete(boolean recurse) { 811 throw new Error("ImapStore.delete() not yet implemented"); 812 } 813 814 /* package */ String[] searchForUids(String searchCriteria) 815 throws MessagingException { 816 checkOpen(); 817 List<ImapResponse> responses; 818 try { 819 try { 820 responses = mConnection.executeSimpleCommand( 821 ImapConstants.UID_SEARCH + " " + searchCriteria); 822 } catch (ImapException e) { 823 return Utility.EMPTY_STRINGS; // not found; 824 } catch (IOException ioe) { 825 throw ioExceptionHandler(mConnection, ioe); 826 } 827 // S: * SEARCH 2 3 6 828 final ArrayList<String> uids = new ArrayList<String>(); 829 for (ImapResponse response : responses) { 830 if (!response.isDataResponse(0, ImapConstants.SEARCH)) { 831 continue; 832 } 833 // Found SEARCH response data 834 for (int i = 1; i < response.size(); i++) { 835 ImapString s = response.getStringOrEmpty(i); 836 if (s.isString()) { 837 uids.add(s.getString()); 838 } 839 } 840 } 841 return uids.toArray(Utility.EMPTY_STRINGS); 842 } finally { 843 destroyResponses(); 844 } 845 } 846 847 @Override 848 public Message getMessage(String uid) throws MessagingException { 849 checkOpen(); 850 851 String[] uids = searchForUids(ImapConstants.UID + " " + uid); 852 for (int i = 0; i < uids.length; i++) { 853 if (uids[i].equals(uid)) { 854 return new ImapMessage(uid, this); 855 } 856 } 857 return null; 858 } 859 860 @Override 861 public Message[] getMessages(int start, int end, MessageRetrievalListener listener) 862 throws MessagingException { 863 if (start < 1 || end < 1 || end < start) { 864 throw new MessagingException(String.format("Invalid range: %d %d", start, end)); 865 } 866 return getMessagesInternal( 867 searchForUids(String.format("%d:%d NOT DELETED", start, end)), listener); 868 } 869 870 @Override 871 public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { 872 return getMessages(null, listener); 873 } 874 875 @Override 876 public Message[] getMessages(String[] uids, MessageRetrievalListener listener) 877 throws MessagingException { 878 if (uids == null) { 879 uids = searchForUids("1:* NOT DELETED"); 880 } 881 return getMessagesInternal(uids, listener); 882 } 883 884 public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) { 885 final ArrayList<Message> messages = new ArrayList<Message>(uids.length); 886 for (int i = 0; i < uids.length; i++) { 887 final String uid = uids[i]; 888 final ImapMessage message = new ImapMessage(uid, this); 889 messages.add(message); 890 if (listener != null) { 891 listener.messageRetrieved(message); 892 } 893 } 894 return messages.toArray(Message.EMPTY_ARRAY); 895 } 896 897 @Override 898 public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) 899 throws MessagingException { 900 try { 901 fetchInternal(messages, fp, listener); 902 } catch (RuntimeException e) { // Probably a parser error. 903 Log.w(Email.LOG_TAG, "Exception detected: " + e.getMessage()); 904 if (mConnection != null) { 905 mConnection.logLastDiscourse(); 906 } 907 throw e; 908 } 909 } 910 911 public void fetchInternal(Message[] messages, FetchProfile fp, 912 MessageRetrievalListener listener) throws MessagingException { 913 if (messages.length == 0) { 914 return; 915 } 916 checkOpen(); 917 HashMap<String, Message> messageMap = new HashMap<String, Message>(); 918 for (Message m : messages) { 919 messageMap.put(m.getUid(), m); 920 } 921 922 /* 923 * Figure out what command we are going to run: 924 * FLAGS - UID FETCH (FLAGS) 925 * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[ 926 * HEADER.FIELDS (date subject from content-type to cc)]) 927 * STRUCTURE - UID FETCH (BODYSTRUCTURE) 928 * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned 929 * BODY - UID FETCH (BODY.PEEK[]) 930 * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID 931 */ 932 933 final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>(); 934 935 fetchFields.add(ImapConstants.UID); 936 if (fp.contains(FetchProfile.Item.FLAGS)) { 937 fetchFields.add(ImapConstants.FLAGS); 938 } 939 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 940 fetchFields.add(ImapConstants.INTERNALDATE); 941 fetchFields.add(ImapConstants.RFC822_SIZE); 942 fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS); 943 } 944 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 945 fetchFields.add(ImapConstants.BODYSTRUCTURE); 946 } 947 948 if (fp.contains(FetchProfile.Item.BODY_SANE)) { 949 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE); 950 } 951 if (fp.contains(FetchProfile.Item.BODY)) { 952 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK); 953 } 954 955 final Part fetchPart = fp.getFirstPart(); 956 if (fetchPart != null) { 957 String[] partIds = 958 fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); 959 if (partIds != null) { 960 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE 961 + "[" + partIds[0] + "]"); 962 } 963 } 964 965 try { 966 mConnection.sendCommand(String.format( 967 ImapConstants.UID_FETCH + " %s (%s)", joinMessageUids(messages), 968 Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') 969 ), false); 970 ImapResponse response; 971 int messageNumber = 0; 972 do { 973 response = null; 974 try { 975 response = mConnection.readResponse(); 976 977 if (!response.isDataResponse(1, ImapConstants.FETCH)) { 978 continue; // Ignore 979 } 980 final ImapList fetchList = response.getListOrEmpty(2); 981 final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID) 982 .getString(); 983 if (TextUtils.isEmpty(uid)) continue; 984 985 ImapMessage message = (ImapMessage) messageMap.get(uid); 986 if (message == null) continue; 987 988 if (fp.contains(FetchProfile.Item.FLAGS)) { 989 final ImapList flags = 990 fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS); 991 for (int i = 0, count = flags.size(); i < count; i++) { 992 final ImapString flag = flags.getStringOrEmpty(i); 993 if (flag.is(ImapConstants.FLAG_DELETED)) { 994 message.setFlagInternal(Flag.DELETED, true); 995 } else if (flag.is(ImapConstants.FLAG_ANSWERED)) { 996 message.setFlagInternal(Flag.ANSWERED, true); 997 } else if (flag.is(ImapConstants.FLAG_SEEN)) { 998 message.setFlagInternal(Flag.SEEN, true); 999 } else if (flag.is(ImapConstants.FLAG_FLAGGED)) { 1000 message.setFlagInternal(Flag.FLAGGED, true); 1001 } 1002 } 1003 } 1004 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 1005 final Date internalDate = fetchList.getKeyedStringOrEmpty( 1006 ImapConstants.INTERNALDATE).getDateOrNull(); 1007 final int size = fetchList.getKeyedStringOrEmpty( 1008 ImapConstants.RFC822_SIZE).getNumberOrZero(); 1009 final String header = fetchList.getKeyedStringOrEmpty( 1010 ImapConstants.BODY_BRACKET_HEADER, true).getString(); 1011 1012 message.setInternalDate(internalDate); 1013 message.setSize(size); 1014 message.parse(Utility.streamFromAsciiString(header)); 1015 } 1016 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 1017 ImapList bs = fetchList.getKeyedListOrEmpty( 1018 ImapConstants.BODYSTRUCTURE); 1019 if (!bs.isEmpty()) { 1020 try { 1021 parseBodyStructure(bs, message, ImapConstants.TEXT); 1022 } catch (MessagingException e) { 1023 if (Email.LOGD) { 1024 Log.v(Email.LOG_TAG, "Error handling message", e); 1025 } 1026 message.setBody(null); 1027 } 1028 } 1029 } 1030 if (fp.contains(FetchProfile.Item.BODY) 1031 || fp.contains(FetchProfile.Item.BODY_SANE)) { 1032 // Body is keyed by "BODY[...". 1033 // TOOD Should we accept "RFC822" as well?? 1034 // The old code didn't really check the key, so it accepted any literal 1035 // that first appeared. 1036 ImapString body = fetchList.getKeyedStringOrEmpty("BODY[", true); 1037 InputStream bodyStream = body.getAsStream(); 1038 message.parse(bodyStream); 1039 } 1040 if (fetchPart != null && fetchPart.getSize() > 0) { 1041 InputStream bodyStream = 1042 fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream(); 1043 String contentType = fetchPart.getContentType(); 1044 String contentTransferEncoding = fetchPart.getHeader( 1045 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; 1046 1047 // TODO Don't create 2 temp files. 1048 // decodeBody creates BinaryTempFileBody, but we could avoid this 1049 // if we implement ImapStringBody. 1050 // (We'll need to share a temp file. Protect it with a ref-count.) 1051 fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding, 1052 fetchPart.getSize(), listener)); 1053 } 1054 1055 if (listener != null) { 1056 listener.messageRetrieved(message); 1057 } 1058 } finally { 1059 destroyResponses(); 1060 } 1061 } while (!response.isTagged()); 1062 } catch (IOException ioe) { 1063 throw ioExceptionHandler(mConnection, ioe); 1064 } 1065 } 1066 1067 /** 1068 * Removes any content transfer encoding from the stream and returns a Body. 1069 * This code is taken/condensed from MimeUtility.decodeBody 1070 */ 1071 private Body decodeBody(InputStream in, String contentTransferEncoding, int size, 1072 MessageRetrievalListener listener) throws IOException { 1073 // Get a properly wrapped input stream 1074 in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding); 1075 BinaryTempFileBody tempBody = new BinaryTempFileBody(); 1076 OutputStream out = tempBody.getOutputStream(); 1077 try { 1078 byte[] buffer = new byte[COPY_BUFFER_SIZE]; 1079 int n = 0; 1080 int count = 0; 1081 while (-1 != (n = in.read(buffer))) { 1082 out.write(buffer, 0, n); 1083 count += n; 1084 if (listener != null) { 1085 listener.loadAttachmentProgress(count * 100 / size); 1086 } 1087 } 1088 } catch (Base64DataException bde) { 1089 String warning = "\n\n" + Email.getMessageDecodeErrorString(); 1090 out.write(warning.getBytes()); 1091 } finally { 1092 out.close(); 1093 } 1094 return tempBody; 1095 } 1096 1097 @Override 1098 public Flag[] getPermanentFlags() { 1099 return PERMANENT_FLAGS; 1100 } 1101 1102 /** 1103 * Handle any untagged responses that the caller doesn't care to handle themselves. 1104 * @param responses 1105 */ 1106 private void handleUntaggedResponses(List<ImapResponse> responses) { 1107 for (ImapResponse response : responses) { 1108 handleUntaggedResponse(response); 1109 } 1110 } 1111 1112 /** 1113 * Handle an untagged response that the caller doesn't care to handle themselves. 1114 * @param response 1115 */ 1116 private void handleUntaggedResponse(ImapResponse response) { 1117 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 1118 mMessageCount = response.getStringOrEmpty(0).getNumberOrZero(); 1119 } 1120 } 1121 1122 private static void parseBodyStructure(ImapList bs, Part part, String id) 1123 throws MessagingException { 1124 if (bs.getElementOrNone(0).isList()) { 1125 /* 1126 * This is a multipart/* 1127 */ 1128 MimeMultipart mp = new MimeMultipart(); 1129 for (int i = 0, count = bs.size(); i < count; i++) { 1130 ImapElement e = bs.getElementOrNone(i); 1131 if (e.isList()) { 1132 /* 1133 * For each part in the message we're going to add a new BodyPart and parse 1134 * into it. 1135 */ 1136 MimeBodyPart bp = new MimeBodyPart(); 1137 if (id.equals(ImapConstants.TEXT)) { 1138 parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1)); 1139 1140 } else { 1141 parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1)); 1142 } 1143 mp.addBodyPart(bp); 1144 1145 } else { 1146 if (e.isString()) { 1147 mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase()); 1148 } 1149 break; // Ignore the rest of the list. 1150 } 1151 } 1152 part.setBody(mp); 1153 } else { 1154 /* 1155 * This is a body. We need to add as much information as we can find out about 1156 * it to the Part. 1157 */ 1158 1159 /* 1160 body type 1161 body subtype 1162 body parameter parenthesized list 1163 body id 1164 body description 1165 body encoding 1166 body size 1167 */ 1168 1169 final ImapString type = bs.getStringOrEmpty(0); 1170 final ImapString subType = bs.getStringOrEmpty(1); 1171 final String mimeType = 1172 (type.getString() + "/" + subType.getString()).toLowerCase(); 1173 1174 final ImapList bodyParams = bs.getListOrEmpty(2); 1175 final ImapString cid = bs.getStringOrEmpty(3); 1176 final ImapString encoding = bs.getStringOrEmpty(5); 1177 final int size = bs.getStringOrEmpty(6).getNumberOrZero(); 1178 1179 if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) { 1180 // A body type of type MESSAGE and subtype RFC822 1181 // contains, immediately after the basic fields, the 1182 // envelope structure, body structure, and size in 1183 // text lines of the encapsulated message. 1184 // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL, 1185 // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL] 1186 /* 1187 * This will be caught by fetch and handled appropriately. 1188 */ 1189 throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 1190 + " not yet supported."); 1191 } 1192 1193 /* 1194 * Set the content type with as much information as we know right now. 1195 */ 1196 final StringBuilder contentType = new StringBuilder(mimeType); 1197 1198 /* 1199 * If there are body params we might be able to get some more information out 1200 * of them. 1201 */ 1202 for (int i = 1, count = bodyParams.size(); i < count; i += 2) { 1203 1204 // TODO We need to convert " into %22, but 1205 // because MimeUtility.getHeaderParameter doesn't recognize it, 1206 // we can't fix it for now. 1207 contentType.append(String.format(";\n %s=\"%s\"", 1208 bodyParams.getStringOrEmpty(i - 1).getString(), 1209 bodyParams.getStringOrEmpty(i).getString())); 1210 } 1211 1212 part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString()); 1213 1214 // Extension items 1215 final ImapList bodyDisposition; 1216 1217 if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) { 1218 // If media-type is TEXT, 9th element might be: [body-fld-lines] := number 1219 // So, if it's not a list, use 10th element. 1220 // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.) 1221 bodyDisposition = bs.getListOrEmpty(9); 1222 } else { 1223 bodyDisposition = bs.getListOrEmpty(8); 1224 } 1225 1226 final StringBuilder contentDisposition = new StringBuilder(); 1227 1228 if (bodyDisposition.size() > 0) { 1229 final String bodyDisposition0Str = 1230 bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(); 1231 if (!TextUtils.isEmpty(bodyDisposition0Str)) { 1232 contentDisposition.append(bodyDisposition0Str); 1233 } 1234 1235 final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1); 1236 if (!bodyDispositionParams.isEmpty()) { 1237 /* 1238 * If there is body disposition information we can pull some more 1239 * information about the attachment out. 1240 */ 1241 for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) { 1242 1243 // TODO We need to convert " into %22. See above. 1244 contentDisposition.append(String.format(";\n %s=\"%s\"", 1245 bodyDispositionParams.getStringOrEmpty(i - 1) 1246 .getString().toLowerCase(), 1247 bodyDispositionParams.getStringOrEmpty(i).getString())); 1248 } 1249 } 1250 } 1251 1252 if ((size > 0) 1253 && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") 1254 == null)) { 1255 contentDisposition.append(String.format(";\n size=%d", size)); 1256 } 1257 1258 if (contentDisposition.length() > 0) { 1259 /* 1260 * Set the content disposition containing at least the size. Attachment 1261 * handling code will use this down the road. 1262 */ 1263 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, 1264 contentDisposition.toString()); 1265 } 1266 1267 /* 1268 * Set the Content-Transfer-Encoding header. Attachment code will use this 1269 * to parse the body. 1270 */ 1271 if (!encoding.isEmpty()) { 1272 part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, 1273 encoding.getString()); 1274 } 1275 1276 /* 1277 * Set the Content-ID header. 1278 */ 1279 if (!cid.isEmpty()) { 1280 part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString()); 1281 } 1282 1283 if (size > 0) { 1284 if (part instanceof ImapMessage) { 1285 ((ImapMessage) part).setSize(size); 1286 } else if (part instanceof MimeBodyPart) { 1287 ((MimeBodyPart) part).setSize(size); 1288 } else { 1289 throw new MessagingException("Unknown part type " + part.toString()); 1290 } 1291 } 1292 part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); 1293 } 1294 1295 } 1296 1297 /** 1298 * Appends the given messages to the selected folder. This implementation also determines 1299 * the new UID of the given message on the IMAP server and sets the Message's UID to the 1300 * new server UID. 1301 */ 1302 @Override 1303 public void appendMessages(Message[] messages) throws MessagingException { 1304 checkOpen(); 1305 try { 1306 for (Message message : messages) { 1307 // Create output count 1308 CountingOutputStream out = new CountingOutputStream(); 1309 EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); 1310 message.writeTo(eolOut); 1311 eolOut.flush(); 1312 // Create flag list (most often this will be "\SEEN") 1313 String flagList = ""; 1314 Flag[] flags = message.getFlags(); 1315 if (flags.length > 0) { 1316 StringBuilder sb = new StringBuilder(); 1317 for (int i = 0, count = flags.length; i < count; i++) { 1318 Flag flag = flags[i]; 1319 if (flag == Flag.SEEN) { 1320 sb.append(" " + ImapConstants.FLAG_SEEN); 1321 } else if (flag == Flag.FLAGGED) { 1322 sb.append(" " + ImapConstants.FLAG_FLAGGED); 1323 } 1324 } 1325 if (sb.length() > 0) { 1326 flagList = sb.substring(1); 1327 } 1328 } 1329 1330 mConnection.sendCommand( 1331 String.format(ImapConstants.APPEND + " \"%s\" (%s) {%d}", 1332 encodeFolderName(mName, mStore.mPathPrefix), 1333 flagList, 1334 out.getCount()), false); 1335 ImapResponse response; 1336 do { 1337 response = mConnection.readResponse(); 1338 if (response.isContinuationRequest()) { 1339 eolOut = new EOLConvertingOutputStream( 1340 mConnection.mTransport.getOutputStream()); 1341 message.writeTo(eolOut); 1342 eolOut.write('\r'); 1343 eolOut.write('\n'); 1344 eolOut.flush(); 1345 } else if (!response.isTagged()) { 1346 handleUntaggedResponse(response); 1347 } 1348 } while (!response.isTagged()); 1349 1350 // TODO Why not check the response? 1351 1352 /* 1353 * Try to recover the UID of the message from an APPENDUID response. 1354 * e.g. 11 OK [APPENDUID 2 238268] APPEND completed 1355 */ 1356 final ImapList appendList = response.getListOrEmpty(1); 1357 if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) { 1358 String serverUid = appendList.getStringOrEmpty(2).getString(); 1359 if (!TextUtils.isEmpty(serverUid)) { 1360 message.setUid(serverUid); 1361 continue; 1362 } 1363 } 1364 1365 /* 1366 * Try to find the UID of the message we just appended using the 1367 * Message-ID header. If there are more than one response, take the 1368 * last one, as it's most likely the newest (the one we just uploaded). 1369 */ 1370 String messageId = message.getMessageId(); 1371 if (messageId == null || messageId.length() == 0) { 1372 continue; 1373 } 1374 String[] uids = searchForUids( 1375 String.format("(HEADER MESSAGE-ID %s)", messageId)); 1376 if (uids.length > 0) { 1377 message.setUid(uids[0]); 1378 } 1379 } 1380 } catch (IOException ioe) { 1381 throw ioExceptionHandler(mConnection, ioe); 1382 } finally { 1383 destroyResponses(); 1384 } 1385 } 1386 1387 @Override 1388 public Message[] expunge() throws MessagingException { 1389 checkOpen(); 1390 try { 1391 handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE)); 1392 } catch (IOException ioe) { 1393 throw ioExceptionHandler(mConnection, ioe); 1394 } finally { 1395 destroyResponses(); 1396 } 1397 return null; 1398 } 1399 1400 @Override 1401 public void setFlags(Message[] messages, Flag[] flags, boolean value) 1402 throws MessagingException { 1403 checkOpen(); 1404 1405 String allFlags = ""; 1406 if (flags.length > 0) { 1407 StringBuilder flagList = new StringBuilder(); 1408 for (int i = 0, count = flags.length; i < count; i++) { 1409 Flag flag = flags[i]; 1410 if (flag == Flag.SEEN) { 1411 flagList.append(" " + ImapConstants.FLAG_SEEN); 1412 } else if (flag == Flag.DELETED) { 1413 flagList.append(" " + ImapConstants.FLAG_DELETED); 1414 } else if (flag == Flag.FLAGGED) { 1415 flagList.append(" " + ImapConstants.FLAG_FLAGGED); 1416 } 1417 } 1418 allFlags = flagList.substring(1); 1419 } 1420 try { 1421 mConnection.executeSimpleCommand(String.format( 1422 ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)", 1423 joinMessageUids(messages), 1424 value ? "+" : "-", 1425 allFlags)); 1426 1427 } catch (IOException ioe) { 1428 throw ioExceptionHandler(mConnection, ioe); 1429 } finally { 1430 destroyResponses(); 1431 } 1432 } 1433 1434 private void checkOpen() throws MessagingException { 1435 if (!isOpen()) { 1436 throw new MessagingException("Folder " + mName + " is not open."); 1437 } 1438 } 1439 1440 private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) { 1441 if (Email.DEBUG) { 1442 Log.d(Email.LOG_TAG, "IO Exception detected: ", ioe); 1443 } 1444 connection.destroyResponses(); 1445 connection.close(); 1446 if (connection == mConnection) { 1447 mConnection = null; // To prevent close() from returning the connection to the pool. 1448 close(false); 1449 } 1450 return new MessagingException("IO Error", ioe); 1451 } 1452 1453 @Override 1454 public boolean equals(Object o) { 1455 if (o instanceof ImapFolder) { 1456 return ((ImapFolder)o).mName.equals(mName); 1457 } 1458 return super.equals(o); 1459 } 1460 1461 @Override 1462 public Message createMessage(String uid) { 1463 return new ImapMessage(uid, this); 1464 } 1465 } 1466 1467 /** 1468 * A cacheable class that stores the details for a single IMAP connection. 1469 */ 1470 class ImapConnection { 1471 private static final String IMAP_DEDACTED_LOG = "[IMAP command redacted]"; 1472 private Transport mTransport; 1473 private ImapResponseParser mParser; 1474 /** # of command/response lines to log upon crash. */ 1475 private static final int DISCOURSE_LOGGER_SIZE = 64; 1476 private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE); 1477 1478 public void open() throws IOException, MessagingException { 1479 if (mTransport != null && mTransport.isOpen()) { 1480 return; 1481 } 1482 1483 try { 1484 // copy configuration into a clean transport, if necessary 1485 if (mTransport == null) { 1486 mTransport = mRootTransport.newInstanceWithConfiguration(); 1487 } 1488 1489 mTransport.open(); 1490 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 1491 1492 createParser(); 1493 1494 // BANNER 1495 mParser.readResponse(); 1496 1497 // CAPABILITY 1498 ImapResponse capabilities = queryCapabilities(); 1499 1500 boolean hasStartTlsCapability = 1501 capabilities.contains(ImapConstants.STARTTLS); 1502 1503 // TLS 1504 ImapResponse newCapabilities = doStartTls(hasStartTlsCapability); 1505 if (newCapabilities != null) { 1506 capabilities = newCapabilities; 1507 } 1508 1509 // NOTE: An IMAP response MUST be processed before issuing any new IMAP 1510 // requests. Subsequent requests may destroy previous response data. As 1511 // such, we save away capability information here for future use. 1512 boolean hasIdCapability = 1513 capabilities.contains(ImapConstants.ID); 1514 boolean hasNamespaceCapability = 1515 capabilities.contains(ImapConstants.NAMESPACE); 1516 String capabilityString = capabilities.flatten(); 1517 1518 // ID 1519 doSendId(hasIdCapability, capabilityString); 1520 1521 // LOGIN 1522 doLogin(); 1523 1524 // NAMESPACE (only valid in the Authenticated state) 1525 doGetNamespace(hasNamespaceCapability); 1526 1527 // Gets the path separator from the server 1528 doGetPathSeparator(); 1529 1530 ensurePrefixIsValid(); 1531 } catch (SSLException e) { 1532 if (Email.DEBUG) { 1533 Log.d(Email.LOG_TAG, e.toString()); 1534 } 1535 throw new CertificateValidationException(e.getMessage(), e); 1536 } catch (IOException ioe) { 1537 // NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot 1538 // of other code here that catches IOException and I don't want to break it. 1539 // This catch is only here to enhance logging of connection-time issues. 1540 if (Email.DEBUG) { 1541 Log.d(Email.LOG_TAG, ioe.toString()); 1542 } 1543 throw ioe; 1544 } finally { 1545 destroyResponses(); 1546 } 1547 } 1548 1549 public void close() { 1550 if (mTransport != null) { 1551 mTransport.close(); 1552 mTransport = null; 1553 } 1554 } 1555 1556 /** 1557 * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and 1558 * set it to {@link #mParser}. 1559 * 1560 * If we already have an {@link ImapResponseParser}, we 1561 * {@link #destroyResponses()} and throw it away. 1562 */ 1563 private void createParser() { 1564 destroyResponses(); 1565 mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse); 1566 } 1567 1568 public void destroyResponses() { 1569 if (mParser != null) { 1570 mParser.destroyResponses(); 1571 } 1572 } 1573 1574 /* package */ boolean isTransportOpenForTest() { 1575 return mTransport != null ? mTransport.isOpen() : false; 1576 } 1577 1578 public ImapResponse readResponse() throws IOException, MessagingException { 1579 return mParser.readResponse(); 1580 } 1581 1582 /** 1583 * Send a single command to the server. The command will be preceded by an IMAP command 1584 * tag and followed by \r\n (caller need not supply them). 1585 * 1586 * @param command The command to send to the server 1587 * @param sensitive If true, the command will not be logged 1588 * @return Returns the command tag that was sent 1589 */ 1590 public String sendCommand(String command, boolean sensitive) 1591 throws MessagingException, IOException { 1592 open(); 1593 String tag = Integer.toString(mNextCommandTag.incrementAndGet()); 1594 String commandToSend = tag + " " + command; 1595 mTransport.writeLine(commandToSend, sensitive ? IMAP_DEDACTED_LOG : null); 1596 mDiscourse.addSentCommand(sensitive ? IMAP_DEDACTED_LOG : commandToSend); 1597 return tag; 1598 } 1599 1600 /*package*/ List<ImapResponse> executeSimpleCommand(String command) throws IOException, 1601 MessagingException { 1602 return executeSimpleCommand(command, false); 1603 } 1604 1605 /*package*/ List<ImapResponse> executeSimpleCommand(String command, boolean sensitive) 1606 throws IOException, MessagingException { 1607 String tag = sendCommand(command, sensitive); 1608 ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>(); 1609 ImapResponse response; 1610 do { 1611 response = mParser.readResponse(); 1612 responses.add(response); 1613 } while (!response.isTagged()); 1614 if (!response.isOk()) { 1615 final String toString = response.toString(); 1616 final String alert = response.getAlertTextOrEmpty().getString(); 1617 destroyResponses(); 1618 throw new ImapException(toString, alert); 1619 } 1620 return responses; 1621 } 1622 1623 /** 1624 * Query server for capabilities. 1625 */ 1626 private ImapResponse queryCapabilities() throws IOException, MessagingException { 1627 ImapResponse capabilityResponse = null; 1628 for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) { 1629 if (r.is(0, ImapConstants.CAPABILITY)) { 1630 capabilityResponse = r; 1631 break; 1632 } 1633 } 1634 if (capabilityResponse == null) { 1635 throw new MessagingException("Invalid CAPABILITY response received"); 1636 } 1637 return capabilityResponse; 1638 } 1639 1640 /** 1641 * Sends client identification information to the IMAP server per RFC 2971. If 1642 * the server does not support the ID command, this will perform no operation. 1643 */ 1644 private void doSendId(boolean hasIdCapability, String capabilities) 1645 throws MessagingException { 1646 if (!hasIdCapability) return; 1647 1648 // Assign user-agent string (for RFC2971 ID command) 1649 String mUserAgent = 1650 getImapId(mContext, mUsername, mRootTransport.getHost(), capabilities); 1651 1652 if (mUserAgent != null) { 1653 mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")"; 1654 } else if (DEBUG_FORCE_SEND_ID) { 1655 mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL; 1656 } 1657 // else: mIdPhrase = null, no ID will be emitted 1658 1659 // Send user-agent in an RFC2971 ID command 1660 if (mIdPhrase != null) { 1661 try { 1662 executeSimpleCommand(mIdPhrase); 1663 } catch (ImapException ie) { 1664 // Log for debugging, but this is not a fatal problem. 1665 if (Email.DEBUG) { 1666 Log.d(Email.LOG_TAG, ie.toString()); 1667 } 1668 } catch (IOException ioe) { 1669 // Special case to handle malformed OK responses and ignore them. 1670 // A true IOException will recur on the following login steps 1671 // This can go away after the parser is fixed - see bug 2138981 1672 } 1673 } 1674 } 1675 1676 /** 1677 * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user 1678 * explicitly sets a namespace (using setup UI) or if the server does not support the 1679 * namespace command, this will perform no operation. 1680 */ 1681 private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException { 1682 // user did not specify a hard-coded prefix; try to get it from the server 1683 if (hasNamespaceCapability && TextUtils.isEmpty(mPathPrefix)) { 1684 List<ImapResponse> responseList = Collections.emptyList(); 1685 1686 try { 1687 responseList = executeSimpleCommand(ImapConstants.NAMESPACE); 1688 } catch (ImapException ie) { 1689 // Log for debugging, but this is not a fatal problem. 1690 if (Email.DEBUG) { 1691 Log.d(Email.LOG_TAG, ie.toString()); 1692 } 1693 } catch (IOException ioe) { 1694 // Special case to handle malformed OK responses and ignore them. 1695 } 1696 1697 for (ImapResponse response: responseList) { 1698 if (response.isDataResponse(0, ImapConstants.NAMESPACE)) { 1699 ImapList namespaceList = response.getListOrEmpty(1); 1700 ImapList namespace = namespaceList.getListOrEmpty(0); 1701 String namespaceString = namespace.getStringOrEmpty(0).getString(); 1702 if (!TextUtils.isEmpty(namespaceString)) { 1703 mPathPrefix = decodeFolderName(namespaceString, null); 1704 mPathSeparator = namespace.getStringOrEmpty(1).getString(); 1705 } 1706 } 1707 } 1708 } 1709 } 1710 1711 /** 1712 * Logs into the IMAP server 1713 */ 1714 private void doLogin() 1715 throws IOException, MessagingException, AuthenticationFailedException { 1716 try { 1717 // TODO eventually we need to add additional authentication 1718 // options such as SASL 1719 executeSimpleCommand(mLoginPhrase, true); 1720 } catch (ImapException ie) { 1721 if (Email.DEBUG) { 1722 Log.d(Email.LOG_TAG, ie.toString()); 1723 } 1724 throw new AuthenticationFailedException(ie.getAlertText(), ie); 1725 1726 } catch (MessagingException me) { 1727 throw new AuthenticationFailedException(null, me); 1728 } 1729 } 1730 1731 /** 1732 * Gets the path separator per the LIST command in RFC 3501. If the path separator 1733 * was obtained while obtaining the namespace or there is no prefix defined, this 1734 * will perform no operation. 1735 */ 1736 private void doGetPathSeparator() throws MessagingException { 1737 // user did not specify a hard-coded prefix; try to get it from the server 1738 if (TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix)) { 1739 List<ImapResponse> responseList = Collections.emptyList(); 1740 1741 try { 1742 responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\""); 1743 } catch (ImapException ie) { 1744 // Log for debugging, but this is not a fatal problem. 1745 if (Email.DEBUG) { 1746 Log.d(Email.LOG_TAG, ie.toString()); 1747 } 1748 } catch (IOException ioe) { 1749 // Special case to handle malformed OK responses and ignore them. 1750 } 1751 1752 for (ImapResponse response: responseList) { 1753 if (response.isDataResponse(0, ImapConstants.LIST)) { 1754 mPathSeparator = response.getStringOrEmpty(2).getString(); 1755 } 1756 } 1757 } 1758 } 1759 1760 /** 1761 * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted 1762 * to use TLS or the server does not support the TLS capability, this will perform 1763 * no operation. 1764 */ 1765 private ImapResponse doStartTls(boolean hasStartTlsCapability) 1766 throws IOException, MessagingException { 1767 if (mTransport.canTryTlsSecurity()) { 1768 if (hasStartTlsCapability) { 1769 // STARTTLS 1770 executeSimpleCommand(ImapConstants.STARTTLS); 1771 1772 mTransport.reopenTls(); 1773 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 1774 createParser(); 1775 // Per RFC requirement (3501-6.2.1) gather new capabilities 1776 return(queryCapabilities()); 1777 } else { 1778 if (Email.DEBUG) { 1779 Log.d(Email.LOG_TAG, "TLS not supported but required"); 1780 } 1781 throw new MessagingException(MessagingException.TLS_REQUIRED); 1782 } 1783 } 1784 return null; 1785 } 1786 1787 /** @see DiscourseLogger#logLastDiscourse() */ 1788 public void logLastDiscourse() { 1789 mDiscourse.logLastDiscourse(); 1790 } 1791 } 1792 1793 static class ImapMessage extends MimeMessage { 1794 ImapMessage(String uid, Folder folder) { 1795 this.mUid = uid; 1796 this.mFolder = folder; 1797 } 1798 1799 public void setSize(int size) { 1800 this.mSize = size; 1801 } 1802 1803 @Override 1804 public void parse(InputStream in) throws IOException, MessagingException { 1805 super.parse(in); 1806 } 1807 1808 public void setFlagInternal(Flag flag, boolean set) throws MessagingException { 1809 super.setFlag(flag, set); 1810 } 1811 1812 @Override 1813 public void setFlag(Flag flag, boolean set) throws MessagingException { 1814 super.setFlag(flag, set); 1815 mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); 1816 } 1817 } 1818 1819 static class ImapException extends MessagingException { 1820 private static final long serialVersionUID = 1L; 1821 1822 String mAlertText; 1823 1824 public ImapException(String message, String alertText, Throwable throwable) { 1825 super(message, throwable); 1826 this.mAlertText = alertText; 1827 } 1828 1829 public ImapException(String message, String alertText) { 1830 super(message); 1831 this.mAlertText = alertText; 1832 } 1833 1834 public String getAlertText() { 1835 return mAlertText; 1836 } 1837 1838 public void setAlertText(String alertText) { 1839 mAlertText = alertText; 1840 } 1841 } 1842} 1843