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