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