ImapFolder.java revision e87ff6c3cbbfc5e3636f9827b58820652e3ea1c5
1/* 2 * Copyright (C) 2011 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 android.content.Context; 20import android.text.TextUtils; 21import android.util.Base64DataException; 22import android.util.Log; 23 24import com.android.email.Email; 25import com.android.email.mail.store.ImapStore.ImapConnection; 26import com.android.email.mail.store.ImapStore.ImapException; 27import com.android.email.mail.store.ImapStore.ImapMessage; 28import com.android.email.mail.store.imap.ImapConstants; 29import com.android.email.mail.store.imap.ImapElement; 30import com.android.email.mail.store.imap.ImapList; 31import com.android.email.mail.store.imap.ImapResponse; 32import com.android.email.mail.store.imap.ImapString; 33import com.android.email.mail.store.imap.ImapUtility; 34import com.android.email.mail.transport.CountingOutputStream; 35import com.android.email.mail.transport.EOLConvertingOutputStream; 36import com.android.emailcommon.Logging; 37import com.android.emailcommon.internet.BinaryTempFileBody; 38import com.android.emailcommon.internet.MimeBodyPart; 39import com.android.emailcommon.internet.MimeHeader; 40import com.android.emailcommon.internet.MimeMultipart; 41import com.android.emailcommon.internet.MimeUtility; 42import com.android.emailcommon.mail.AuthenticationFailedException; 43import com.android.emailcommon.mail.Body; 44import com.android.emailcommon.mail.FetchProfile; 45import com.android.emailcommon.mail.Flag; 46import com.android.emailcommon.mail.Folder; 47import com.android.emailcommon.mail.Message; 48import com.android.emailcommon.mail.MessagingException; 49import com.android.emailcommon.mail.Part; 50import com.android.emailcommon.provider.EmailContent.Mailbox; 51import com.android.emailcommon.utility.Utility; 52 53import java.io.IOException; 54import java.io.InputStream; 55import java.io.OutputStream; 56import java.util.ArrayList; 57import java.util.Arrays; 58import java.util.Date; 59import java.util.HashMap; 60import java.util.LinkedHashSet; 61import java.util.List; 62 63class ImapFolder extends Folder { 64 private final ImapStore mStore; 65 private final String mName; 66 private int mMessageCount = -1; 67 private ImapConnection mConnection; 68 private OpenMode mMode; 69 private boolean mExists; 70 /** The local mailbox associated with this remote folder */ 71 Mailbox mMailbox; 72 /** A set of hashes that can be used to track dirtiness */ 73 Object mHash[]; 74 75 /*package*/ ImapFolder(ImapStore store, String name) { 76 mStore = store; 77 mName = name; 78 } 79 80 private void destroyResponses() { 81 if (mConnection != null) { 82 mConnection.destroyResponses(); 83 } 84 } 85 86 @Override 87 public void open(OpenMode mode, PersistentDataCallbacks callbacks) 88 throws MessagingException { 89 try { 90 if (isOpenForTest()) { 91 if (mMode == mode) { 92 // Make sure the connection is valid. 93 // If it's not we'll close it down and continue on to get a new one. 94 try { 95 mConnection.executeSimpleCommand(ImapConstants.NOOP); 96 return; 97 98 } catch (IOException ioe) { 99 ioExceptionHandler(mConnection, ioe); 100 } finally { 101 destroyResponses(); 102 } 103 } else { 104 // Return the connection to the pool, if exists. 105 close(false); 106 } 107 } 108 synchronized (this) { 109 mConnection = mStore.getConnection(); 110 } 111 // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk 112 // $MDNSent) 113 // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft 114 // NonJunk $MDNSent \*)] Flags permitted. 115 // * 23 EXISTS 116 // * 0 RECENT 117 // * OK [UIDVALIDITY 1125022061] UIDs valid 118 // * OK [UIDNEXT 57576] Predicted next UID 119 // 2 OK [READ-WRITE] Select completed. 120 try { 121 doSelect(); 122 } catch (IOException ioe) { 123 throw ioExceptionHandler(mConnection, ioe); 124 } finally { 125 destroyResponses(); 126 } 127 } catch (AuthenticationFailedException e) { 128 // Don't cache this connection, so we're forced to try connecting/login again 129 mConnection = null; 130 close(false); 131 throw e; 132 } catch (MessagingException e) { 133 mExists = false; 134 close(false); 135 throw e; 136 } 137 } 138 139 @Override 140 public boolean isOpenForTest() { 141 return mExists && mConnection != null; 142 } 143 144 @Override 145 public OpenMode getMode() { 146 return mMode; 147 } 148 149 @Override 150 public void close(boolean expunge) { 151 // TODO implement expunge 152 mMessageCount = -1; 153 synchronized (this) { 154 destroyResponses(); 155 mStore.poolConnection(mConnection); 156 mConnection = null; 157 } 158 } 159 160 @Override 161 public String getName() { 162 return mName; 163 } 164 165 @Override 166 public boolean exists() throws MessagingException { 167 if (mExists) { 168 return true; 169 } 170 /* 171 * This method needs to operate in the unselected mode as well as the selected mode 172 * so we must get the connection ourselves if it's not there. We are specifically 173 * not calling checkOpen() since we don't care if the folder is open. 174 */ 175 ImapConnection connection = null; 176 synchronized(this) { 177 if (mConnection == null) { 178 connection = mStore.getConnection(); 179 } else { 180 connection = mConnection; 181 } 182 } 183 try { 184 connection.executeSimpleCommand(String.format( 185 ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")", 186 ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); 187 mExists = true; 188 return true; 189 190 } catch (MessagingException me) { 191 return false; 192 193 } catch (IOException ioe) { 194 throw ioExceptionHandler(connection, ioe); 195 196 } finally { 197 connection.destroyResponses(); 198 if (mConnection == null) { 199 mStore.poolConnection(connection); 200 } 201 } 202 } 203 204 // IMAP supports folder creation 205 @Override 206 public boolean canCreate(FolderType type) { 207 return true; 208 } 209 210 @Override 211 public boolean create(FolderType type) throws MessagingException { 212 /* 213 * This method needs to operate in the unselected mode as well as the selected mode 214 * so we must get the connection ourselves if it's not there. We are specifically 215 * not calling checkOpen() since we don't care if the folder is open. 216 */ 217 ImapConnection connection = null; 218 synchronized(this) { 219 if (mConnection == null) { 220 connection = mStore.getConnection(); 221 } else { 222 connection = mConnection; 223 } 224 } 225 try { 226 connection.executeSimpleCommand(String.format(ImapConstants.CREATE + " \"%s\"", 227 ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); 228 return true; 229 230 } catch (MessagingException me) { 231 return false; 232 233 } catch (IOException ioe) { 234 throw ioExceptionHandler(connection, ioe); 235 236 } finally { 237 connection.destroyResponses(); 238 if (mConnection == null) { 239 mStore.poolConnection(connection); 240 } 241 } 242 } 243 244 @Override 245 public void copyMessages(Message[] messages, Folder folder, 246 MessageUpdateCallbacks callbacks) throws MessagingException { 247 checkOpen(); 248 try { 249 List<ImapResponse> responseList = mConnection.executeSimpleCommand( 250 String.format(ImapConstants.UID_COPY + " %s \"%s\"", 251 ImapStore.joinMessageUids(messages), 252 ImapStore.encodeFolderName(folder.getName(), mStore.mPathPrefix))); 253 // Build a message map for faster UID matching 254 HashMap<String, Message> messageMap = new HashMap<String, Message>(); 255 boolean handledUidPlus = false; 256 for (Message m : messages) { 257 messageMap.put(m.getUid(), m); 258 } 259 // Process response to get the new UIDs 260 for (ImapResponse response : responseList) { 261 // All "BAD" responses are bad. Only "NO", tagged responses are bad. 262 if (response.isBad() || (response.isNo() && response.isTagged())) { 263 String responseText = response.getStatusResponseTextOrEmpty().getString(); 264 throw new MessagingException(responseText); 265 } 266 // Skip untagged responses; they're just status 267 if (!response.isTagged()) { 268 continue; 269 } 270 // No callback provided to report of UID changes; nothing more to do here 271 // NOTE: We check this here to catch any server errors 272 if (callbacks == null) { 273 continue; 274 } 275 ImapList copyResponse = response.getListOrEmpty(1); 276 String responseCode = copyResponse.getStringOrEmpty(0).getString(); 277 if (ImapConstants.COPYUID.equals(responseCode)) { 278 handledUidPlus = true; 279 String origIdSet = copyResponse.getStringOrEmpty(2).getString(); 280 String newIdSet = copyResponse.getStringOrEmpty(3).getString(); 281 String[] origIdArray = ImapUtility.getImapSequenceValues(origIdSet); 282 String[] newIdArray = ImapUtility.getImapSequenceValues(newIdSet); 283 // There has to be a 1:1 mapping between old and new IDs 284 if (origIdArray.length != newIdArray.length) { 285 throw new MessagingException("Set length mis-match; orig IDs \"" + 286 origIdSet + "\" new IDs \"" + newIdSet + "\""); 287 } 288 for (int i = 0; i < origIdArray.length; i++) { 289 final String id = origIdArray[i]; 290 final Message m = messageMap.get(id); 291 if (m != null) { 292 callbacks.onMessageUidChange(m, newIdArray[i]); 293 } 294 } 295 } 296 } 297 // If the server doesn't support UIDPLUS, try a different way to get the new UID(s) 298 if (callbacks != null && !handledUidPlus) { 299 ImapFolder newFolder = (ImapFolder)folder; 300 try { 301 // Temporarily select the destination folder 302 newFolder.open(OpenMode.READ_WRITE, null); 303 // Do the search(es) ... 304 for (Message m : messages) { 305 String searchString = "HEADER Message-Id \"" + m.getMessageId() + "\""; 306 String[] newIdArray = newFolder.searchForUids(searchString); 307 if (newIdArray.length == 1) { 308 callbacks.onMessageUidChange(m, newIdArray[0]); 309 } 310 } 311 } catch (MessagingException e) { 312 // Log, but, don't abort; failures here don't need to be propagated 313 Log.d(Logging.LOG_TAG, "Failed to find message", e); 314 } finally { 315 newFolder.close(false); 316 } 317 // Re-select the original folder 318 doSelect(); 319 } 320 } catch (IOException ioe) { 321 throw ioExceptionHandler(mConnection, ioe); 322 } finally { 323 destroyResponses(); 324 } 325 } 326 327 @Override 328 public int getMessageCount() { 329 return mMessageCount; 330 } 331 332 @Override 333 public int getUnreadMessageCount() throws MessagingException { 334 checkOpen(); 335 try { 336 int unreadMessageCount = 0; 337 List<ImapResponse> responses = mConnection.executeSimpleCommand(String.format( 338 ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")", 339 ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); 340 // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292) 341 for (ImapResponse response : responses) { 342 if (response.isDataResponse(0, ImapConstants.STATUS)) { 343 unreadMessageCount = response.getListOrEmpty(2) 344 .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero(); 345 } 346 } 347 return unreadMessageCount; 348 } catch (IOException ioe) { 349 throw ioExceptionHandler(mConnection, ioe); 350 } finally { 351 destroyResponses(); 352 } 353 } 354 355 @Override 356 public void delete(boolean recurse) { 357 throw new Error("ImapStore.delete() not yet implemented"); 358 } 359 360 /* package */ String[] searchForUids(String searchCriteria) 361 throws MessagingException { 362 checkOpen(); 363 List<ImapResponse> responses; 364 try { 365 try { 366 responses = mConnection.executeSimpleCommand( 367 ImapConstants.UID_SEARCH + " " + searchCriteria); 368 } catch (ImapException e) { 369 return Utility.EMPTY_STRINGS; // not found; 370 } catch (IOException ioe) { 371 throw ioExceptionHandler(mConnection, ioe); 372 } 373 // S: * SEARCH 2 3 6 374 final ArrayList<String> uids = new ArrayList<String>(); 375 for (ImapResponse response : responses) { 376 if (!response.isDataResponse(0, ImapConstants.SEARCH)) { 377 continue; 378 } 379 // Found SEARCH response data 380 for (int i = 1; i < response.size(); i++) { 381 ImapString s = response.getStringOrEmpty(i); 382 if (s.isString()) { 383 uids.add(s.getString()); 384 } 385 } 386 } 387 return uids.toArray(Utility.EMPTY_STRINGS); 388 } finally { 389 destroyResponses(); 390 } 391 } 392 393 @Override 394 public Message getMessage(String uid) throws MessagingException { 395 checkOpen(); 396 397 String[] uids = searchForUids(ImapConstants.UID + " " + uid); 398 for (int i = 0; i < uids.length; i++) { 399 if (uids[i].equals(uid)) { 400 return new ImapMessage(uid, this); 401 } 402 } 403 return null; 404 } 405 406 @Override 407 public Message[] getMessages(int start, int end, MessageRetrievalListener listener) 408 throws MessagingException { 409 if (start < 1 || end < 1 || end < start) { 410 throw new MessagingException(String.format("Invalid range: %d %d", start, end)); 411 } 412 return getMessagesInternal( 413 searchForUids(String.format("%d:%d NOT DELETED", start, end)), listener); 414 } 415 416 @Override 417 public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { 418 return getMessages(null, listener); 419 } 420 421 @Override 422 public Message[] getMessages(String[] uids, MessageRetrievalListener listener) 423 throws MessagingException { 424 if (uids == null) { 425 uids = searchForUids("1:* NOT DELETED"); 426 } 427 return getMessagesInternal(uids, listener); 428 } 429 430 public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) { 431 final ArrayList<Message> messages = new ArrayList<Message>(uids.length); 432 for (int i = 0; i < uids.length; i++) { 433 final String uid = uids[i]; 434 final ImapMessage message = new ImapMessage(uid, this); 435 messages.add(message); 436 if (listener != null) { 437 listener.messageRetrieved(message); 438 } 439 } 440 return messages.toArray(Message.EMPTY_ARRAY); 441 } 442 443 @Override 444 public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) 445 throws MessagingException { 446 try { 447 fetchInternal(messages, fp, listener); 448 } catch (RuntimeException e) { // Probably a parser error. 449 Log.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage()); 450 if (mConnection != null) { 451 mConnection.logLastDiscourse(); 452 } 453 throw e; 454 } 455 } 456 457 public void fetchInternal(Message[] messages, FetchProfile fp, 458 MessageRetrievalListener listener) throws MessagingException { 459 if (messages.length == 0) { 460 return; 461 } 462 checkOpen(); 463 HashMap<String, Message> messageMap = new HashMap<String, Message>(); 464 for (Message m : messages) { 465 messageMap.put(m.getUid(), m); 466 } 467 468 /* 469 * Figure out what command we are going to run: 470 * FLAGS - UID FETCH (FLAGS) 471 * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[ 472 * HEADER.FIELDS (date subject from content-type to cc)]) 473 * STRUCTURE - UID FETCH (BODYSTRUCTURE) 474 * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned 475 * BODY - UID FETCH (BODY.PEEK[]) 476 * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID 477 */ 478 479 final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>(); 480 481 fetchFields.add(ImapConstants.UID); 482 if (fp.contains(FetchProfile.Item.FLAGS)) { 483 fetchFields.add(ImapConstants.FLAGS); 484 } 485 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 486 fetchFields.add(ImapConstants.INTERNALDATE); 487 fetchFields.add(ImapConstants.RFC822_SIZE); 488 fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS); 489 } 490 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 491 fetchFields.add(ImapConstants.BODYSTRUCTURE); 492 } 493 494 if (fp.contains(FetchProfile.Item.BODY_SANE)) { 495 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE); 496 } 497 if (fp.contains(FetchProfile.Item.BODY)) { 498 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK); 499 } 500 501 final Part fetchPart = fp.getFirstPart(); 502 if (fetchPart != null) { 503 String[] partIds = 504 fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); 505 if (partIds != null) { 506 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE 507 + "[" + partIds[0] + "]"); 508 } 509 } 510 511 try { 512 mConnection.sendCommand(String.format( 513 ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages), 514 Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') 515 ), false); 516 ImapResponse response; 517 int messageNumber = 0; 518 do { 519 response = null; 520 try { 521 response = mConnection.readResponse(); 522 523 if (!response.isDataResponse(1, ImapConstants.FETCH)) { 524 continue; // Ignore 525 } 526 final ImapList fetchList = response.getListOrEmpty(2); 527 final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID) 528 .getString(); 529 if (TextUtils.isEmpty(uid)) continue; 530 531 ImapMessage message = (ImapMessage) messageMap.get(uid); 532 if (message == null) continue; 533 534 if (fp.contains(FetchProfile.Item.FLAGS)) { 535 final ImapList flags = 536 fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS); 537 for (int i = 0, count = flags.size(); i < count; i++) { 538 final ImapString flag = flags.getStringOrEmpty(i); 539 if (flag.is(ImapConstants.FLAG_DELETED)) { 540 message.setFlagInternal(Flag.DELETED, true); 541 } else if (flag.is(ImapConstants.FLAG_ANSWERED)) { 542 message.setFlagInternal(Flag.ANSWERED, true); 543 } else if (flag.is(ImapConstants.FLAG_SEEN)) { 544 message.setFlagInternal(Flag.SEEN, true); 545 } else if (flag.is(ImapConstants.FLAG_FLAGGED)) { 546 message.setFlagInternal(Flag.FLAGGED, true); 547 } 548 } 549 } 550 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 551 final Date internalDate = fetchList.getKeyedStringOrEmpty( 552 ImapConstants.INTERNALDATE).getDateOrNull(); 553 final int size = fetchList.getKeyedStringOrEmpty( 554 ImapConstants.RFC822_SIZE).getNumberOrZero(); 555 final String header = fetchList.getKeyedStringOrEmpty( 556 ImapConstants.BODY_BRACKET_HEADER, true).getString(); 557 558 message.setInternalDate(internalDate); 559 message.setSize(size); 560 message.parse(Utility.streamFromAsciiString(header)); 561 } 562 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 563 ImapList bs = fetchList.getKeyedListOrEmpty( 564 ImapConstants.BODYSTRUCTURE); 565 if (!bs.isEmpty()) { 566 try { 567 parseBodyStructure(bs, message, ImapConstants.TEXT); 568 } catch (MessagingException e) { 569 if (Email.LOGD) { 570 Log.v(Logging.LOG_TAG, "Error handling message", e); 571 } 572 message.setBody(null); 573 } 574 } 575 } 576 if (fp.contains(FetchProfile.Item.BODY) 577 || fp.contains(FetchProfile.Item.BODY_SANE)) { 578 // Body is keyed by "BODY[...". 579 // TOOD Should we accept "RFC822" as well?? 580 // The old code didn't really check the key, so it accepted any literal 581 // that first appeared. 582 ImapString body = fetchList.getKeyedStringOrEmpty("BODY[", true); 583 InputStream bodyStream = body.getAsStream(); 584 message.parse(bodyStream); 585 } 586 if (fetchPart != null && fetchPart.getSize() > 0) { 587 InputStream bodyStream = 588 fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream(); 589 String contentType = fetchPart.getContentType(); 590 String contentTransferEncoding = fetchPart.getHeader( 591 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; 592 593 // TODO Don't create 2 temp files. 594 // decodeBody creates BinaryTempFileBody, but we could avoid this 595 // if we implement ImapStringBody. 596 // (We'll need to share a temp file. Protect it with a ref-count.) 597 fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding, 598 fetchPart.getSize(), listener)); 599 } 600 601 if (listener != null) { 602 listener.messageRetrieved(message); 603 } 604 } finally { 605 destroyResponses(); 606 } 607 } while (!response.isTagged()); 608 } catch (IOException ioe) { 609 throw ioExceptionHandler(mConnection, ioe); 610 } 611 } 612 613 /** 614 * Removes any content transfer encoding from the stream and returns a Body. 615 * This code is taken/condensed from MimeUtility.decodeBody 616 */ 617 private Body decodeBody(InputStream in, String contentTransferEncoding, int size, 618 MessageRetrievalListener listener) throws IOException { 619 // Get a properly wrapped input stream 620 in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding); 621 BinaryTempFileBody tempBody = new BinaryTempFileBody(); 622 OutputStream out = tempBody.getOutputStream(); 623 try { 624 byte[] buffer = new byte[ImapStore.COPY_BUFFER_SIZE]; 625 int n = 0; 626 int count = 0; 627 while (-1 != (n = in.read(buffer))) { 628 out.write(buffer, 0, n); 629 count += n; 630 if (listener != null) { 631 listener.loadAttachmentProgress(count * 100 / size); 632 } 633 } 634 } catch (Base64DataException bde) { 635 String warning = "\n\n" + Email.getMessageDecodeErrorString(); 636 out.write(warning.getBytes()); 637 } finally { 638 out.close(); 639 } 640 return tempBody; 641 } 642 643 @Override 644 public Flag[] getPermanentFlags() { 645 return ImapStore.PERMANENT_FLAGS; 646 } 647 648 /** 649 * Handle any untagged responses that the caller doesn't care to handle themselves. 650 * @param responses 651 */ 652 private void handleUntaggedResponses(List<ImapResponse> responses) { 653 for (ImapResponse response : responses) { 654 handleUntaggedResponse(response); 655 } 656 } 657 658 /** 659 * Handle an untagged response that the caller doesn't care to handle themselves. 660 * @param response 661 */ 662 private void handleUntaggedResponse(ImapResponse response) { 663 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 664 mMessageCount = response.getStringOrEmpty(0).getNumberOrZero(); 665 } 666 } 667 668 private static void parseBodyStructure(ImapList bs, Part part, String id) 669 throws MessagingException { 670 if (bs.getElementOrNone(0).isList()) { 671 /* 672 * This is a multipart/* 673 */ 674 MimeMultipart mp = new MimeMultipart(); 675 for (int i = 0, count = bs.size(); i < count; i++) { 676 ImapElement e = bs.getElementOrNone(i); 677 if (e.isList()) { 678 /* 679 * For each part in the message we're going to add a new BodyPart and parse 680 * into it. 681 */ 682 MimeBodyPart bp = new MimeBodyPart(); 683 if (id.equals(ImapConstants.TEXT)) { 684 parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1)); 685 686 } else { 687 parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1)); 688 } 689 mp.addBodyPart(bp); 690 691 } else { 692 if (e.isString()) { 693 mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase()); 694 } 695 break; // Ignore the rest of the list. 696 } 697 } 698 part.setBody(mp); 699 } else { 700 /* 701 * This is a body. We need to add as much information as we can find out about 702 * it to the Part. 703 */ 704 705 /* 706 body type 707 body subtype 708 body parameter parenthesized list 709 body id 710 body description 711 body encoding 712 body size 713 */ 714 715 final ImapString type = bs.getStringOrEmpty(0); 716 final ImapString subType = bs.getStringOrEmpty(1); 717 final String mimeType = 718 (type.getString() + "/" + subType.getString()).toLowerCase(); 719 720 final ImapList bodyParams = bs.getListOrEmpty(2); 721 final ImapString cid = bs.getStringOrEmpty(3); 722 final ImapString encoding = bs.getStringOrEmpty(5); 723 final int size = bs.getStringOrEmpty(6).getNumberOrZero(); 724 725 if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) { 726 // A body type of type MESSAGE and subtype RFC822 727 // contains, immediately after the basic fields, the 728 // envelope structure, body structure, and size in 729 // text lines of the encapsulated message. 730 // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL, 731 // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL] 732 /* 733 * This will be caught by fetch and handled appropriately. 734 */ 735 throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 736 + " not yet supported."); 737 } 738 739 /* 740 * Set the content type with as much information as we know right now. 741 */ 742 final StringBuilder contentType = new StringBuilder(mimeType); 743 744 /* 745 * If there are body params we might be able to get some more information out 746 * of them. 747 */ 748 for (int i = 1, count = bodyParams.size(); i < count; i += 2) { 749 750 // TODO We need to convert " into %22, but 751 // because MimeUtility.getHeaderParameter doesn't recognize it, 752 // we can't fix it for now. 753 contentType.append(String.format(";\n %s=\"%s\"", 754 bodyParams.getStringOrEmpty(i - 1).getString(), 755 bodyParams.getStringOrEmpty(i).getString())); 756 } 757 758 part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString()); 759 760 // Extension items 761 final ImapList bodyDisposition; 762 763 if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) { 764 // If media-type is TEXT, 9th element might be: [body-fld-lines] := number 765 // So, if it's not a list, use 10th element. 766 // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.) 767 bodyDisposition = bs.getListOrEmpty(9); 768 } else { 769 bodyDisposition = bs.getListOrEmpty(8); 770 } 771 772 final StringBuilder contentDisposition = new StringBuilder(); 773 774 if (bodyDisposition.size() > 0) { 775 final String bodyDisposition0Str = 776 bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(); 777 if (!TextUtils.isEmpty(bodyDisposition0Str)) { 778 contentDisposition.append(bodyDisposition0Str); 779 } 780 781 final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1); 782 if (!bodyDispositionParams.isEmpty()) { 783 /* 784 * If there is body disposition information we can pull some more 785 * information about the attachment out. 786 */ 787 for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) { 788 789 // TODO We need to convert " into %22. See above. 790 contentDisposition.append(String.format(";\n %s=\"%s\"", 791 bodyDispositionParams.getStringOrEmpty(i - 1) 792 .getString().toLowerCase(), 793 bodyDispositionParams.getStringOrEmpty(i).getString())); 794 } 795 } 796 } 797 798 if ((size > 0) 799 && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") 800 == null)) { 801 contentDisposition.append(String.format(";\n size=%d", size)); 802 } 803 804 if (contentDisposition.length() > 0) { 805 /* 806 * Set the content disposition containing at least the size. Attachment 807 * handling code will use this down the road. 808 */ 809 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, 810 contentDisposition.toString()); 811 } 812 813 /* 814 * Set the Content-Transfer-Encoding header. Attachment code will use this 815 * to parse the body. 816 */ 817 if (!encoding.isEmpty()) { 818 part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, 819 encoding.getString()); 820 } 821 822 /* 823 * Set the Content-ID header. 824 */ 825 if (!cid.isEmpty()) { 826 part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString()); 827 } 828 829 if (size > 0) { 830 if (part instanceof ImapMessage) { 831 ((ImapMessage) part).setSize(size); 832 } else if (part instanceof MimeBodyPart) { 833 ((MimeBodyPart) part).setSize(size); 834 } else { 835 throw new MessagingException("Unknown part type " + part.toString()); 836 } 837 } 838 part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); 839 } 840 841 } 842 843 /** 844 * Appends the given messages to the selected folder. This implementation also determines 845 * the new UID of the given message on the IMAP server and sets the Message's UID to the 846 * new server UID. 847 */ 848 @Override 849 public void appendMessages(Message[] messages) throws MessagingException { 850 checkOpen(); 851 try { 852 for (Message message : messages) { 853 // Create output count 854 CountingOutputStream out = new CountingOutputStream(); 855 EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); 856 message.writeTo(eolOut); 857 eolOut.flush(); 858 // Create flag list (most often this will be "\SEEN") 859 String flagList = ""; 860 Flag[] flags = message.getFlags(); 861 if (flags.length > 0) { 862 StringBuilder sb = new StringBuilder(); 863 for (int i = 0, count = flags.length; i < count; i++) { 864 Flag flag = flags[i]; 865 if (flag == Flag.SEEN) { 866 sb.append(" " + ImapConstants.FLAG_SEEN); 867 } else if (flag == Flag.FLAGGED) { 868 sb.append(" " + ImapConstants.FLAG_FLAGGED); 869 } 870 } 871 if (sb.length() > 0) { 872 flagList = sb.substring(1); 873 } 874 } 875 876 mConnection.sendCommand( 877 String.format(ImapConstants.APPEND + " \"%s\" (%s) {%d}", 878 ImapStore.encodeFolderName(mName, mStore.mPathPrefix), 879 flagList, 880 out.getCount()), false); 881 ImapResponse response; 882 do { 883 response = mConnection.readResponse(); 884 if (response.isContinuationRequest()) { 885 eolOut = new EOLConvertingOutputStream( 886 mConnection.mTransport.getOutputStream()); 887 message.writeTo(eolOut); 888 eolOut.write('\r'); 889 eolOut.write('\n'); 890 eolOut.flush(); 891 } else if (!response.isTagged()) { 892 handleUntaggedResponse(response); 893 } 894 } while (!response.isTagged()); 895 896 // TODO Why not check the response? 897 898 /* 899 * Try to recover the UID of the message from an APPENDUID response. 900 * e.g. 11 OK [APPENDUID 2 238268] APPEND completed 901 */ 902 final ImapList appendList = response.getListOrEmpty(1); 903 if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) { 904 String serverUid = appendList.getStringOrEmpty(2).getString(); 905 if (!TextUtils.isEmpty(serverUid)) { 906 message.setUid(serverUid); 907 continue; 908 } 909 } 910 911 /* 912 * Try to find the UID of the message we just appended using the 913 * Message-ID header. If there are more than one response, take the 914 * last one, as it's most likely the newest (the one we just uploaded). 915 */ 916 String messageId = message.getMessageId(); 917 if (messageId == null || messageId.length() == 0) { 918 continue; 919 } 920 String[] uids = searchForUids( 921 String.format("(HEADER MESSAGE-ID %s)", messageId)); 922 if (uids.length > 0) { 923 message.setUid(uids[0]); 924 } 925 } 926 } catch (IOException ioe) { 927 throw ioExceptionHandler(mConnection, ioe); 928 } finally { 929 destroyResponses(); 930 } 931 } 932 933 @Override 934 public Message[] expunge() throws MessagingException { 935 checkOpen(); 936 try { 937 handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE)); 938 } catch (IOException ioe) { 939 throw ioExceptionHandler(mConnection, ioe); 940 } finally { 941 destroyResponses(); 942 } 943 return null; 944 } 945 946 @Override 947 public void setFlags(Message[] messages, Flag[] flags, boolean value) 948 throws MessagingException { 949 checkOpen(); 950 951 String allFlags = ""; 952 if (flags.length > 0) { 953 StringBuilder flagList = new StringBuilder(); 954 for (int i = 0, count = flags.length; i < count; i++) { 955 Flag flag = flags[i]; 956 if (flag == Flag.SEEN) { 957 flagList.append(" " + ImapConstants.FLAG_SEEN); 958 } else if (flag == Flag.DELETED) { 959 flagList.append(" " + ImapConstants.FLAG_DELETED); 960 } else if (flag == Flag.FLAGGED) { 961 flagList.append(" " + ImapConstants.FLAG_FLAGGED); 962 } 963 } 964 allFlags = flagList.substring(1); 965 } 966 try { 967 mConnection.executeSimpleCommand(String.format( 968 ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)", 969 ImapStore.joinMessageUids(messages), 970 value ? "+" : "-", 971 allFlags)); 972 973 } catch (IOException ioe) { 974 throw ioExceptionHandler(mConnection, ioe); 975 } finally { 976 destroyResponses(); 977 } 978 } 979 980 /** 981 * Persists this folder. We will always perform the proper database operation (e.g. 982 * 'save' or 'update'). As an optimization, if a folder has not been modified, no 983 * database operations are performed. 984 */ 985 void save(Context context) { 986 final Mailbox mailbox = mMailbox; 987 if (!mailbox.isSaved()) { 988 mailbox.save(context); 989 mHash = mailbox.getHashes(); 990 } else { 991 Object[] hash = mailbox.getHashes(); 992 if (!Arrays.equals(mHash, hash)) { 993 mailbox.update(context, mailbox.toContentValues()); 994 mHash = hash; // Save updated hash 995 } 996 } 997 } 998 999 /** 1000 * Selects the folder for use. Before performing any operations on this folder, it 1001 * must be selected. 1002 */ 1003 private void doSelect() throws IOException, MessagingException { 1004 List<ImapResponse> responses = mConnection.executeSimpleCommand( 1005 String.format(ImapConstants.SELECT + " \"%s\"", 1006 ImapStore.encodeFolderName(mName, mStore.mPathPrefix))); 1007 1008 // Assume the folder is opened read-write; unless we are notified otherwise 1009 mMode = OpenMode.READ_WRITE; 1010 int messageCount = -1; 1011 for (ImapResponse response : responses) { 1012 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 1013 messageCount = response.getStringOrEmpty(0).getNumberOrZero(); 1014 } else if (response.isOk()) { 1015 final ImapString responseCode = response.getResponseCodeOrEmpty(); 1016 if (responseCode.is(ImapConstants.READ_ONLY)) { 1017 mMode = OpenMode.READ_ONLY; 1018 } else if (responseCode.is(ImapConstants.READ_WRITE)) { 1019 mMode = OpenMode.READ_WRITE; 1020 } 1021 } else if (response.isTagged()) { // Not OK 1022 throw new MessagingException("Can't open mailbox: " 1023 + response.getStatusResponseTextOrEmpty()); 1024 } 1025 } 1026 if (messageCount == -1) { 1027 throw new MessagingException("Did not find message count during select"); 1028 } 1029 mMessageCount = messageCount; 1030 mExists = true; 1031 } 1032 1033 private void checkOpen() throws MessagingException { 1034 if (!isOpenForTest()) { 1035 throw new MessagingException("Folder " + mName + " is not open."); 1036 } 1037 } 1038 1039 private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) { 1040 if (Email.DEBUG) { 1041 Log.d(Logging.LOG_TAG, "IO Exception detected: ", ioe); 1042 } 1043 connection.destroyResponses(); 1044 connection.close(); 1045 if (connection == mConnection) { 1046 mConnection = null; // To prevent close() from returning the connection to the pool. 1047 close(false); 1048 } 1049 return new MessagingException("IO Error", ioe); 1050 } 1051 1052 @Override 1053 public boolean equals(Object o) { 1054 if (o instanceof ImapFolder) { 1055 return ((ImapFolder)o).mName.equals(mName); 1056 } 1057 return super.equals(o); 1058 } 1059 1060 @Override 1061 public Message createMessage(String uid) { 1062 return new ImapMessage(uid, this); 1063 } 1064} 1065