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