ImapFolder.java revision 3f51d09afa61649a1bcf02599bc1df5aafccf088
1/* 2 * Copyright (C) 2015 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 */ 16package com.android.phone.common.mail.store; 17 18import android.content.Context; 19import android.text.TextUtils; 20import android.util.Base64DataException; 21 22import com.android.phone.common.mail.store.ImapStore.ImapException; 23import com.android.phone.common.mail.store.ImapStore.ImapMessage; 24import com.android.phone.common.mail.store.imap.ImapConstants; 25import com.android.phone.common.mail.store.imap.ImapElement; 26import com.android.phone.common.mail.store.imap.ImapList; 27import com.android.phone.common.mail.store.imap.ImapResponse; 28import com.android.phone.common.mail.store.imap.ImapString; 29import com.android.phone.common.mail.store.imap.ImapUtility; 30import com.android.phone.common.mail.AuthenticationFailedException; 31import com.android.phone.common.mail.Body; 32import com.android.phone.common.mail.FetchProfile; 33import com.android.phone.common.mail.Flag; 34import com.android.phone.common.mail.Message; 35import com.android.phone.common.mail.MessagingException; 36import com.android.phone.common.mail.Part; 37import com.android.phone.common.mail.internet.BinaryTempFileBody; 38import com.android.phone.common.mail.internet.MimeBodyPart; 39import com.android.phone.common.mail.internet.MimeHeader; 40import com.android.phone.common.mail.internet.MimeMultipart; 41import com.android.phone.common.mail.internet.MimeUtility; 42import com.android.phone.common.mail.utility.CountingOutputStream; 43import com.android.phone.common.mail.utility.EOLConvertingOutputStream; 44import com.android.phone.common.mail.utils.Utility; 45import com.android.phone.common.mail.utils.LogUtils; 46import com.android.internal.annotations.VisibleForTesting; 47import com.android.phone.common.R; 48 49import org.apache.commons.io.IOUtils; 50 51import java.io.File; 52import java.io.FileInputStream; 53import java.io.FileOutputStream; 54import java.io.IOException; 55import java.io.InputStream; 56import java.io.OutputStream; 57import java.text.SimpleDateFormat; 58import java.util.ArrayList; 59import java.util.Arrays; 60import java.util.Date; 61import java.util.HashMap; 62import java.util.LinkedHashSet; 63import java.util.List; 64import java.util.Locale; 65import java.util.TimeZone; 66 67public class ImapFolder { 68 private static final String TAG = "ImapFolder"; 69 private final static String[] PERMANENT_FLAGS = 70 { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED }; 71 private static final int COPY_BUFFER_SIZE = 16*1024; 72 73 private final ImapStore mStore; 74 private final String mName; 75 private int mMessageCount = -1; 76 private ImapConnection mConnection; 77 private String mMode; 78 private boolean mExists; 79 /** A set of hashes that can be used to track dirtiness */ 80 Object mHash[]; 81 82 public static final String MODE_READ_ONLY = "mode_read_only"; 83 public static final String MODE_READ_WRITE = "mode_read_write"; 84 85 public ImapFolder(ImapStore store, String name) { 86 mStore = store; 87 mName = name; 88 } 89 90 /** 91 * Callback for each message retrieval. 92 */ 93 public interface MessageRetrievalListener { 94 public void messageRetrieved(Message message); 95 public void loadAttachmentProgress(int progress); 96 } 97 98 private void destroyResponses() { 99 if (mConnection != null) { 100 mConnection.destroyResponses(); 101 } 102 } 103 104 public void open(String mode) throws MessagingException { 105 try { 106 if (isOpen()) { 107 if (mMode == mode) { 108 // Make sure the connection is valid. 109 // If it's not we'll close it down and continue on to get a new one. 110 try { 111 mConnection.executeSimpleCommand(ImapConstants.NOOP); 112 return; 113 114 } catch (IOException ioe) { 115 ioExceptionHandler(mConnection, ioe); 116 } finally { 117 destroyResponses(); 118 } 119 } else { 120 // Return the connection to the pool, if exists. 121 close(false); 122 } 123 } 124 synchronized (this) { 125 mConnection = mStore.getConnection(); 126 } 127 // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk 128 // $MDNSent) 129 // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft 130 // NonJunk $MDNSent \*)] Flags permitted. 131 // * 23 EXISTS 132 // * 0 RECENT 133 // * OK [UIDVALIDITY 1125022061] UIDs valid 134 // * OK [UIDNEXT 57576] Predicted next UID 135 // 2 OK [READ-WRITE] Select completed. 136 try { 137 doSelect(); 138 } catch (IOException ioe) { 139 throw ioExceptionHandler(mConnection, ioe); 140 } finally { 141 destroyResponses(); 142 } 143 } catch (AuthenticationFailedException e) { 144 // Don't cache this connection, so we're forced to try connecting/login again 145 mConnection = null; 146 close(false); 147 throw e; 148 } catch (MessagingException e) { 149 mExists = false; 150 close(false); 151 throw e; 152 } 153 } 154 155 public boolean isOpen() { 156 return mExists && mConnection != null; 157 } 158 159 public String getMode() { 160 return mMode; 161 } 162 163 public void close(boolean expunge) { 164 if (expunge) { 165 try { 166 expunge(); 167 } catch (MessagingException e) { 168 LogUtils.e(TAG, e, "Messaging Exception"); 169 } 170 } 171 mMessageCount = -1; 172 synchronized (this) { 173 mStore.closeConnection(); 174 mConnection = null; 175 } 176 } 177 178 public int getMessageCount() { 179 return mMessageCount; 180 } 181 182 String[] getSearchUids(List<ImapResponse> responses) { 183 // S: * SEARCH 2 3 6 184 final ArrayList<String> uids = new ArrayList<String>(); 185 for (ImapResponse response : responses) { 186 if (!response.isDataResponse(0, ImapConstants.SEARCH)) { 187 continue; 188 } 189 // Found SEARCH response data 190 for (int i = 1; i < response.size(); i++) { 191 ImapString s = response.getStringOrEmpty(i); 192 if (s.isString()) { 193 uids.add(s.getString()); 194 } 195 } 196 } 197 return uids.toArray(Utility.EMPTY_STRINGS); 198 } 199 200 @VisibleForTesting 201 String[] searchForUids(String searchCriteria) throws MessagingException { 202 checkOpen(); 203 try { 204 try { 205 final String command = ImapConstants.UID_SEARCH + " " + searchCriteria; 206 final String[] result = getSearchUids(mConnection.executeSimpleCommand(command)); 207 LogUtils.d(TAG, "searchForUids '" + searchCriteria + "' results: " + 208 result.length); 209 return result; 210 } catch (ImapException me) { 211 LogUtils.d(TAG, "ImapException in search: " + searchCriteria, me); 212 return Utility.EMPTY_STRINGS; // Not found 213 } catch (IOException ioe) { 214 LogUtils.d(TAG, "IOException in search: " + searchCriteria, ioe); 215 throw ioExceptionHandler(mConnection, ioe); 216 } 217 } finally { 218 destroyResponses(); 219 } 220 } 221 222 223 public Message getMessage(String uid) throws MessagingException { 224 checkOpen(); 225 226 final String[] uids = searchForUids(ImapConstants.UID + " " + uid); 227 for (int i = 0; i < uids.length; i++) { 228 if (uids[i].equals(uid)) { 229 return new ImapMessage(uid, this); 230 } 231 } 232 return null; 233 } 234 235 @VisibleForTesting 236 protected static boolean isAsciiString(String str) { 237 int len = str.length(); 238 for (int i = 0; i < len; i++) { 239 char c = str.charAt(i); 240 if (c >= 128) return false; 241 } 242 return true; 243 } 244 245 public Message[] getMessages(String[] uids) throws MessagingException { 246 if (uids == null) { 247 uids = searchForUids("1:* NOT DELETED"); 248 } 249 return getMessagesInternal(uids); 250 } 251 252 public Message[] getMessagesInternal(String[] uids) { 253 final ArrayList<Message> messages = new ArrayList<Message>(uids.length); 254 for (int i = 0; i < uids.length; i++) { 255 final String uid = uids[i]; 256 final ImapMessage message = new ImapMessage(uid, this); 257 messages.add(message); 258 } 259 return messages.toArray(Message.EMPTY_ARRAY); 260 } 261 262 public void fetch(Message[] messages, FetchProfile fp, 263 MessageRetrievalListener listener) throws MessagingException { 264 try { 265 fetchInternal(messages, fp, listener); 266 } catch (RuntimeException e) { // Probably a parser error. 267 LogUtils.w(TAG, "Exception detected: " + e.getMessage()); 268 throw e; 269 } 270 } 271 272 public void fetchInternal(Message[] messages, FetchProfile fp, 273 MessageRetrievalListener listener) throws MessagingException { 274 if (messages.length == 0) { 275 return; 276 } 277 checkOpen(); 278 HashMap<String, Message> messageMap = new HashMap<String, Message>(); 279 for (Message m : messages) { 280 messageMap.put(m.getUid(), m); 281 } 282 283 /* 284 * Figure out what command we are going to run: 285 * FLAGS - UID FETCH (FLAGS) 286 * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[ 287 * HEADER.FIELDS (date subject from content-type to cc)]) 288 * STRUCTURE - UID FETCH (BODYSTRUCTURE) 289 * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned 290 * BODY - UID FETCH (BODY.PEEK[]) 291 * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID 292 */ 293 294 final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>(); 295 296 fetchFields.add(ImapConstants.UID); 297 if (fp.contains(FetchProfile.Item.FLAGS)) { 298 fetchFields.add(ImapConstants.FLAGS); 299 } 300 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 301 fetchFields.add(ImapConstants.INTERNALDATE); 302 fetchFields.add(ImapConstants.RFC822_SIZE); 303 fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS); 304 } 305 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 306 fetchFields.add(ImapConstants.BODYSTRUCTURE); 307 } 308 309 if (fp.contains(FetchProfile.Item.BODY_SANE)) { 310 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE); 311 } 312 if (fp.contains(FetchProfile.Item.BODY)) { 313 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK); 314 } 315 316 // TODO Why are we only fetching the first part given? 317 final Part fetchPart = fp.getFirstPart(); 318 if (fetchPart != null) { 319 final String[] partIds = 320 fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); 321 // TODO Why can a single part have more than one Id? And why should we only fetch 322 // the first id if there are more than one? 323 if (partIds != null) { 324 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE 325 + "[" + partIds[0] + "]"); 326 } 327 } 328 329 try { 330 mConnection.sendCommand(String.format(Locale.US, 331 ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages), 332 Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') 333 ), false); 334 ImapResponse response; 335 do { 336 response = null; 337 try { 338 response = mConnection.readResponse(); 339 340 if (!response.isDataResponse(1, ImapConstants.FETCH)) { 341 continue; // Ignore 342 } 343 final ImapList fetchList = response.getListOrEmpty(2); 344 final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID) 345 .getString(); 346 if (TextUtils.isEmpty(uid)) continue; 347 348 ImapMessage message = (ImapMessage) messageMap.get(uid); 349 if (message == null) continue; 350 351 if (fp.contains(FetchProfile.Item.FLAGS)) { 352 final ImapList flags = 353 fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS); 354 for (int i = 0, count = flags.size(); i < count; i++) { 355 final ImapString flag = flags.getStringOrEmpty(i); 356 if (flag.is(ImapConstants.FLAG_DELETED)) { 357 message.setFlagInternal(Flag.DELETED, true); 358 } else if (flag.is(ImapConstants.FLAG_ANSWERED)) { 359 message.setFlagInternal(Flag.ANSWERED, true); 360 } else if (flag.is(ImapConstants.FLAG_SEEN)) { 361 message.setFlagInternal(Flag.SEEN, true); 362 } else if (flag.is(ImapConstants.FLAG_FLAGGED)) { 363 message.setFlagInternal(Flag.FLAGGED, true); 364 } 365 } 366 } 367 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 368 final Date internalDate = fetchList.getKeyedStringOrEmpty( 369 ImapConstants.INTERNALDATE).getDateOrNull(); 370 final int size = fetchList.getKeyedStringOrEmpty( 371 ImapConstants.RFC822_SIZE).getNumberOrZero(); 372 final String header = fetchList.getKeyedStringOrEmpty( 373 ImapConstants.BODY_BRACKET_HEADER, true).getString(); 374 375 message.setInternalDate(internalDate); 376 message.setSize(size); 377 message.parse(Utility.streamFromAsciiString(header)); 378 } 379 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 380 ImapList bs = fetchList.getKeyedListOrEmpty( 381 ImapConstants.BODYSTRUCTURE); 382 if (!bs.isEmpty()) { 383 try { 384 parseBodyStructure(bs, message, ImapConstants.TEXT); 385 } catch (MessagingException e) { 386 LogUtils.v(TAG, e, "Error handling message"); 387 message.setBody(null); 388 } 389 } 390 } 391 if (fp.contains(FetchProfile.Item.BODY) 392 || fp.contains(FetchProfile.Item.BODY_SANE)) { 393 // Body is keyed by "BODY[]...". 394 // Previously used "BODY[..." but this can be confused with "BODY[HEADER..." 395 // TODO Should we accept "RFC822" as well?? 396 ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true); 397 InputStream bodyStream = body.getAsStream(); 398 message.parse(bodyStream); 399 } 400 if (fetchPart != null) { 401 InputStream bodyStream = 402 fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream(); 403 String encodings[] = fetchPart.getHeader( 404 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING); 405 406 String contentTransferEncoding = null; 407 if (encodings != null && encodings.length > 0) { 408 contentTransferEncoding = encodings[0]; 409 } else { 410 // According to http://tools.ietf.org/html/rfc2045#section-6.1 411 // "7bit" is the default. 412 contentTransferEncoding = "7bit"; 413 } 414 415 try { 416 // TODO Don't create 2 temp files. 417 // decodeBody creates BinaryTempFileBody, but we could avoid this 418 // if we implement ImapStringBody. 419 // (We'll need to share a temp file. Protect it with a ref-count.) 420 fetchPart.setBody(decodeBody(mStore.getContext(), bodyStream, 421 contentTransferEncoding, fetchPart.getSize(), listener)); 422 } catch(Exception e) { 423 // TODO: Figure out what kinds of exceptions might actually be thrown 424 // from here. This blanket catch-all is because we're not sure what to 425 // do if we don't have a contentTransferEncoding, and we don't have 426 // time to figure out what exceptions might be thrown. 427 LogUtils.e(TAG, "Error fetching body %s", e); 428 } 429 } 430 431 if (listener != null) { 432 listener.messageRetrieved(message); 433 } 434 } finally { 435 destroyResponses(); 436 } 437 } while (!response.isTagged()); 438 } catch (IOException ioe) { 439 throw ioExceptionHandler(mConnection, ioe); 440 } 441 } 442 443 /** 444 * Removes any content transfer encoding from the stream and returns a Body. 445 * This code is taken/condensed from MimeUtility.decodeBody 446 */ 447 private static Body decodeBody(Context context,InputStream in, String contentTransferEncoding, 448 int size, MessageRetrievalListener listener) throws IOException { 449 // Get a properly wrapped input stream 450 in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding); 451 BinaryTempFileBody tempBody = new BinaryTempFileBody(); 452 OutputStream out = tempBody.getOutputStream(); 453 try { 454 byte[] buffer = new byte[COPY_BUFFER_SIZE]; 455 int n = 0; 456 int count = 0; 457 while (-1 != (n = in.read(buffer))) { 458 out.write(buffer, 0, n); 459 count += n; 460 if (listener != null) { 461 if (size == 0) { 462 // We don't know how big the file is, so just fake it. 463 listener.loadAttachmentProgress((int)Math.ceil(100 * (1-1.0/count))); 464 } else { 465 listener.loadAttachmentProgress(count * 100 / size); 466 } 467 } 468 } 469 } catch (Base64DataException bde) { 470 String warning = "\n\n" + context.getString(R.string.message_decode_error); 471 out.write(warning.getBytes()); 472 } finally { 473 out.close(); 474 } 475 return tempBody; 476 } 477 478 public String[] getPermanentFlags() { 479 return PERMANENT_FLAGS; 480 } 481 482 /** 483 * Handle any untagged responses that the caller doesn't care to handle themselves. 484 * @param responses 485 */ 486 private void handleUntaggedResponses(List<ImapResponse> responses) { 487 for (ImapResponse response : responses) { 488 handleUntaggedResponse(response); 489 } 490 } 491 492 /** 493 * Handle an untagged response that the caller doesn't care to handle themselves. 494 * @param response 495 */ 496 private void handleUntaggedResponse(ImapResponse response) { 497 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 498 mMessageCount = response.getStringOrEmpty(0).getNumberOrZero(); 499 } 500 } 501 502 private static void parseBodyStructure(ImapList bs, Part part, String id) 503 throws MessagingException { 504 if (bs.getElementOrNone(0).isList()) { 505 /* 506 * This is a multipart/* 507 */ 508 MimeMultipart mp = new MimeMultipart(); 509 for (int i = 0, count = bs.size(); i < count; i++) { 510 ImapElement e = bs.getElementOrNone(i); 511 if (e.isList()) { 512 /* 513 * For each part in the message we're going to add a new BodyPart and parse 514 * into it. 515 */ 516 MimeBodyPart bp = new MimeBodyPart(); 517 if (id.equals(ImapConstants.TEXT)) { 518 parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1)); 519 520 } else { 521 parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1)); 522 } 523 mp.addBodyPart(bp); 524 525 } else { 526 if (e.isString()) { 527 mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US)); 528 } 529 break; // Ignore the rest of the list. 530 } 531 } 532 part.setBody(mp); 533 } else { 534 /* 535 * This is a body. We need to add as much information as we can find out about 536 * it to the Part. 537 */ 538 539 /* 540 body type 541 body subtype 542 body parameter parenthesized list 543 body id 544 body description 545 body encoding 546 body size 547 */ 548 549 final ImapString type = bs.getStringOrEmpty(0); 550 final ImapString subType = bs.getStringOrEmpty(1); 551 final String mimeType = 552 (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US); 553 554 final ImapList bodyParams = bs.getListOrEmpty(2); 555 final ImapString cid = bs.getStringOrEmpty(3); 556 final ImapString encoding = bs.getStringOrEmpty(5); 557 final int size = bs.getStringOrEmpty(6).getNumberOrZero(); 558 559 if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) { 560 // A body type of type MESSAGE and subtype RFC822 561 // contains, immediately after the basic fields, the 562 // envelope structure, body structure, and size in 563 // text lines of the encapsulated message. 564 // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL, 565 // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL] 566 /* 567 * This will be caught by fetch and handled appropriately. 568 */ 569 throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 570 + " not yet supported."); 571 } 572 573 /* 574 * Set the content type with as much information as we know right now. 575 */ 576 final StringBuilder contentType = new StringBuilder(mimeType); 577 578 /* 579 * If there are body params we might be able to get some more information out 580 * of them. 581 */ 582 for (int i = 1, count = bodyParams.size(); i < count; i += 2) { 583 584 // TODO We need to convert " into %22, but 585 // because MimeUtility.getHeaderParameter doesn't recognize it, 586 // we can't fix it for now. 587 contentType.append(String.format(";\n %s=\"%s\"", 588 bodyParams.getStringOrEmpty(i - 1).getString(), 589 bodyParams.getStringOrEmpty(i).getString())); 590 } 591 592 part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString()); 593 594 // Extension items 595 final ImapList bodyDisposition; 596 597 if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) { 598 // If media-type is TEXT, 9th element might be: [body-fld-lines] := number 599 // So, if it's not a list, use 10th element. 600 // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.) 601 bodyDisposition = bs.getListOrEmpty(9); 602 } else { 603 bodyDisposition = bs.getListOrEmpty(8); 604 } 605 606 final StringBuilder contentDisposition = new StringBuilder(); 607 608 if (bodyDisposition.size() > 0) { 609 final String bodyDisposition0Str = 610 bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US); 611 if (!TextUtils.isEmpty(bodyDisposition0Str)) { 612 contentDisposition.append(bodyDisposition0Str); 613 } 614 615 final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1); 616 if (!bodyDispositionParams.isEmpty()) { 617 /* 618 * If there is body disposition information we can pull some more 619 * information about the attachment out. 620 */ 621 for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) { 622 623 // TODO We need to convert " into %22. See above. 624 contentDisposition.append(String.format(Locale.US, ";\n %s=\"%s\"", 625 bodyDispositionParams.getStringOrEmpty(i - 1) 626 .getString().toLowerCase(Locale.US), 627 bodyDispositionParams.getStringOrEmpty(i).getString())); 628 } 629 } 630 } 631 632 if ((size > 0) 633 && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") 634 == null)) { 635 contentDisposition.append(String.format(Locale.US, ";\n size=%d", size)); 636 } 637 638 if (contentDisposition.length() > 0) { 639 /* 640 * Set the content disposition containing at least the size. Attachment 641 * handling code will use this down the road. 642 */ 643 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, 644 contentDisposition.toString()); 645 } 646 647 /* 648 * Set the Content-Transfer-Encoding header. Attachment code will use this 649 * to parse the body. 650 */ 651 if (!encoding.isEmpty()) { 652 part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, 653 encoding.getString()); 654 } 655 656 /* 657 * Set the Content-ID header. 658 */ 659 if (!cid.isEmpty()) { 660 part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString()); 661 } 662 663 if (size > 0) { 664 if (part instanceof ImapMessage) { 665 ((ImapMessage) part).setSize(size); 666 } else if (part instanceof MimeBodyPart) { 667 ((MimeBodyPart) part).setSize(size); 668 } else { 669 throw new MessagingException("Unknown part type " + part.toString()); 670 } 671 } 672 part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); 673 } 674 675 } 676 677 public Message[] expunge() throws MessagingException { 678 checkOpen(); 679 try { 680 handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE)); 681 } catch (IOException ioe) { 682 throw ioExceptionHandler(mConnection, ioe); 683 } finally { 684 destroyResponses(); 685 } 686 return null; 687 } 688 689 public void setFlags(Message[] messages, String[] flags, boolean value) 690 throws MessagingException { 691 checkOpen(); 692 693 String allFlags = ""; 694 if (flags.length > 0) { 695 StringBuilder flagList = new StringBuilder(); 696 for (int i = 0, count = flags.length; i < count; i++) { 697 String flag = flags[i]; 698 if (flag == Flag.SEEN) { 699 flagList.append(" " + ImapConstants.FLAG_SEEN); 700 } else if (flag == Flag.DELETED) { 701 flagList.append(" " + ImapConstants.FLAG_DELETED); 702 } else if (flag == Flag.FLAGGED) { 703 flagList.append(" " + ImapConstants.FLAG_FLAGGED); 704 } else if (flag == Flag.ANSWERED) { 705 flagList.append(" " + ImapConstants.FLAG_ANSWERED); 706 } 707 } 708 allFlags = flagList.substring(1); 709 } 710 try { 711 mConnection.executeSimpleCommand(String.format(Locale.US, 712 ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)", 713 ImapStore.joinMessageUids(messages), 714 value ? "+" : "-", 715 allFlags)); 716 717 } catch (IOException ioe) { 718 throw ioExceptionHandler(mConnection, ioe); 719 } finally { 720 destroyResponses(); 721 } 722 } 723 724 /** 725 * Selects the folder for use. Before performing any operations on this folder, it 726 * must be selected. 727 */ 728 private void doSelect() throws IOException, MessagingException { 729 final List<ImapResponse> responses = mConnection.executeSimpleCommand( 730 String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", mName)); 731 732 // Assume the folder is opened read-write; unless we are notified otherwise 733 mMode = MODE_READ_WRITE; 734 int messageCount = -1; 735 for (ImapResponse response : responses) { 736 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 737 messageCount = response.getStringOrEmpty(0).getNumberOrZero(); 738 } else if (response.isOk()) { 739 final ImapString responseCode = response.getResponseCodeOrEmpty(); 740 if (responseCode.is(ImapConstants.READ_ONLY)) { 741 mMode = MODE_READ_ONLY; 742 } else if (responseCode.is(ImapConstants.READ_WRITE)) { 743 mMode = MODE_READ_WRITE; 744 } 745 } else if (response.isTagged()) { // Not OK 746 throw new MessagingException("Can't open mailbox: " 747 + response.getStatusResponseTextOrEmpty()); 748 } 749 } 750 if (messageCount == -1) { 751 throw new MessagingException("Did not find message count during select"); 752 } 753 mMessageCount = messageCount; 754 mExists = true; 755 } 756 757 private void checkOpen() throws MessagingException { 758 if (!isOpen()) { 759 throw new MessagingException("Folder " + mName + " is not open."); 760 } 761 } 762 763 private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) { 764 LogUtils.d(TAG, "IO Exception detected: ", ioe); 765 connection.close(); 766 if (connection == mConnection) { 767 mConnection = null; // To prevent close() from returning the connection to the pool. 768 close(false); 769 } 770 return new MessagingException(MessagingException.IOERROR, "IO Error", ioe); 771 } 772 773 public Message createMessage(String uid) { 774 return new ImapMessage(uid, this); 775 } 776}