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