ImapStore.java revision d31238ca881354938b9d923819da3c63ffb4ac12
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.store.imap.ImapUtility; 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.Logging; 36import com.android.emailcommon.internet.BinaryTempFileBody; 37import com.android.emailcommon.internet.MimeBodyPart; 38import com.android.emailcommon.internet.MimeHeader; 39import com.android.emailcommon.internet.MimeMessage; 40import com.android.emailcommon.internet.MimeMultipart; 41import com.android.emailcommon.internet.MimeUtility; 42import com.android.emailcommon.mail.AuthenticationFailedException; 43import com.android.emailcommon.mail.Body; 44import com.android.emailcommon.mail.CertificateValidationException; 45import com.android.emailcommon.mail.FetchProfile; 46import com.android.emailcommon.mail.Flag; 47import com.android.emailcommon.mail.Folder; 48import com.android.emailcommon.mail.Message; 49import com.android.emailcommon.mail.MessagingException; 50import com.android.emailcommon.mail.Part; 51import com.android.emailcommon.service.EmailServiceProxy; 52import com.android.emailcommon.utility.Utility; 53import com.beetstra.jutf7.CharsetProvider; 54 55import android.content.Context; 56import android.os.Build; 57import android.os.Bundle; 58import android.telephony.TelephonyManager; 59import android.text.TextUtils; 60import android.util.Base64; 61import android.util.Base64DataException; 62import android.util.Log; 63 64import java.io.IOException; 65import java.io.InputStream; 66import java.io.OutputStream; 67import java.net.URI; 68import java.net.URISyntaxException; 69import java.nio.ByteBuffer; 70import java.nio.charset.Charset; 71import java.security.MessageDigest; 72import java.security.NoSuchAlgorithmException; 73import java.util.ArrayList; 74import java.util.Collection; 75import java.util.Collections; 76import java.util.Date; 77import java.util.HashMap; 78import java.util.LinkedHashSet; 79import java.util.List; 80import java.util.concurrent.ConcurrentLinkedQueue; 81import java.util.concurrent.atomic.AtomicInteger; 82import java.util.regex.Pattern; 83 84import javax.net.ssl.SSLException; 85 86/** 87 * <pre> 88 * TODO Need to start keeping track of UIDVALIDITY 89 * TODO Need a default response handler for things like folder updates 90 * TODO In fetch(), if we need a ImapMessage and were given 91 * TODO Collect ALERT messages and show them to users. 92 * something else we can try to do a pre-fetch first. 93 * 94 * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for 95 * certain information in a FETCH command, the server may return the requested 96 * information in any order, not necessarily in the order that it was requested. 97 * Further, the server may return the information in separate FETCH responses 98 * and may also return information that was not explicitly requested (to reflect 99 * to the client changes in the state of the subject message). 100 * </pre> 101 */ 102public class ImapStore extends Store { 103 104 // Always check in FALSE 105 private static final boolean DEBUG_FORCE_SEND_ID = false; 106 107 private static final int COPY_BUFFER_SIZE = 16*1024; 108 109 private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED }; 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 + ImapUtility.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 doSelect(); 612 } catch (IOException ioe) { 613 throw ioExceptionHandler(mConnection, ioe); 614 } finally { 615 destroyResponses(); 616 } 617 } catch (AuthenticationFailedException e) { 618 // Don't cache this connection, so we're forced to try connecting/login again 619 mConnection = null; 620 close(false); 621 throw e; 622 } catch (MessagingException e) { 623 mExists = false; 624 close(false); 625 throw e; 626 } 627 } 628 629 @Override 630 public boolean isOpen() { 631 return mExists && mConnection != null; 632 } 633 634 @Override 635 public OpenMode getMode() { 636 return mMode; 637 } 638 639 @Override 640 public void close(boolean expunge) { 641 // TODO implement expunge 642 mMessageCount = -1; 643 synchronized (this) { 644 destroyResponses(); 645 mStore.poolConnection(mConnection); 646 mConnection = null; 647 } 648 } 649 650 @Override 651 public String getName() { 652 return mName; 653 } 654 655 @Override 656 public boolean exists() throws MessagingException { 657 if (mExists) { 658 return true; 659 } 660 /* 661 * This method needs to operate in the unselected mode as well as the selected mode 662 * so we must get the connection ourselves if it's not there. We are specifically 663 * not calling checkOpen() since we don't care if the folder is open. 664 */ 665 ImapConnection connection = null; 666 synchronized(this) { 667 if (mConnection == null) { 668 connection = mStore.getConnection(); 669 } else { 670 connection = mConnection; 671 } 672 } 673 try { 674 connection.executeSimpleCommand(String.format( 675 ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")", 676 encodeFolderName(mName, mStore.mPathPrefix))); 677 mExists = true; 678 return true; 679 680 } catch (MessagingException me) { 681 return false; 682 683 } catch (IOException ioe) { 684 throw ioExceptionHandler(connection, ioe); 685 686 } finally { 687 connection.destroyResponses(); 688 if (mConnection == null) { 689 mStore.poolConnection(connection); 690 } 691 } 692 } 693 694 // IMAP supports folder creation 695 @Override 696 public boolean canCreate(FolderType type) { 697 return true; 698 } 699 700 @Override 701 public boolean create(FolderType type) throws MessagingException { 702 /* 703 * This method needs to operate in the unselected mode as well as the selected mode 704 * so we must get the connection ourselves if it's not there. We are specifically 705 * not calling checkOpen() since we don't care if the folder is open. 706 */ 707 ImapConnection connection = null; 708 synchronized(this) { 709 if (mConnection == null) { 710 connection = mStore.getConnection(); 711 } else { 712 connection = mConnection; 713 } 714 } 715 try { 716 connection.executeSimpleCommand(String.format(ImapConstants.CREATE + " \"%s\"", 717 encodeFolderName(mName, mStore.mPathPrefix))); 718 return true; 719 720 } catch (MessagingException me) { 721 return false; 722 723 } catch (IOException ioe) { 724 throw ioExceptionHandler(connection, ioe); 725 726 } finally { 727 connection.destroyResponses(); 728 if (mConnection == null) { 729 mStore.poolConnection(connection); 730 } 731 } 732 } 733 734 @Override 735 public void copyMessages(Message[] messages, Folder folder, 736 MessageUpdateCallbacks callbacks) throws MessagingException { 737 checkOpen(); 738 try { 739 List<ImapResponse> responseList = mConnection.executeSimpleCommand( 740 String.format(ImapConstants.UID_COPY + " %s \"%s\"", 741 joinMessageUids(messages), 742 encodeFolderName(folder.getName(), mStore.mPathPrefix))); 743 // Build a message map for faster UID matching 744 HashMap<String, Message> messageMap = new HashMap<String, Message>(); 745 boolean handledUidPlus = false; 746 for (Message m : messages) { 747 messageMap.put(m.getUid(), m); 748 } 749 // Process response to get the new UIDs 750 for (ImapResponse response : responseList) { 751 // All "BAD" responses are bad. Only "NO", tagged responses are bad. 752 if (response.isBad() || (response.isNo() && response.isTagged())) { 753 String responseText = response.getStatusResponseTextOrEmpty().getString(); 754 throw new MessagingException(responseText); 755 } 756 // Skip untagged responses; they're just status 757 if (!response.isTagged()) { 758 continue; 759 } 760 // No callback provided to report of UID changes; nothing more to do here 761 // NOTE: We check this here to catch any server errors 762 if (callbacks == null) { 763 continue; 764 } 765 ImapList copyResponse = response.getListOrEmpty(1); 766 String responseCode = copyResponse.getStringOrEmpty(0).getString(); 767 if (ImapConstants.COPYUID.equals(responseCode)) { 768 handledUidPlus = true; 769 String origIdSet = copyResponse.getStringOrEmpty(2).getString(); 770 String newIdSet = copyResponse.getStringOrEmpty(3).getString(); 771 String[] origIdArray = ImapUtility.getImapSequenceValues(origIdSet); 772 String[] newIdArray = ImapUtility.getImapSequenceValues(newIdSet); 773 // There has to be a 1:1 mapping between old and new IDs 774 if (origIdArray.length != newIdArray.length) { 775 throw new MessagingException("Set length mis-match; orig IDs \"" + 776 origIdSet + "\" new IDs \"" + newIdSet + "\""); 777 } 778 for (int i = 0; i < origIdArray.length; i++) { 779 final String id = origIdArray[i]; 780 final Message m = messageMap.get(id); 781 if (m != null) { 782 callbacks.onMessageUidChange(m, newIdArray[i]); 783 } 784 } 785 } 786 } 787 // If the server doesn't support UIDPLUS, try a different way to get the new UID(s) 788 if (callbacks != null && !handledUidPlus) { 789 ImapFolder newFolder = (ImapFolder)folder; 790 try { 791 // Temporarily select the destination folder 792 newFolder.open(OpenMode.READ_WRITE, null); 793 // Do the search(es) ... 794 for (Message m : messages) { 795 String searchString = "HEADER Message-Id \"" + m.getMessageId() + "\""; 796 String[] newIdArray = newFolder.searchForUids(searchString); 797 if (newIdArray.length == 1) { 798 callbacks.onMessageUidChange(m, newIdArray[0]); 799 } 800 } 801 } catch (MessagingException e) { 802 // Log, but, don't abort; failures here don't need to be propagated 803 Log.d(Logging.LOG_TAG, "Failed to find message", e); 804 } finally { 805 newFolder.close(false); 806 } 807 // Re-select the original folder 808 doSelect(); 809 } 810 } catch (IOException ioe) { 811 throw ioExceptionHandler(mConnection, ioe); 812 } finally { 813 destroyResponses(); 814 } 815 } 816 817 @Override 818 public int getMessageCount() { 819 return mMessageCount; 820 } 821 822 @Override 823 public int getUnreadMessageCount() throws MessagingException { 824 checkOpen(); 825 try { 826 int unreadMessageCount = 0; 827 List<ImapResponse> responses = mConnection.executeSimpleCommand(String.format( 828 ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")", 829 encodeFolderName(mName, mStore.mPathPrefix))); 830 // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292) 831 for (ImapResponse response : responses) { 832 if (response.isDataResponse(0, ImapConstants.STATUS)) { 833 unreadMessageCount = response.getListOrEmpty(2) 834 .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero(); 835 } 836 } 837 return unreadMessageCount; 838 } catch (IOException ioe) { 839 throw ioExceptionHandler(mConnection, ioe); 840 } finally { 841 destroyResponses(); 842 } 843 } 844 845 @Override 846 public void delete(boolean recurse) { 847 throw new Error("ImapStore.delete() not yet implemented"); 848 } 849 850 /* package */ String[] searchForUids(String searchCriteria) 851 throws MessagingException { 852 checkOpen(); 853 List<ImapResponse> responses; 854 try { 855 try { 856 responses = mConnection.executeSimpleCommand( 857 ImapConstants.UID_SEARCH + " " + searchCriteria); 858 } catch (ImapException e) { 859 return Utility.EMPTY_STRINGS; // not found; 860 } catch (IOException ioe) { 861 throw ioExceptionHandler(mConnection, ioe); 862 } 863 // S: * SEARCH 2 3 6 864 final ArrayList<String> uids = new ArrayList<String>(); 865 for (ImapResponse response : responses) { 866 if (!response.isDataResponse(0, ImapConstants.SEARCH)) { 867 continue; 868 } 869 // Found SEARCH response data 870 for (int i = 1; i < response.size(); i++) { 871 ImapString s = response.getStringOrEmpty(i); 872 if (s.isString()) { 873 uids.add(s.getString()); 874 } 875 } 876 } 877 return uids.toArray(Utility.EMPTY_STRINGS); 878 } finally { 879 destroyResponses(); 880 } 881 } 882 883 @Override 884 public Message getMessage(String uid) throws MessagingException { 885 checkOpen(); 886 887 String[] uids = searchForUids(ImapConstants.UID + " " + uid); 888 for (int i = 0; i < uids.length; i++) { 889 if (uids[i].equals(uid)) { 890 return new ImapMessage(uid, this); 891 } 892 } 893 return null; 894 } 895 896 @Override 897 public Message[] getMessages(int start, int end, MessageRetrievalListener listener) 898 throws MessagingException { 899 if (start < 1 || end < 1 || end < start) { 900 throw new MessagingException(String.format("Invalid range: %d %d", start, end)); 901 } 902 return getMessagesInternal( 903 searchForUids(String.format("%d:%d NOT DELETED", start, end)), listener); 904 } 905 906 @Override 907 public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { 908 return getMessages(null, listener); 909 } 910 911 @Override 912 public Message[] getMessages(String[] uids, MessageRetrievalListener listener) 913 throws MessagingException { 914 if (uids == null) { 915 uids = searchForUids("1:* NOT DELETED"); 916 } 917 return getMessagesInternal(uids, listener); 918 } 919 920 public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) { 921 final ArrayList<Message> messages = new ArrayList<Message>(uids.length); 922 for (int i = 0; i < uids.length; i++) { 923 final String uid = uids[i]; 924 final ImapMessage message = new ImapMessage(uid, this); 925 messages.add(message); 926 if (listener != null) { 927 listener.messageRetrieved(message); 928 } 929 } 930 return messages.toArray(Message.EMPTY_ARRAY); 931 } 932 933 @Override 934 public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) 935 throws MessagingException { 936 try { 937 fetchInternal(messages, fp, listener); 938 } catch (RuntimeException e) { // Probably a parser error. 939 Log.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage()); 940 if (mConnection != null) { 941 mConnection.logLastDiscourse(); 942 } 943 throw e; 944 } 945 } 946 947 public void fetchInternal(Message[] messages, FetchProfile fp, 948 MessageRetrievalListener listener) throws MessagingException { 949 if (messages.length == 0) { 950 return; 951 } 952 checkOpen(); 953 HashMap<String, Message> messageMap = new HashMap<String, Message>(); 954 for (Message m : messages) { 955 messageMap.put(m.getUid(), m); 956 } 957 958 /* 959 * Figure out what command we are going to run: 960 * FLAGS - UID FETCH (FLAGS) 961 * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[ 962 * HEADER.FIELDS (date subject from content-type to cc)]) 963 * STRUCTURE - UID FETCH (BODYSTRUCTURE) 964 * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned 965 * BODY - UID FETCH (BODY.PEEK[]) 966 * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID 967 */ 968 969 final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>(); 970 971 fetchFields.add(ImapConstants.UID); 972 if (fp.contains(FetchProfile.Item.FLAGS)) { 973 fetchFields.add(ImapConstants.FLAGS); 974 } 975 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 976 fetchFields.add(ImapConstants.INTERNALDATE); 977 fetchFields.add(ImapConstants.RFC822_SIZE); 978 fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS); 979 } 980 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 981 fetchFields.add(ImapConstants.BODYSTRUCTURE); 982 } 983 984 if (fp.contains(FetchProfile.Item.BODY_SANE)) { 985 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE); 986 } 987 if (fp.contains(FetchProfile.Item.BODY)) { 988 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK); 989 } 990 991 final Part fetchPart = fp.getFirstPart(); 992 if (fetchPart != null) { 993 String[] partIds = 994 fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); 995 if (partIds != null) { 996 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE 997 + "[" + partIds[0] + "]"); 998 } 999 } 1000 1001 try { 1002 mConnection.sendCommand(String.format( 1003 ImapConstants.UID_FETCH + " %s (%s)", joinMessageUids(messages), 1004 Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') 1005 ), false); 1006 ImapResponse response; 1007 int messageNumber = 0; 1008 do { 1009 response = null; 1010 try { 1011 response = mConnection.readResponse(); 1012 1013 if (!response.isDataResponse(1, ImapConstants.FETCH)) { 1014 continue; // Ignore 1015 } 1016 final ImapList fetchList = response.getListOrEmpty(2); 1017 final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID) 1018 .getString(); 1019 if (TextUtils.isEmpty(uid)) continue; 1020 1021 ImapMessage message = (ImapMessage) messageMap.get(uid); 1022 if (message == null) continue; 1023 1024 if (fp.contains(FetchProfile.Item.FLAGS)) { 1025 final ImapList flags = 1026 fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS); 1027 for (int i = 0, count = flags.size(); i < count; i++) { 1028 final ImapString flag = flags.getStringOrEmpty(i); 1029 if (flag.is(ImapConstants.FLAG_DELETED)) { 1030 message.setFlagInternal(Flag.DELETED, true); 1031 } else if (flag.is(ImapConstants.FLAG_ANSWERED)) { 1032 message.setFlagInternal(Flag.ANSWERED, true); 1033 } else if (flag.is(ImapConstants.FLAG_SEEN)) { 1034 message.setFlagInternal(Flag.SEEN, true); 1035 } else if (flag.is(ImapConstants.FLAG_FLAGGED)) { 1036 message.setFlagInternal(Flag.FLAGGED, true); 1037 } 1038 } 1039 } 1040 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 1041 final Date internalDate = fetchList.getKeyedStringOrEmpty( 1042 ImapConstants.INTERNALDATE).getDateOrNull(); 1043 final int size = fetchList.getKeyedStringOrEmpty( 1044 ImapConstants.RFC822_SIZE).getNumberOrZero(); 1045 final String header = fetchList.getKeyedStringOrEmpty( 1046 ImapConstants.BODY_BRACKET_HEADER, true).getString(); 1047 1048 message.setInternalDate(internalDate); 1049 message.setSize(size); 1050 message.parse(Utility.streamFromAsciiString(header)); 1051 } 1052 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 1053 ImapList bs = fetchList.getKeyedListOrEmpty( 1054 ImapConstants.BODYSTRUCTURE); 1055 if (!bs.isEmpty()) { 1056 try { 1057 parseBodyStructure(bs, message, ImapConstants.TEXT); 1058 } catch (MessagingException e) { 1059 if (Email.LOGD) { 1060 Log.v(Logging.LOG_TAG, "Error handling message", e); 1061 } 1062 message.setBody(null); 1063 } 1064 } 1065 } 1066 if (fp.contains(FetchProfile.Item.BODY) 1067 || fp.contains(FetchProfile.Item.BODY_SANE)) { 1068 // Body is keyed by "BODY[...". 1069 // TOOD Should we accept "RFC822" as well?? 1070 // The old code didn't really check the key, so it accepted any literal 1071 // that first appeared. 1072 ImapString body = fetchList.getKeyedStringOrEmpty("BODY[", true); 1073 InputStream bodyStream = body.getAsStream(); 1074 message.parse(bodyStream); 1075 } 1076 if (fetchPart != null && fetchPart.getSize() > 0) { 1077 InputStream bodyStream = 1078 fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream(); 1079 String contentType = fetchPart.getContentType(); 1080 String contentTransferEncoding = fetchPart.getHeader( 1081 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; 1082 1083 // TODO Don't create 2 temp files. 1084 // decodeBody creates BinaryTempFileBody, but we could avoid this 1085 // if we implement ImapStringBody. 1086 // (We'll need to share a temp file. Protect it with a ref-count.) 1087 fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding, 1088 fetchPart.getSize(), listener)); 1089 } 1090 1091 if (listener != null) { 1092 listener.messageRetrieved(message); 1093 } 1094 } finally { 1095 destroyResponses(); 1096 } 1097 } while (!response.isTagged()); 1098 } catch (IOException ioe) { 1099 throw ioExceptionHandler(mConnection, ioe); 1100 } 1101 } 1102 1103 /** 1104 * Removes any content transfer encoding from the stream and returns a Body. 1105 * This code is taken/condensed from MimeUtility.decodeBody 1106 */ 1107 private Body decodeBody(InputStream in, String contentTransferEncoding, int size, 1108 MessageRetrievalListener listener) throws IOException { 1109 // Get a properly wrapped input stream 1110 in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding); 1111 BinaryTempFileBody tempBody = new BinaryTempFileBody(); 1112 OutputStream out = tempBody.getOutputStream(); 1113 try { 1114 byte[] buffer = new byte[COPY_BUFFER_SIZE]; 1115 int n = 0; 1116 int count = 0; 1117 while (-1 != (n = in.read(buffer))) { 1118 out.write(buffer, 0, n); 1119 count += n; 1120 if (listener != null) { 1121 listener.loadAttachmentProgress(count * 100 / size); 1122 } 1123 } 1124 } catch (Base64DataException bde) { 1125 String warning = "\n\n" + Email.getMessageDecodeErrorString(); 1126 out.write(warning.getBytes()); 1127 } finally { 1128 out.close(); 1129 } 1130 return tempBody; 1131 } 1132 1133 @Override 1134 public Flag[] getPermanentFlags() { 1135 return PERMANENT_FLAGS; 1136 } 1137 1138 /** 1139 * Handle any untagged responses that the caller doesn't care to handle themselves. 1140 * @param responses 1141 */ 1142 private void handleUntaggedResponses(List<ImapResponse> responses) { 1143 for (ImapResponse response : responses) { 1144 handleUntaggedResponse(response); 1145 } 1146 } 1147 1148 /** 1149 * Handle an untagged response that the caller doesn't care to handle themselves. 1150 * @param response 1151 */ 1152 private void handleUntaggedResponse(ImapResponse response) { 1153 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 1154 mMessageCount = response.getStringOrEmpty(0).getNumberOrZero(); 1155 } 1156 } 1157 1158 private static void parseBodyStructure(ImapList bs, Part part, String id) 1159 throws MessagingException { 1160 if (bs.getElementOrNone(0).isList()) { 1161 /* 1162 * This is a multipart/* 1163 */ 1164 MimeMultipart mp = new MimeMultipart(); 1165 for (int i = 0, count = bs.size(); i < count; i++) { 1166 ImapElement e = bs.getElementOrNone(i); 1167 if (e.isList()) { 1168 /* 1169 * For each part in the message we're going to add a new BodyPart and parse 1170 * into it. 1171 */ 1172 MimeBodyPart bp = new MimeBodyPart(); 1173 if (id.equals(ImapConstants.TEXT)) { 1174 parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1)); 1175 1176 } else { 1177 parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1)); 1178 } 1179 mp.addBodyPart(bp); 1180 1181 } else { 1182 if (e.isString()) { 1183 mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase()); 1184 } 1185 break; // Ignore the rest of the list. 1186 } 1187 } 1188 part.setBody(mp); 1189 } else { 1190 /* 1191 * This is a body. We need to add as much information as we can find out about 1192 * it to the Part. 1193 */ 1194 1195 /* 1196 body type 1197 body subtype 1198 body parameter parenthesized list 1199 body id 1200 body description 1201 body encoding 1202 body size 1203 */ 1204 1205 final ImapString type = bs.getStringOrEmpty(0); 1206 final ImapString subType = bs.getStringOrEmpty(1); 1207 final String mimeType = 1208 (type.getString() + "/" + subType.getString()).toLowerCase(); 1209 1210 final ImapList bodyParams = bs.getListOrEmpty(2); 1211 final ImapString cid = bs.getStringOrEmpty(3); 1212 final ImapString encoding = bs.getStringOrEmpty(5); 1213 final int size = bs.getStringOrEmpty(6).getNumberOrZero(); 1214 1215 if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) { 1216 // A body type of type MESSAGE and subtype RFC822 1217 // contains, immediately after the basic fields, the 1218 // envelope structure, body structure, and size in 1219 // text lines of the encapsulated message. 1220 // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL, 1221 // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL] 1222 /* 1223 * This will be caught by fetch and handled appropriately. 1224 */ 1225 throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 1226 + " not yet supported."); 1227 } 1228 1229 /* 1230 * Set the content type with as much information as we know right now. 1231 */ 1232 final StringBuilder contentType = new StringBuilder(mimeType); 1233 1234 /* 1235 * If there are body params we might be able to get some more information out 1236 * of them. 1237 */ 1238 for (int i = 1, count = bodyParams.size(); i < count; i += 2) { 1239 1240 // TODO We need to convert " into %22, but 1241 // because MimeUtility.getHeaderParameter doesn't recognize it, 1242 // we can't fix it for now. 1243 contentType.append(String.format(";\n %s=\"%s\"", 1244 bodyParams.getStringOrEmpty(i - 1).getString(), 1245 bodyParams.getStringOrEmpty(i).getString())); 1246 } 1247 1248 part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString()); 1249 1250 // Extension items 1251 final ImapList bodyDisposition; 1252 1253 if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) { 1254 // If media-type is TEXT, 9th element might be: [body-fld-lines] := number 1255 // So, if it's not a list, use 10th element. 1256 // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.) 1257 bodyDisposition = bs.getListOrEmpty(9); 1258 } else { 1259 bodyDisposition = bs.getListOrEmpty(8); 1260 } 1261 1262 final StringBuilder contentDisposition = new StringBuilder(); 1263 1264 if (bodyDisposition.size() > 0) { 1265 final String bodyDisposition0Str = 1266 bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(); 1267 if (!TextUtils.isEmpty(bodyDisposition0Str)) { 1268 contentDisposition.append(bodyDisposition0Str); 1269 } 1270 1271 final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1); 1272 if (!bodyDispositionParams.isEmpty()) { 1273 /* 1274 * If there is body disposition information we can pull some more 1275 * information about the attachment out. 1276 */ 1277 for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) { 1278 1279 // TODO We need to convert " into %22. See above. 1280 contentDisposition.append(String.format(";\n %s=\"%s\"", 1281 bodyDispositionParams.getStringOrEmpty(i - 1) 1282 .getString().toLowerCase(), 1283 bodyDispositionParams.getStringOrEmpty(i).getString())); 1284 } 1285 } 1286 } 1287 1288 if ((size > 0) 1289 && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") 1290 == null)) { 1291 contentDisposition.append(String.format(";\n size=%d", size)); 1292 } 1293 1294 if (contentDisposition.length() > 0) { 1295 /* 1296 * Set the content disposition containing at least the size. Attachment 1297 * handling code will use this down the road. 1298 */ 1299 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, 1300 contentDisposition.toString()); 1301 } 1302 1303 /* 1304 * Set the Content-Transfer-Encoding header. Attachment code will use this 1305 * to parse the body. 1306 */ 1307 if (!encoding.isEmpty()) { 1308 part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, 1309 encoding.getString()); 1310 } 1311 1312 /* 1313 * Set the Content-ID header. 1314 */ 1315 if (!cid.isEmpty()) { 1316 part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString()); 1317 } 1318 1319 if (size > 0) { 1320 if (part instanceof ImapMessage) { 1321 ((ImapMessage) part).setSize(size); 1322 } else if (part instanceof MimeBodyPart) { 1323 ((MimeBodyPart) part).setSize(size); 1324 } else { 1325 throw new MessagingException("Unknown part type " + part.toString()); 1326 } 1327 } 1328 part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); 1329 } 1330 1331 } 1332 1333 /** 1334 * Appends the given messages to the selected folder. This implementation also determines 1335 * the new UID of the given message on the IMAP server and sets the Message's UID to the 1336 * new server UID. 1337 */ 1338 @Override 1339 public void appendMessages(Message[] messages) throws MessagingException { 1340 checkOpen(); 1341 try { 1342 for (Message message : messages) { 1343 // Create output count 1344 CountingOutputStream out = new CountingOutputStream(); 1345 EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); 1346 message.writeTo(eolOut); 1347 eolOut.flush(); 1348 // Create flag list (most often this will be "\SEEN") 1349 String flagList = ""; 1350 Flag[] flags = message.getFlags(); 1351 if (flags.length > 0) { 1352 StringBuilder sb = new StringBuilder(); 1353 for (int i = 0, count = flags.length; i < count; i++) { 1354 Flag flag = flags[i]; 1355 if (flag == Flag.SEEN) { 1356 sb.append(" " + ImapConstants.FLAG_SEEN); 1357 } else if (flag == Flag.FLAGGED) { 1358 sb.append(" " + ImapConstants.FLAG_FLAGGED); 1359 } 1360 } 1361 if (sb.length() > 0) { 1362 flagList = sb.substring(1); 1363 } 1364 } 1365 1366 mConnection.sendCommand( 1367 String.format(ImapConstants.APPEND + " \"%s\" (%s) {%d}", 1368 encodeFolderName(mName, mStore.mPathPrefix), 1369 flagList, 1370 out.getCount()), false); 1371 ImapResponse response; 1372 do { 1373 response = mConnection.readResponse(); 1374 if (response.isContinuationRequest()) { 1375 eolOut = new EOLConvertingOutputStream( 1376 mConnection.mTransport.getOutputStream()); 1377 message.writeTo(eolOut); 1378 eolOut.write('\r'); 1379 eolOut.write('\n'); 1380 eolOut.flush(); 1381 } else if (!response.isTagged()) { 1382 handleUntaggedResponse(response); 1383 } 1384 } while (!response.isTagged()); 1385 1386 // TODO Why not check the response? 1387 1388 /* 1389 * Try to recover the UID of the message from an APPENDUID response. 1390 * e.g. 11 OK [APPENDUID 2 238268] APPEND completed 1391 */ 1392 final ImapList appendList = response.getListOrEmpty(1); 1393 if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) { 1394 String serverUid = appendList.getStringOrEmpty(2).getString(); 1395 if (!TextUtils.isEmpty(serverUid)) { 1396 message.setUid(serverUid); 1397 continue; 1398 } 1399 } 1400 1401 /* 1402 * Try to find the UID of the message we just appended using the 1403 * Message-ID header. If there are more than one response, take the 1404 * last one, as it's most likely the newest (the one we just uploaded). 1405 */ 1406 String messageId = message.getMessageId(); 1407 if (messageId == null || messageId.length() == 0) { 1408 continue; 1409 } 1410 String[] uids = searchForUids( 1411 String.format("(HEADER MESSAGE-ID %s)", messageId)); 1412 if (uids.length > 0) { 1413 message.setUid(uids[0]); 1414 } 1415 } 1416 } catch (IOException ioe) { 1417 throw ioExceptionHandler(mConnection, ioe); 1418 } finally { 1419 destroyResponses(); 1420 } 1421 } 1422 1423 @Override 1424 public Message[] expunge() throws MessagingException { 1425 checkOpen(); 1426 try { 1427 handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE)); 1428 } catch (IOException ioe) { 1429 throw ioExceptionHandler(mConnection, ioe); 1430 } finally { 1431 destroyResponses(); 1432 } 1433 return null; 1434 } 1435 1436 @Override 1437 public void setFlags(Message[] messages, Flag[] flags, boolean value) 1438 throws MessagingException { 1439 checkOpen(); 1440 1441 String allFlags = ""; 1442 if (flags.length > 0) { 1443 StringBuilder flagList = new StringBuilder(); 1444 for (int i = 0, count = flags.length; i < count; i++) { 1445 Flag flag = flags[i]; 1446 if (flag == Flag.SEEN) { 1447 flagList.append(" " + ImapConstants.FLAG_SEEN); 1448 } else if (flag == Flag.DELETED) { 1449 flagList.append(" " + ImapConstants.FLAG_DELETED); 1450 } else if (flag == Flag.FLAGGED) { 1451 flagList.append(" " + ImapConstants.FLAG_FLAGGED); 1452 } 1453 } 1454 allFlags = flagList.substring(1); 1455 } 1456 try { 1457 mConnection.executeSimpleCommand(String.format( 1458 ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)", 1459 joinMessageUids(messages), 1460 value ? "+" : "-", 1461 allFlags)); 1462 1463 } catch (IOException ioe) { 1464 throw ioExceptionHandler(mConnection, ioe); 1465 } finally { 1466 destroyResponses(); 1467 } 1468 } 1469 1470 /** 1471 * Selects the folder for use. Before performing any operations on this folder, it 1472 * must be selected. 1473 */ 1474 private void doSelect() throws IOException, MessagingException { 1475 List<ImapResponse> responses = mConnection.executeSimpleCommand( 1476 String.format(ImapConstants.SELECT + " \"%s\"", 1477 encodeFolderName(mName, mStore.mPathPrefix))); 1478 1479 // Assume the folder is opened read-write; unless we are notified otherwise 1480 mMode = OpenMode.READ_WRITE; 1481 int messageCount = -1; 1482 for (ImapResponse response : responses) { 1483 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 1484 messageCount = response.getStringOrEmpty(0).getNumberOrZero(); 1485 } else if (response.isOk()) { 1486 final ImapString responseCode = response.getResponseCodeOrEmpty(); 1487 if (responseCode.is(ImapConstants.READ_ONLY)) { 1488 mMode = OpenMode.READ_ONLY; 1489 } else if (responseCode.is(ImapConstants.READ_WRITE)) { 1490 mMode = OpenMode.READ_WRITE; 1491 } 1492 } else if (response.isTagged()) { // Not OK 1493 throw new MessagingException("Can't open mailbox: " 1494 + response.getStatusResponseTextOrEmpty()); 1495 } 1496 } 1497 if (messageCount == -1) { 1498 throw new MessagingException("Did not find message count during select"); 1499 } 1500 mMessageCount = messageCount; 1501 mExists = true; 1502 } 1503 1504 private void checkOpen() throws MessagingException { 1505 if (!isOpen()) { 1506 throw new MessagingException("Folder " + mName + " is not open."); 1507 } 1508 } 1509 1510 private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) { 1511 if (Email.DEBUG) { 1512 Log.d(Logging.LOG_TAG, "IO Exception detected: ", ioe); 1513 } 1514 connection.destroyResponses(); 1515 connection.close(); 1516 if (connection == mConnection) { 1517 mConnection = null; // To prevent close() from returning the connection to the pool. 1518 close(false); 1519 } 1520 return new MessagingException("IO Error", ioe); 1521 } 1522 1523 @Override 1524 public boolean equals(Object o) { 1525 if (o instanceof ImapFolder) { 1526 return ((ImapFolder)o).mName.equals(mName); 1527 } 1528 return super.equals(o); 1529 } 1530 1531 @Override 1532 public Message createMessage(String uid) { 1533 return new ImapMessage(uid, this); 1534 } 1535 } 1536 1537 /** 1538 * A cacheable class that stores the details for a single IMAP connection. 1539 */ 1540 class ImapConnection { 1541 /** ID capability per RFC 2971*/ 1542 public static final int CAPABILITY_ID = 1 << 0; 1543 /** NAMESPACE capability per RFC 2342 */ 1544 public static final int CAPABILITY_NAMESPACE = 1 << 1; 1545 /** STARTTLS capability per RFC 3501 */ 1546 public static final int CAPABILITY_STARTTLS = 1 << 2; 1547 /** UIDPLUS capability per RFC 4315 */ 1548 public static final int CAPABILITY_UIDPLUS = 1 << 3; 1549 1550 /** The capabilities supported; a set of CAPABILITY_* values. */ 1551 private int mCapabilities; 1552 private static final String IMAP_DEDACTED_LOG = "[IMAP command redacted]"; 1553 private Transport mTransport; 1554 private ImapResponseParser mParser; 1555 /** # of command/response lines to log upon crash. */ 1556 private static final int DISCOURSE_LOGGER_SIZE = 64; 1557 private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE); 1558 1559 public void open() throws IOException, MessagingException { 1560 if (mTransport != null && mTransport.isOpen()) { 1561 return; 1562 } 1563 1564 try { 1565 // copy configuration into a clean transport, if necessary 1566 if (mTransport == null) { 1567 mTransport = mRootTransport.newInstanceWithConfiguration(); 1568 } 1569 1570 mTransport.open(); 1571 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 1572 1573 createParser(); 1574 1575 // BANNER 1576 mParser.readResponse(); 1577 1578 // CAPABILITY 1579 ImapResponse capabilities = queryCapabilities(); 1580 1581 boolean hasStartTlsCapability = 1582 capabilities.contains(ImapConstants.STARTTLS); 1583 1584 // TLS 1585 ImapResponse newCapabilities = doStartTls(hasStartTlsCapability); 1586 if (newCapabilities != null) { 1587 capabilities = newCapabilities; 1588 } 1589 1590 // NOTE: An IMAP response MUST be processed before issuing any new IMAP 1591 // requests. Subsequent requests may destroy previous response data. As 1592 // such, we save away capability information here for future use. 1593 setCapabilities(capabilities); 1594 String capabilityString = capabilities.flatten(); 1595 1596 // ID 1597 doSendId(isCapable(CAPABILITY_ID), capabilityString); 1598 1599 // LOGIN 1600 doLogin(); 1601 1602 // NAMESPACE (only valid in the Authenticated state) 1603 doGetNamespace(isCapable(CAPABILITY_NAMESPACE)); 1604 1605 // Gets the path separator from the server 1606 doGetPathSeparator(); 1607 1608 ensurePrefixIsValid(); 1609 } catch (SSLException e) { 1610 if (Email.DEBUG) { 1611 Log.d(Logging.LOG_TAG, e.toString()); 1612 } 1613 throw new CertificateValidationException(e.getMessage(), e); 1614 } catch (IOException ioe) { 1615 // NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot 1616 // of other code here that catches IOException and I don't want to break it. 1617 // This catch is only here to enhance logging of connection-time issues. 1618 if (Email.DEBUG) { 1619 Log.d(Logging.LOG_TAG, ioe.toString()); 1620 } 1621 throw ioe; 1622 } finally { 1623 destroyResponses(); 1624 } 1625 } 1626 1627 public void close() { 1628 if (mTransport != null) { 1629 mTransport.close(); 1630 mTransport = null; 1631 } 1632 } 1633 1634 /** 1635 * Returns whether or not the specified capability is supported by the server. 1636 */ 1637 public boolean isCapable(int capability) { 1638 return (mCapabilities & capability) != 0; 1639 } 1640 1641 /** 1642 * Sets the capability flags according to the response provided by the server. 1643 * Note: We only set the capability flags that we are interested in. There are many IMAP 1644 * capabilities that we do not track. 1645 */ 1646 private void setCapabilities(ImapResponse capabilities) { 1647 if (capabilities.contains(ImapConstants.ID)) { 1648 mCapabilities |= CAPABILITY_ID; 1649 } 1650 if (capabilities.contains(ImapConstants.NAMESPACE)) { 1651 mCapabilities |= CAPABILITY_NAMESPACE; 1652 } 1653 if (capabilities.contains(ImapConstants.UIDPLUS)) { 1654 mCapabilities |= CAPABILITY_UIDPLUS; 1655 } 1656 if (capabilities.contains(ImapConstants.STARTTLS)) { 1657 mCapabilities |= CAPABILITY_STARTTLS; 1658 } 1659 } 1660 1661 /** 1662 * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and 1663 * set it to {@link #mParser}. 1664 * 1665 * If we already have an {@link ImapResponseParser}, we 1666 * {@link #destroyResponses()} and throw it away. 1667 */ 1668 private void createParser() { 1669 destroyResponses(); 1670 mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse); 1671 } 1672 1673 public void destroyResponses() { 1674 if (mParser != null) { 1675 mParser.destroyResponses(); 1676 } 1677 } 1678 1679 /* package */ boolean isTransportOpenForTest() { 1680 return mTransport != null ? mTransport.isOpen() : false; 1681 } 1682 1683 public ImapResponse readResponse() throws IOException, MessagingException { 1684 return mParser.readResponse(); 1685 } 1686 1687 /** 1688 * Send a single command to the server. The command will be preceded by an IMAP command 1689 * tag and followed by \r\n (caller need not supply them). 1690 * 1691 * @param command The command to send to the server 1692 * @param sensitive If true, the command will not be logged 1693 * @return Returns the command tag that was sent 1694 */ 1695 public String sendCommand(String command, boolean sensitive) 1696 throws MessagingException, IOException { 1697 open(); 1698 String tag = Integer.toString(mNextCommandTag.incrementAndGet()); 1699 String commandToSend = tag + " " + command; 1700 mTransport.writeLine(commandToSend, sensitive ? IMAP_DEDACTED_LOG : null); 1701 mDiscourse.addSentCommand(sensitive ? IMAP_DEDACTED_LOG : commandToSend); 1702 return tag; 1703 } 1704 1705 /*package*/ List<ImapResponse> executeSimpleCommand(String command) throws IOException, 1706 MessagingException { 1707 return executeSimpleCommand(command, false); 1708 } 1709 1710 /*package*/ List<ImapResponse> executeSimpleCommand(String command, boolean sensitive) 1711 throws IOException, MessagingException { 1712 String tag = sendCommand(command, sensitive); 1713 ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>(); 1714 ImapResponse response; 1715 do { 1716 response = mParser.readResponse(); 1717 responses.add(response); 1718 } while (!response.isTagged()); 1719 if (!response.isOk()) { 1720 final String toString = response.toString(); 1721 final String alert = response.getAlertTextOrEmpty().getString(); 1722 destroyResponses(); 1723 throw new ImapException(toString, alert); 1724 } 1725 return responses; 1726 } 1727 1728 /** 1729 * Query server for capabilities. 1730 */ 1731 private ImapResponse queryCapabilities() throws IOException, MessagingException { 1732 ImapResponse capabilityResponse = null; 1733 for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) { 1734 if (r.is(0, ImapConstants.CAPABILITY)) { 1735 capabilityResponse = r; 1736 break; 1737 } 1738 } 1739 if (capabilityResponse == null) { 1740 throw new MessagingException("Invalid CAPABILITY response received"); 1741 } 1742 return capabilityResponse; 1743 } 1744 1745 /** 1746 * Sends client identification information to the IMAP server per RFC 2971. If 1747 * the server does not support the ID command, this will perform no operation. 1748 * 1749 * Interoperability hack: Never send ID to *.secureserver.net, which sends back a 1750 * malformed response that our parser can't deal with. 1751 */ 1752 private void doSendId(boolean hasIdCapability, String capabilities) 1753 throws MessagingException { 1754 if (!hasIdCapability) return; 1755 1756 // Never send ID to *.secureserver.net 1757 String host = mRootTransport.getHost(); 1758 if (host.toLowerCase().endsWith(".secureserver.net")) return; 1759 1760 // Assign user-agent string (for RFC2971 ID command) 1761 String mUserAgent = getImapId(mContext, mUsername, host, capabilities); 1762 1763 if (mUserAgent != null) { 1764 mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")"; 1765 } else if (DEBUG_FORCE_SEND_ID) { 1766 mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL; 1767 } 1768 // else: mIdPhrase = null, no ID will be emitted 1769 1770 // Send user-agent in an RFC2971 ID command 1771 if (mIdPhrase != null) { 1772 try { 1773 executeSimpleCommand(mIdPhrase); 1774 } catch (ImapException ie) { 1775 // Log for debugging, but this is not a fatal problem. 1776 if (Email.DEBUG) { 1777 Log.d(Logging.LOG_TAG, ie.toString()); 1778 } 1779 } catch (IOException ioe) { 1780 // Special case to handle malformed OK responses and ignore them. 1781 // A true IOException will recur on the following login steps 1782 // This can go away after the parser is fixed - see bug 2138981 1783 } 1784 } 1785 } 1786 1787 /** 1788 * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user 1789 * explicitly sets a namespace (using setup UI) or if the server does not support the 1790 * namespace command, this will perform no operation. 1791 */ 1792 private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException { 1793 // user did not specify a hard-coded prefix; try to get it from the server 1794 if (hasNamespaceCapability && TextUtils.isEmpty(mPathPrefix)) { 1795 List<ImapResponse> responseList = Collections.emptyList(); 1796 1797 try { 1798 responseList = executeSimpleCommand(ImapConstants.NAMESPACE); 1799 } catch (ImapException ie) { 1800 // Log for debugging, but this is not a fatal problem. 1801 if (Email.DEBUG) { 1802 Log.d(Logging.LOG_TAG, ie.toString()); 1803 } 1804 } catch (IOException ioe) { 1805 // Special case to handle malformed OK responses and ignore them. 1806 } 1807 1808 for (ImapResponse response: responseList) { 1809 if (response.isDataResponse(0, ImapConstants.NAMESPACE)) { 1810 ImapList namespaceList = response.getListOrEmpty(1); 1811 ImapList namespace = namespaceList.getListOrEmpty(0); 1812 String namespaceString = namespace.getStringOrEmpty(0).getString(); 1813 if (!TextUtils.isEmpty(namespaceString)) { 1814 mPathPrefix = decodeFolderName(namespaceString, null); 1815 mPathSeparator = namespace.getStringOrEmpty(1).getString(); 1816 } 1817 } 1818 } 1819 } 1820 } 1821 1822 /** 1823 * Logs into the IMAP server 1824 */ 1825 private void doLogin() 1826 throws IOException, MessagingException, AuthenticationFailedException { 1827 try { 1828 // TODO eventually we need to add additional authentication 1829 // options such as SASL 1830 executeSimpleCommand(mLoginPhrase, true); 1831 } catch (ImapException ie) { 1832 if (Email.DEBUG) { 1833 Log.d(Logging.LOG_TAG, ie.toString()); 1834 } 1835 throw new AuthenticationFailedException(ie.getAlertText(), ie); 1836 1837 } catch (MessagingException me) { 1838 throw new AuthenticationFailedException(null, me); 1839 } 1840 } 1841 1842 /** 1843 * Gets the path separator per the LIST command in RFC 3501. If the path separator 1844 * was obtained while obtaining the namespace or there is no prefix defined, this 1845 * will perform no operation. 1846 */ 1847 private void doGetPathSeparator() throws MessagingException { 1848 // user did not specify a hard-coded prefix; try to get it from the server 1849 if (TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix)) { 1850 List<ImapResponse> responseList = Collections.emptyList(); 1851 1852 try { 1853 responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\""); 1854 } catch (ImapException ie) { 1855 // Log for debugging, but this is not a fatal problem. 1856 if (Email.DEBUG) { 1857 Log.d(Logging.LOG_TAG, ie.toString()); 1858 } 1859 } catch (IOException ioe) { 1860 // Special case to handle malformed OK responses and ignore them. 1861 } 1862 1863 for (ImapResponse response: responseList) { 1864 if (response.isDataResponse(0, ImapConstants.LIST)) { 1865 mPathSeparator = response.getStringOrEmpty(2).getString(); 1866 } 1867 } 1868 } 1869 } 1870 1871 /** 1872 * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted 1873 * to use TLS or the server does not support the TLS capability, this will perform 1874 * no operation. 1875 */ 1876 private ImapResponse doStartTls(boolean hasStartTlsCapability) 1877 throws IOException, MessagingException { 1878 if (mTransport.canTryTlsSecurity()) { 1879 if (hasStartTlsCapability) { 1880 // STARTTLS 1881 executeSimpleCommand(ImapConstants.STARTTLS); 1882 1883 mTransport.reopenTls(); 1884 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 1885 createParser(); 1886 // Per RFC requirement (3501-6.2.1) gather new capabilities 1887 return(queryCapabilities()); 1888 } else { 1889 if (Email.DEBUG) { 1890 Log.d(Logging.LOG_TAG, "TLS not supported but required"); 1891 } 1892 throw new MessagingException(MessagingException.TLS_REQUIRED); 1893 } 1894 } 1895 return null; 1896 } 1897 1898 /** @see DiscourseLogger#logLastDiscourse() */ 1899 public void logLastDiscourse() { 1900 mDiscourse.logLastDiscourse(); 1901 } 1902 } 1903 1904 static class ImapMessage extends MimeMessage { 1905 ImapMessage(String uid, Folder folder) { 1906 this.mUid = uid; 1907 this.mFolder = folder; 1908 } 1909 1910 public void setSize(int size) { 1911 this.mSize = size; 1912 } 1913 1914 @Override 1915 public void parse(InputStream in) throws IOException, MessagingException { 1916 super.parse(in); 1917 } 1918 1919 public void setFlagInternal(Flag flag, boolean set) throws MessagingException { 1920 super.setFlag(flag, set); 1921 } 1922 1923 @Override 1924 public void setFlag(Flag flag, boolean set) throws MessagingException { 1925 super.setFlag(flag, set); 1926 mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); 1927 } 1928 } 1929 1930 static class ImapException extends MessagingException { 1931 private static final long serialVersionUID = 1L; 1932 1933 String mAlertText; 1934 1935 public ImapException(String message, String alertText, Throwable throwable) { 1936 super(message, throwable); 1937 this.mAlertText = alertText; 1938 } 1939 1940 public ImapException(String message, String alertText) { 1941 super(message); 1942 this.mAlertText = alertText; 1943 } 1944 1945 public String getAlertText() { 1946 return mAlertText; 1947 } 1948 1949 public void setAlertText(String alertText) { 1950 mAlertText = alertText; 1951 } 1952 } 1953} 1954