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