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