ImapStore.java revision 8978aac1977408b05e386ae846c30920c7faa0a6
1 2package com.android.email.mail.store; 3 4import java.io.BufferedInputStream; 5import java.io.BufferedOutputStream; 6import java.io.IOException; 7import java.io.InputStream; 8import java.io.OutputStream; 9import java.io.UnsupportedEncodingException; 10import java.net.InetSocketAddress; 11import java.net.Socket; 12import java.net.SocketAddress; 13import java.net.URI; 14import java.net.URISyntaxException; 15import java.nio.ByteBuffer; 16import java.nio.CharBuffer; 17import java.nio.charset.Charset; 18import java.security.GeneralSecurityException; 19import java.security.SecureRandom; 20import java.util.ArrayList; 21import java.util.Date; 22import java.util.HashMap; 23import java.util.LinkedHashSet; 24import java.util.LinkedList; 25import java.util.List; 26 27import javax.net.ssl.SSLContext; 28import javax.net.ssl.TrustManager; 29import javax.net.ssl.SSLException; 30 31import android.util.Config; 32import android.util.Log; 33 34import com.android.email.Email; 35import com.android.email.PeekableInputStream; 36import com.android.email.Utility; 37import com.android.email.mail.AuthenticationFailedException; 38import com.android.email.mail.FetchProfile; 39import com.android.email.mail.Flag; 40import com.android.email.mail.Folder; 41import com.android.email.mail.Message; 42import com.android.email.mail.MessageRetrievalListener; 43import com.android.email.mail.MessagingException; 44import com.android.email.mail.Part; 45import com.android.email.mail.Store; 46import com.android.email.mail.CertificateValidationException; 47import com.android.email.mail.internet.MimeBodyPart; 48import com.android.email.mail.internet.MimeHeader; 49import com.android.email.mail.internet.MimeMessage; 50import com.android.email.mail.internet.MimeMultipart; 51import com.android.email.mail.internet.MimeUtility; 52import com.android.email.mail.store.ImapResponseParser.ImapList; 53import com.android.email.mail.store.ImapResponseParser.ImapResponse; 54import com.android.email.mail.transport.CountingOutputStream; 55import com.android.email.mail.transport.EOLConvertingOutputStream; 56import com.beetstra.jutf7.CharsetProvider; 57 58/** 59 * <pre> 60 * TODO Need to start keeping track of UIDVALIDITY 61 * TODO Need a default response handler for things like folder updates 62 * TODO In fetch(), if we need a ImapMessage and were given 63 * something else we can try to do a pre-fetch first. 64 * 65 * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for 66 * certain information in a FETCH command, the server may return the requested 67 * information in any order, not necessarily in the order that it was requested. 68 * Further, the server may return the information in separate FETCH responses 69 * and may also return information that was not explicitly requested (to reflect 70 * to the client changes in the state of the subject message). 71 * </pre> 72 */ 73public class ImapStore extends Store { 74 public static final int CONNECTION_SECURITY_NONE = 0; 75 public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; 76 public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; 77 public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; 78 public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; 79 80 private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN }; 81 82 private String mHost; 83 private int mPort; 84 private String mUsername; 85 private String mPassword; 86 private int mConnectionSecurity; 87 private String mPathPrefix; 88 89 private LinkedList<ImapConnection> mConnections = 90 new LinkedList<ImapConnection>(); 91 92 /** 93 * Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. 94 */ 95 private Charset mModifiedUtf7Charset; 96 97 /** 98 * Cache of ImapFolder objects. ImapFolders are attached to a given folder on the server 99 * and as long as their associated connection remains open they are reusable between 100 * requests. This cache lets us make sure we always reuse, if possible, for a given 101 * folder name. 102 */ 103 private HashMap<String, ImapFolder> mFolderCache = new HashMap<String, ImapFolder>(); 104 105 /** 106 * imap://user:password@server:port CONNECTION_SECURITY_NONE 107 * imap+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL 108 * imap+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED 109 * imap+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED 110 * imap+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL 111 * 112 * @param _uri 113 */ 114 public ImapStore(String _uri) throws MessagingException { 115 URI uri; 116 try { 117 uri = new URI(_uri); 118 } catch (URISyntaxException use) { 119 throw new MessagingException("Invalid ImapStore URI", use); 120 } 121 122 String scheme = uri.getScheme(); 123 if (scheme.equals("imap")) { 124 mConnectionSecurity = CONNECTION_SECURITY_NONE; 125 mPort = 143; 126 } else if (scheme.equals("imap+tls")) { 127 mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; 128 mPort = 143; 129 } else if (scheme.equals("imap+tls+")) { 130 mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; 131 mPort = 143; 132 } else if (scheme.equals("imap+ssl+")) { 133 mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; 134 mPort = 993; 135 } else if (scheme.equals("imap+ssl")) { 136 mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; 137 mPort = 993; 138 } else { 139 throw new MessagingException("Unsupported protocol"); 140 } 141 142 mHost = uri.getHost(); 143 144 if (uri.getPort() != -1) { 145 mPort = uri.getPort(); 146 } 147 148 if (uri.getUserInfo() != null) { 149 String[] userInfoParts = uri.getUserInfo().split(":", 2); 150 mUsername = userInfoParts[0]; 151 if (userInfoParts.length > 1) { 152 mPassword = userInfoParts[1]; 153 } 154 } 155 156 if ((uri.getPath() != null) && (uri.getPath().length() > 0)) { 157 mPathPrefix = uri.getPath().substring(1); 158 } 159 160 mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501"); 161 } 162 163 @Override 164 public Folder getFolder(String name) throws MessagingException { 165 ImapFolder folder; 166 synchronized (mFolderCache) { 167 folder = mFolderCache.get(name); 168 if (folder == null) { 169 folder = new ImapFolder(name); 170 mFolderCache.put(name, folder); 171 } 172 } 173 return folder; 174 } 175 176 177 @Override 178 public Folder[] getPersonalNamespaces() throws MessagingException { 179 ImapConnection connection = getConnection(); 180 try { 181 ArrayList<Folder> folders = new ArrayList<Folder>(); 182 List<ImapResponse> responses = 183 connection.executeSimpleCommand(String.format("LIST \"\" \"%s*\"", 184 mPathPrefix == null ? "" : mPathPrefix)); 185 for (ImapResponse response : responses) { 186 if (response.get(0).equals("LIST")) { 187 boolean includeFolder = true; 188 String folder = decodeFolderName(response.getString(3)); 189 if (folder.equalsIgnoreCase("INBOX")) { 190 continue; 191 } 192 ImapList attributes = response.getList(1); 193 for (int i = 0, count = attributes.size(); i < count; i++) { 194 String attribute = attributes.getString(i); 195 if (attribute.equalsIgnoreCase("\\NoSelect")) { 196 includeFolder = false; 197 } 198 } 199 if (includeFolder) { 200 folders.add(getFolder(folder)); 201 } 202 } 203 } 204 folders.add(getFolder("INBOX")); 205 return folders.toArray(new Folder[] {}); 206 } catch (IOException ioe) { 207 connection.close(); 208 throw new MessagingException("Unable to get folder list.", ioe); 209 } finally { 210 releaseConnection(connection); 211 } 212 } 213 214 @Override 215 public void checkSettings() throws MessagingException { 216 try { 217 ImapConnection connection = new ImapConnection(); 218 connection.open(); 219 connection.close(); 220 } 221 catch (IOException ioe) { 222 throw new MessagingException("Unable to connect.", ioe); 223 } 224 } 225 226 /** 227 * Gets a connection if one is available for reuse, or creates a new one if not. 228 * @return 229 */ 230 private ImapConnection getConnection() throws MessagingException { 231 synchronized (mConnections) { 232 ImapConnection connection = null; 233 while ((connection = mConnections.poll()) != null) { 234 try { 235 connection.executeSimpleCommand("NOOP"); 236 break; 237 } 238 catch (IOException ioe) { 239 connection.close(); 240 } 241 } 242 if (connection == null) { 243 connection = new ImapConnection(); 244 } 245 return connection; 246 } 247 } 248 249 private void releaseConnection(ImapConnection connection) { 250 mConnections.offer(connection); 251 } 252 253 private String encodeFolderName(String name) { 254 try { 255 ByteBuffer bb = mModifiedUtf7Charset.encode(name); 256 byte[] b = new byte[bb.limit()]; 257 bb.get(b); 258 return new String(b, "US-ASCII"); 259 } 260 catch (UnsupportedEncodingException uee) { 261 /* 262 * The only thing that can throw this is getBytes("US-ASCII") and if US-ASCII doesn't 263 * exist we're totally screwed. 264 */ 265 throw new RuntimeException("Unabel to encode folder name: " + name, uee); 266 } 267 } 268 269 private String decodeFolderName(String name) { 270 /* 271 * Convert the encoded name to US-ASCII, then pass it through the modified UTF-7 272 * decoder and return the Unicode String. 273 */ 274 try { 275 byte[] encoded = name.getBytes("US-ASCII"); 276 CharBuffer cb = mModifiedUtf7Charset.decode(ByteBuffer.wrap(encoded)); 277 return cb.toString(); 278 } 279 catch (UnsupportedEncodingException uee) { 280 /* 281 * The only thing that can throw this is getBytes("US-ASCII") and if US-ASCII doesn't 282 * exist we're totally screwed. 283 */ 284 throw new RuntimeException("Unable to decode folder name: " + name, uee); 285 } 286 } 287 288 class ImapFolder extends Folder { 289 private String mName; 290 private int mMessageCount = -1; 291 private ImapConnection mConnection; 292 private OpenMode mMode; 293 private boolean mExists; 294 295 public ImapFolder(String name) { 296 this.mName = name; 297 } 298 299 public void open(OpenMode mode) throws MessagingException { 300 if (isOpen() && mMode == mode) { 301 // Make sure the connection is valid. If it's not we'll close it down and continue 302 // on to get a new one. 303 try { 304 mConnection.executeSimpleCommand("NOOP"); 305 return; 306 } 307 catch (IOException ioe) { 308 ioExceptionHandler(mConnection, ioe); 309 } 310 } 311 synchronized (this) { 312 mConnection = getConnection(); 313 } 314 // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk 315 // $MDNSent) 316 // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft 317 // NonJunk $MDNSent \*)] Flags permitted. 318 // * 23 EXISTS 319 // * 0 RECENT 320 // * OK [UIDVALIDITY 1125022061] UIDs valid 321 // * OK [UIDNEXT 57576] Predicted next UID 322 // 2 OK [READ-WRITE] Select completed. 323 try { 324 List<ImapResponse> responses = mConnection.executeSimpleCommand( 325 String.format("SELECT \"%s\"", 326 encodeFolderName(mName))); 327 /* 328 * If the command succeeds we expect the folder has been opened read-write 329 * unless we are notified otherwise in the responses. 330 */ 331 mMode = OpenMode.READ_WRITE; 332 333 for (ImapResponse response : responses) { 334 if (response.mTag == null && response.get(1).equals("EXISTS")) { 335 mMessageCount = response.getNumber(0); 336 } 337 else if (response.mTag != null && response.size() >= 2) { 338 if ("[READ-ONLY]".equalsIgnoreCase(response.getString(1))) { 339 mMode = OpenMode.READ_ONLY; 340 } 341 else if ("[READ-WRITE]".equalsIgnoreCase(response.getString(1))) { 342 mMode = OpenMode.READ_WRITE; 343 } 344 } 345 } 346 347 if (mMessageCount == -1) { 348 throw new MessagingException( 349 "Did not find message count during select"); 350 } 351 mExists = true; 352 353 } catch (IOException ioe) { 354 throw ioExceptionHandler(mConnection, ioe); 355 } 356 } 357 358 public boolean isOpen() { 359 return mConnection != null; 360 } 361 362 @Override 363 public OpenMode getMode() throws MessagingException { 364 return mMode; 365 } 366 367 public void close(boolean expunge) { 368 if (!isOpen()) { 369 return; 370 } 371 // TODO implement expunge 372 mMessageCount = -1; 373 synchronized (this) { 374 releaseConnection(mConnection); 375 mConnection = null; 376 } 377 } 378 379 public String getName() { 380 return mName; 381 } 382 383 public boolean exists() throws MessagingException { 384 if (mExists) { 385 return true; 386 } 387 /* 388 * This method needs to operate in the unselected mode as well as the selected mode 389 * so we must get the connection ourselves if it's not there. We are specifically 390 * not calling checkOpen() since we don't care if the folder is open. 391 */ 392 ImapConnection connection = null; 393 synchronized(this) { 394 if (mConnection == null) { 395 connection = getConnection(); 396 } 397 else { 398 connection = mConnection; 399 } 400 } 401 try { 402 connection.executeSimpleCommand(String.format("STATUS \"%s\" (UIDVALIDITY)", 403 encodeFolderName(mName))); 404 mExists = true; 405 return true; 406 } 407 catch (MessagingException me) { 408 return false; 409 } 410 catch (IOException ioe) { 411 throw ioExceptionHandler(connection, ioe); 412 } 413 finally { 414 if (mConnection == null) { 415 releaseConnection(connection); 416 } 417 } 418 } 419 420 public boolean create(FolderType type) throws MessagingException { 421 /* 422 * This method needs to operate in the unselected mode as well as the selected mode 423 * so we must get the connection ourselves if it's not there. We are specifically 424 * not calling checkOpen() since we don't care if the folder is open. 425 */ 426 ImapConnection connection = null; 427 synchronized(this) { 428 if (mConnection == null) { 429 connection = getConnection(); 430 } 431 else { 432 connection = mConnection; 433 } 434 } 435 try { 436 connection.executeSimpleCommand(String.format("CREATE \"%s\"", 437 encodeFolderName(mName))); 438 return true; 439 } 440 catch (MessagingException me) { 441 return false; 442 } 443 catch (IOException ioe) { 444 throw ioExceptionHandler(mConnection, ioe); 445 } 446 finally { 447 if (mConnection == null) { 448 releaseConnection(connection); 449 } 450 } 451 } 452 453 @Override 454 public void copyMessages(Message[] messages, Folder folder) throws MessagingException { 455 checkOpen(); 456 String[] uids = new String[messages.length]; 457 for (int i = 0, count = messages.length; i < count; i++) { 458 uids[i] = messages[i].getUid(); 459 } 460 try { 461 mConnection.executeSimpleCommand(String.format("UID COPY %s \"%s\"", 462 Utility.combine(uids, ','), 463 encodeFolderName(folder.getName()))); 464 } 465 catch (IOException ioe) { 466 throw ioExceptionHandler(mConnection, ioe); 467 } 468 } 469 470 @Override 471 public int getMessageCount() { 472 return mMessageCount; 473 } 474 475 @Override 476 public int getUnreadMessageCount() throws MessagingException { 477 checkOpen(); 478 try { 479 int unreadMessageCount = 0; 480 List<ImapResponse> responses = mConnection.executeSimpleCommand( 481 String.format("STATUS \"%s\" (UNSEEN)", 482 encodeFolderName(mName))); 483 for (ImapResponse response : responses) { 484 if (response.mTag == null && response.get(0).equals("STATUS")) { 485 ImapList status = response.getList(2); 486 unreadMessageCount = status.getKeyedNumber("UNSEEN"); 487 } 488 } 489 return unreadMessageCount; 490 } 491 catch (IOException ioe) { 492 throw ioExceptionHandler(mConnection, ioe); 493 } 494 } 495 496 @Override 497 public void delete(boolean recurse) throws MessagingException { 498 throw new Error("ImapStore.delete() not yet implemented"); 499 } 500 501 @Override 502 public Message getMessage(String uid) throws MessagingException { 503 checkOpen(); 504 505 try { 506 try { 507 List<ImapResponse> responses = 508 mConnection.executeSimpleCommand(String.format("UID SEARCH UID %S", uid)); 509 for (ImapResponse response : responses) { 510 if (response.mTag == null && response.get(0).equals("SEARCH")) { 511 for (int i = 1, count = response.size(); i < count; i++) { 512 if (uid.equals(response.get(i))) { 513 return new ImapMessage(uid, this); 514 } 515 } 516 } 517 } 518 } 519 catch (MessagingException me) { 520 return null; 521 } 522 } 523 catch (IOException ioe) { 524 throw ioExceptionHandler(mConnection, ioe); 525 } 526 return null; 527 } 528 529 @Override 530 public Message[] getMessages(int start, int end, MessageRetrievalListener listener) 531 throws MessagingException { 532 if (start < 1 || end < 1 || end < start) { 533 throw new MessagingException( 534 String.format("Invalid message set %d %d", 535 start, end)); 536 } 537 checkOpen(); 538 ArrayList<Message> messages = new ArrayList<Message>(); 539 try { 540 ArrayList<String> uids = new ArrayList<String>(); 541 List<ImapResponse> responses = mConnection 542 .executeSimpleCommand(String.format("UID SEARCH %d:%d NOT DELETED", start, end)); 543 for (ImapResponse response : responses) { 544 if (response.get(0).equals("SEARCH")) { 545 for (int i = 1, count = response.size(); i < count; i++) { 546 uids.add(response.getString(i)); 547 } 548 } 549 } 550 for (int i = 0, count = uids.size(); i < count; i++) { 551 if (listener != null) { 552 listener.messageStarted(uids.get(i), i, count); 553 } 554 ImapMessage message = new ImapMessage(uids.get(i), this); 555 messages.add(message); 556 if (listener != null) { 557 listener.messageFinished(message, i, count); 558 } 559 } 560 } catch (IOException ioe) { 561 throw ioExceptionHandler(mConnection, ioe); 562 } 563 return messages.toArray(new Message[] {}); 564 } 565 566 public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { 567 return getMessages(null, listener); 568 } 569 570 public Message[] getMessages(String[] uids, MessageRetrievalListener listener) 571 throws MessagingException { 572 checkOpen(); 573 ArrayList<Message> messages = new ArrayList<Message>(); 574 try { 575 if (uids == null) { 576 List<ImapResponse> responses = mConnection 577 .executeSimpleCommand("UID SEARCH 1:* NOT DELETED"); 578 ArrayList<String> tempUids = new ArrayList<String>(); 579 for (ImapResponse response : responses) { 580 if (response.get(0).equals("SEARCH")) { 581 for (int i = 1, count = response.size(); i < count; i++) { 582 tempUids.add(response.getString(i)); 583 } 584 } 585 } 586 uids = tempUids.toArray(new String[] {}); 587 } 588 for (int i = 0, count = uids.length; i < count; i++) { 589 if (listener != null) { 590 listener.messageStarted(uids[i], i, count); 591 } 592 ImapMessage message = new ImapMessage(uids[i], this); 593 messages.add(message); 594 if (listener != null) { 595 listener.messageFinished(message, i, count); 596 } 597 } 598 } catch (IOException ioe) { 599 throw ioExceptionHandler(mConnection, ioe); 600 } 601 return messages.toArray(new Message[] {}); 602 } 603 604 public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) 605 throws MessagingException { 606 if (messages == null || messages.length == 0) { 607 return; 608 } 609 checkOpen(); 610 String[] uids = new String[messages.length]; 611 HashMap<String, Message> messageMap = new HashMap<String, Message>(); 612 for (int i = 0, count = messages.length; i < count; i++) { 613 uids[i] = messages[i].getUid(); 614 messageMap.put(uids[i], messages[i]); 615 } 616 617 /* 618 * Figure out what command we are going to run: 619 * Flags - UID FETCH (FLAGS) 620 * Envelope - UID FETCH ([FLAGS] INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc)]) 621 * 622 */ 623 LinkedHashSet<String> fetchFields = new LinkedHashSet<String>(); 624 fetchFields.add("UID"); 625 if (fp.contains(FetchProfile.Item.FLAGS)) { 626 fetchFields.add("FLAGS"); 627 } 628 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 629 fetchFields.add("INTERNALDATE"); 630 fetchFields.add("RFC822.SIZE"); 631 fetchFields.add("BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc)]"); 632 } 633 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 634 fetchFields.add("BODYSTRUCTURE"); 635 } 636 if (fp.contains(FetchProfile.Item.BODY_SANE)) { 637 fetchFields.add(String.format("BODY.PEEK[]<0.%d>", FETCH_BODY_SANE_SUGGESTED_SIZE)); 638 } 639 if (fp.contains(FetchProfile.Item.BODY)) { 640 fetchFields.add("BODY.PEEK[]"); 641 } 642 for (Object o : fp) { 643 if (o instanceof Part) { 644 Part part = (Part) o; 645 String partId = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA)[0]; 646 fetchFields.add("BODY.PEEK[" + partId + "]"); 647 } 648 } 649 650 try { 651 String tag = mConnection.sendCommand(String.format("UID FETCH %s (%s)", 652 Utility.combine(uids, ','), 653 Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') 654 ), false); 655 ImapResponse response; 656 int messageNumber = 0; 657 do { 658 response = mConnection.readResponse(); 659 660 if (response.mTag == null && response.get(1).equals("FETCH")) { 661 ImapList fetchList = (ImapList)response.getKeyedValue("FETCH"); 662 String uid = fetchList.getKeyedString("UID"); 663 664 Message message = messageMap.get(uid); 665 666 if (listener != null) { 667 listener.messageStarted(uid, messageNumber++, messageMap.size()); 668 } 669 670 if (fp.contains(FetchProfile.Item.FLAGS)) { 671 ImapList flags = fetchList.getKeyedList("FLAGS"); 672 ImapMessage imapMessage = (ImapMessage) message; 673 if (flags != null) { 674 for (int i = 0, count = flags.size(); i < count; i++) { 675 String flag = flags.getString(i); 676 if (flag.equals("\\Deleted")) { 677 imapMessage.setFlagInternal(Flag.DELETED, true); 678 } 679 else if (flag.equals("\\Answered")) { 680 imapMessage.setFlagInternal(Flag.ANSWERED, true); 681 } 682 else if (flag.equals("\\Seen")) { 683 imapMessage.setFlagInternal(Flag.SEEN, true); 684 } 685 else if (flag.equals("\\Flagged")) { 686 imapMessage.setFlagInternal(Flag.FLAGGED, true); 687 } 688 } 689 } 690 } 691 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 692 Date internalDate = fetchList.getKeyedDate("INTERNALDATE"); 693 int size = fetchList.getKeyedNumber("RFC822.SIZE"); 694 InputStream headerStream = fetchList.getLiteral(fetchList.size() - 1); 695 696 ImapMessage imapMessage = (ImapMessage) message; 697 698 message.setInternalDate(internalDate); 699 imapMessage.setSize(size); 700 imapMessage.parse(headerStream); 701 } 702 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 703 ImapList bs = fetchList.getKeyedList("BODYSTRUCTURE"); 704 if (bs != null) { 705 try { 706 parseBodyStructure(bs, message, "TEXT"); 707 } 708 catch (MessagingException e) { 709 if (Config.LOGV) { 710 Log.v(Email.LOG_TAG, "Error handling message", e); 711 } 712 message.setBody(null); 713 } 714 } 715 } 716 if (fp.contains(FetchProfile.Item.BODY)) { 717 InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1); 718 ImapMessage imapMessage = (ImapMessage) message; 719 imapMessage.parse(bodyStream); 720 } 721 if (fp.contains(FetchProfile.Item.BODY_SANE)) { 722 InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1); 723 ImapMessage imapMessage = (ImapMessage) message; 724 imapMessage.parse(bodyStream); 725 } 726 for (Object o : fp) { 727 if (o instanceof Part) { 728 Part part = (Part) o; 729 InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1); 730 String contentType = part.getContentType(); 731 String contentTransferEncoding = part.getHeader( 732 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; 733 part.setBody(MimeUtility.decodeBody( 734 bodyStream, 735 contentTransferEncoding)); 736 } 737 } 738 739 if (listener != null) { 740 listener.messageFinished(message, messageNumber, messageMap.size()); 741 } 742 } 743 744 while (response.more()); 745 746 } while (response.mTag == null); 747 } 748 catch (IOException ioe) { 749 throw ioExceptionHandler(mConnection, ioe); 750 } 751 } 752 753 @Override 754 public Flag[] getPermanentFlags() throws MessagingException { 755 return PERMANENT_FLAGS; 756 } 757 758 /** 759 * Handle any untagged responses that the caller doesn't care to handle themselves. 760 * @param responses 761 */ 762 private void handleUntaggedResponses(List<ImapResponse> responses) { 763 for (ImapResponse response : responses) { 764 handleUntaggedResponse(response); 765 } 766 } 767 768 /** 769 * Handle an untagged response that the caller doesn't care to handle themselves. 770 * @param response 771 */ 772 private void handleUntaggedResponse(ImapResponse response) { 773 if (response.mTag == null && response.get(1).equals("EXISTS")) { 774 mMessageCount = response.getNumber(0); 775 } 776 } 777 778 private void parseBodyStructure(ImapList bs, Part part, String id) 779 throws MessagingException { 780 if (bs.get(0) instanceof ImapList) { 781 /* 782 * This is a multipart/* 783 */ 784 MimeMultipart mp = new MimeMultipart(); 785 for (int i = 0, count = bs.size(); i < count; i++) { 786 if (bs.get(i) instanceof ImapList) { 787 /* 788 * For each part in the message we're going to add a new BodyPart and parse 789 * into it. 790 */ 791 ImapBodyPart bp = new ImapBodyPart(); 792 if (id.equals("TEXT")) { 793 parseBodyStructure(bs.getList(i), bp, Integer.toString(i + 1)); 794 } 795 else { 796 parseBodyStructure(bs.getList(i), bp, id + "." + (i + 1)); 797 } 798 mp.addBodyPart(bp); 799 } 800 else { 801 /* 802 * We've got to the end of the children of the part, so now we can find out 803 * what type it is and bail out. 804 */ 805 String subType = bs.getString(i); 806 mp.setSubType(subType.toLowerCase()); 807 break; 808 } 809 } 810 part.setBody(mp); 811 } 812 else{ 813 /* 814 * This is a body. We need to add as much information as we can find out about 815 * it to the Part. 816 */ 817 818 /* 819 body type 820 body subtype 821 body parameter parenthesized list 822 body id 823 body description 824 body encoding 825 body size 826 */ 827 828 829 String type = bs.getString(0); 830 String subType = bs.getString(1); 831 String mimeType = (type + "/" + subType).toLowerCase(); 832 833 ImapList bodyParams = null; 834 if (bs.get(2) instanceof ImapList) { 835 bodyParams = bs.getList(2); 836 } 837 String encoding = bs.getString(5); 838 int size = bs.getNumber(6); 839 840 if (MimeUtility.mimeTypeMatches(mimeType, "message/rfc822")) { 841// A body type of type MESSAGE and subtype RFC822 842// contains, immediately after the basic fields, the 843// envelope structure, body structure, and size in 844// text lines of the encapsulated message. 845// [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] 846 /* 847 * This will be caught by fetch and handled appropriately. 848 */ 849 throw new MessagingException("BODYSTRUCTURE message/rfc822 not yet supported."); 850 } 851 852 /* 853 * Set the content type with as much information as we know right now. 854 */ 855 String contentType = String.format("%s", mimeType); 856 857 if (bodyParams != null) { 858 /* 859 * If there are body params we might be able to get some more information out 860 * of them. 861 */ 862 for (int i = 0, count = bodyParams.size(); i < count; i += 2) { 863 contentType += String.format(";\n %s=\"%s\"", 864 bodyParams.getString(i), 865 bodyParams.getString(i + 1)); 866 } 867 } 868 869 part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); 870 871 // Extension items 872 ImapList bodyDisposition = null; 873 if (("text".equalsIgnoreCase(type)) 874 && (bs.size() > 8) 875 && (bs.get(9) instanceof ImapList)) { 876 bodyDisposition = bs.getList(9); 877 } 878 else if (!("text".equalsIgnoreCase(type)) 879 && (bs.size() > 7) 880 && (bs.get(8) instanceof ImapList)) { 881 bodyDisposition = bs.getList(8); 882 } 883 884 String contentDisposition = ""; 885 886 if (bodyDisposition != null && bodyDisposition.size() > 0) { 887 if (!"NIL".equalsIgnoreCase(bodyDisposition.getString(0))) { 888 contentDisposition = bodyDisposition.getString(0).toLowerCase(); 889 } 890 891 if ((bodyDisposition.size() > 1) 892 && (bodyDisposition.get(1) instanceof ImapList)) { 893 ImapList bodyDispositionParams = bodyDisposition.getList(1); 894 /* 895 * If there is body disposition information we can pull some more information 896 * about the attachment out. 897 */ 898 for (int i = 0, count = bodyDispositionParams.size(); i < count; i += 2) { 899 contentDisposition += String.format(";\n %s=\"%s\"", 900 bodyDispositionParams.getString(i).toLowerCase(), 901 bodyDispositionParams.getString(i + 1)); 902 } 903 } 904 } 905 906 if (MimeUtility.getHeaderParameter(contentDisposition, "size") == null) { 907 contentDisposition += String.format(";\n size=%d", size); 908 } 909 910 /* 911 * Set the content disposition containing at least the size. Attachment 912 * handling code will use this down the road. 913 */ 914 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition); 915 916 917 /* 918 * Set the Content-Transfer-Encoding header. Attachment code will use this 919 * to parse the body. 920 */ 921 part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); 922 923 if (part instanceof ImapMessage) { 924 ((ImapMessage) part).setSize(size); 925 } 926 else if (part instanceof ImapBodyPart) { 927 ((ImapBodyPart) part).setSize(size); 928 } 929 else { 930 throw new MessagingException("Unknown part type " + part.toString()); 931 } 932 part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); 933 } 934 935 } 936 937 /** 938 * Appends the given messages to the selected folder. This implementation also determines 939 * the new UID of the given message on the IMAP server and sets the Message's UID to the 940 * new server UID. 941 */ 942 public void appendMessages(Message[] messages) throws MessagingException { 943 checkOpen(); 944 try { 945 for (Message message : messages) { 946 CountingOutputStream out = new CountingOutputStream(); 947 EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); 948 message.writeTo(eolOut); 949 eolOut.flush(); 950 mConnection.sendCommand( 951 String.format("APPEND \"%s\" {%d}", 952 encodeFolderName(mName), 953 out.getCount()), false); 954 ImapResponse response; 955 do { 956 response = mConnection.readResponse(); 957 if (response.mCommandContinuationRequested) { 958 eolOut = new EOLConvertingOutputStream(mConnection.mOut); 959 message.writeTo(eolOut); 960 eolOut.write('\r'); 961 eolOut.write('\n'); 962 eolOut.flush(); 963 } 964 else if (response.mTag == null) { 965 handleUntaggedResponse(response); 966 } 967 while (response.more()); 968 } while(response.mTag == null); 969 970 /* 971 * Try to find the UID of the message we just appended using the 972 * Message-ID header. 973 */ 974 String[] messageIdHeader = message.getHeader("Message-ID"); 975 if (messageIdHeader == null || messageIdHeader.length == 0) { 976 continue; 977 } 978 String messageId = messageIdHeader[0]; 979 List<ImapResponse> responses = 980 mConnection.executeSimpleCommand( 981 String.format("UID SEARCH (HEADER MESSAGE-ID %s)", messageId)); 982 for (ImapResponse response1 : responses) { 983 if (response1.mTag == null && response1.get(0).equals("SEARCH") 984 && response1.size() > 1) { 985 message.setUid(response1.getString(1)); 986 } 987 } 988 989 } 990 } 991 catch (IOException ioe) { 992 throw ioExceptionHandler(mConnection, ioe); 993 } 994 } 995 996 public Message[] expunge() throws MessagingException { 997 checkOpen(); 998 try { 999 handleUntaggedResponses(mConnection.executeSimpleCommand("EXPUNGE")); 1000 } catch (IOException ioe) { 1001 throw ioExceptionHandler(mConnection, ioe); 1002 } 1003 return null; 1004 } 1005 1006 public void setFlags(Message[] messages, Flag[] flags, boolean value) 1007 throws MessagingException { 1008 checkOpen(); 1009 String[] uids = new String[messages.length]; 1010 for (int i = 0, count = messages.length; i < count; i++) { 1011 uids[i] = messages[i].getUid(); 1012 } 1013 ArrayList<String> flagNames = new ArrayList<String>(); 1014 for (int i = 0, count = flags.length; i < count; i++) { 1015 Flag flag = flags[i]; 1016 if (flag == Flag.SEEN) { 1017 flagNames.add("\\Seen"); 1018 } 1019 else if (flag == Flag.DELETED) { 1020 flagNames.add("\\Deleted"); 1021 } 1022 } 1023 try { 1024 mConnection.executeSimpleCommand(String.format("UID STORE %s %sFLAGS.SILENT (%s)", 1025 Utility.combine(uids, ','), 1026 value ? "+" : "-", 1027 Utility.combine(flagNames.toArray(new String[flagNames.size()]), ' '))); 1028 } 1029 catch (IOException ioe) { 1030 throw ioExceptionHandler(mConnection, ioe); 1031 } 1032 } 1033 1034 private void checkOpen() throws MessagingException { 1035 if (!isOpen()) { 1036 throw new MessagingException("Folder " + mName + " is not open."); 1037 } 1038 } 1039 1040 private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) 1041 throws MessagingException { 1042 connection.close(); 1043 close(false); 1044 return new MessagingException("IO Error", ioe); 1045 } 1046 1047 @Override 1048 public boolean equals(Object o) { 1049 if (o instanceof ImapFolder) { 1050 return ((ImapFolder)o).mName.equals(mName); 1051 } 1052 return super.equals(o); 1053 } 1054 } 1055 1056 /** 1057 * A cacheable class that stores the details for a single IMAP connection. 1058 */ 1059 class ImapConnection { 1060 private Socket mSocket; 1061 private PeekableInputStream mIn; 1062 private OutputStream mOut; 1063 private ImapResponseParser mParser; 1064 private int mNextCommandTag; 1065 1066 public void open() throws IOException, MessagingException { 1067 if (isOpen()) { 1068 return; 1069 } 1070 1071 mNextCommandTag = 1; 1072 1073 try { 1074 SocketAddress socketAddress = new InetSocketAddress(mHost, mPort); 1075 if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED || 1076 mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) { 1077 SSLContext sslContext = SSLContext.getInstance("TLS"); 1078 final boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED; 1079 sslContext.init(null, new TrustManager[] { 1080 TrustManagerFactory.get(mHost, secure) 1081 }, new SecureRandom()); 1082 mSocket = sslContext.getSocketFactory().createSocket(); 1083 mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); 1084 } else { 1085 mSocket = new Socket(); 1086 mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); 1087 } 1088 1089 mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); 1090 1091 mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1092 1024)); 1093 mParser = new ImapResponseParser(mIn); 1094 mOut = mSocket.getOutputStream(); 1095 1096 // BANNER 1097 mParser.readResponse(); 1098 1099 if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL 1100 || mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { 1101 // CAPABILITY 1102 List<ImapResponse> responses = executeSimpleCommand("CAPABILITY"); 1103 if (responses.size() != 2) { 1104 throw new MessagingException("Invalid CAPABILITY response received"); 1105 } 1106 if (responses.get(0).contains("STARTTLS")) { 1107 // STARTTLS 1108 executeSimpleCommand("STARTTLS"); 1109 1110 SSLContext sslContext = SSLContext.getInstance("TLS"); 1111 boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED; 1112 sslContext.init(null, new TrustManager[] { 1113 TrustManagerFactory.get(mHost, secure) 1114 }, new SecureRandom()); 1115 mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort, 1116 true); 1117 mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); 1118 mIn = new PeekableInputStream(new BufferedInputStream(mSocket 1119 .getInputStream(), 1024)); 1120 mParser = new ImapResponseParser(mIn); 1121 mOut = mSocket.getOutputStream(); 1122 } else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { 1123 throw new MessagingException("TLS not supported but required"); 1124 } 1125 } 1126 1127 mOut = new BufferedOutputStream(mOut); 1128 1129 try { 1130 // TODO eventually we need to add additional authentication 1131 // options such as SASL 1132 executeSimpleCommand("LOGIN " + mUsername + " " + mPassword, true); 1133 } catch (ImapException ie) { 1134 throw new AuthenticationFailedException(ie.getAlertText(), ie); 1135 1136 } catch (MessagingException me) { 1137 throw new AuthenticationFailedException(null, me); 1138 } 1139 } catch (SSLException e) { 1140 throw new CertificateValidationException(e.getMessage(), e); 1141 } catch (GeneralSecurityException gse) { 1142 throw new MessagingException( 1143 "Unable to open connection to IMAP server due to security error.", gse); 1144 } 1145 } 1146 1147 public boolean isOpen() { 1148 return (mIn != null && mOut != null && mSocket != null && mSocket.isConnected() && !mSocket 1149 .isClosed()); 1150 } 1151 1152 public void close() { 1153// if (isOpen()) { 1154// try { 1155// executeSimpleCommand("LOGOUT"); 1156// } catch (Exception e) { 1157// 1158// } 1159// } 1160 try { 1161 mIn.close(); 1162 } catch (Exception e) { 1163 1164 } 1165 try { 1166 mOut.close(); 1167 } catch (Exception e) { 1168 1169 } 1170 try { 1171 mSocket.close(); 1172 } catch (Exception e) { 1173 1174 } 1175 mIn = null; 1176 mOut = null; 1177 mSocket = null; 1178 } 1179 1180 public ImapResponse readResponse() throws IOException, MessagingException { 1181 return mParser.readResponse(); 1182 } 1183 1184 public String sendCommand(String command, boolean sensitive) 1185 throws MessagingException, IOException { 1186 open(); 1187 String tag = Integer.toString(mNextCommandTag++); 1188 String commandToSend = tag + " " + command; 1189 mOut.write(commandToSend.getBytes()); 1190 mOut.write('\r'); 1191 mOut.write('\n'); 1192 mOut.flush(); 1193 if (Config.LOGD) { 1194 if (Email.DEBUG) { 1195 if (sensitive && !Email.DEBUG_SENSITIVE) { 1196 Log.d(Email.LOG_TAG, ">>> " 1197 + "[Command Hidden, Enable Sensitive Debug Logging To Show]"); 1198 } else { 1199 Log.d(Email.LOG_TAG, ">>> " + commandToSend); 1200 } 1201 } 1202 } 1203 return tag; 1204 } 1205 1206 public List<ImapResponse> executeSimpleCommand(String command) throws IOException, 1207 ImapException, MessagingException { 1208 return executeSimpleCommand(command, false); 1209 } 1210 1211 public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive) 1212 throws IOException, ImapException, MessagingException { 1213 String tag = sendCommand(command, sensitive); 1214 ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>(); 1215 ImapResponse response; 1216 do { 1217 response = mParser.readResponse(); 1218 responses.add(response); 1219 } while (response.mTag == null); 1220 if (response.size() < 1 || !response.get(0).equals("OK")) { 1221 throw new ImapException(response.toString(), response.getAlertText()); 1222 } 1223 return responses; 1224 } 1225 } 1226 1227 class ImapMessage extends MimeMessage { 1228 ImapMessage(String uid, Folder folder) throws MessagingException { 1229 this.mUid = uid; 1230 this.mFolder = folder; 1231 } 1232 1233 public void setSize(int size) { 1234 this.mSize = size; 1235 } 1236 1237 public void parse(InputStream in) throws IOException, MessagingException { 1238 super.parse(in); 1239 } 1240 1241 public void setFlagInternal(Flag flag, boolean set) throws MessagingException { 1242 super.setFlag(flag, set); 1243 } 1244 1245 @Override 1246 public void setFlag(Flag flag, boolean set) throws MessagingException { 1247 super.setFlag(flag, set); 1248 mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); 1249 } 1250 } 1251 1252 class ImapBodyPart extends MimeBodyPart { 1253 public ImapBodyPart() throws MessagingException { 1254 super(); 1255 } 1256 1257 public void setSize(int size) { 1258 this.mSize = size; 1259 } 1260 } 1261 1262 class ImapException extends MessagingException { 1263 String mAlertText; 1264 1265 public ImapException(String message, String alertText, Throwable throwable) { 1266 super(message, throwable); 1267 this.mAlertText = alertText; 1268 } 1269 1270 public ImapException(String message, String alertText) { 1271 super(message); 1272 this.mAlertText = alertText; 1273 } 1274 1275 public String getAlertText() { 1276 return mAlertText; 1277 } 1278 1279 public void setAlertText(String alertText) { 1280 mAlertText = alertText; 1281 } 1282 } 1283} 1284