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