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