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