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