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