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 store; 63 private final String name; 64 private int messageCount = -1; 65 private ImapConnection connection; 66 private String mode; 67 private boolean exists; 68 /** A set of hashes that can be used to track dirtiness */ 69 Object[] hash; 70 71 public static final String MODE_READ_ONLY = "mode_read_only"; 72 public static final String MODE_READ_WRITE = "mode_read_write"; 73 ImapFolder(ImapStore store, String name)74 public ImapFolder(ImapStore store, String name) { 75 this.store = store; 76 this.name = name; 77 } 78 79 /** Callback for each message retrieval. */ 80 public interface MessageRetrievalListener { messageRetrieved(Message message)81 public void messageRetrieved(Message message); 82 } 83 destroyResponses()84 private void destroyResponses() { 85 if (connection != null) { 86 connection.destroyResponses(); 87 } 88 } 89 open(String mode)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 connection = store.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(connection, 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 connection = null; 117 close(false); 118 throw e; 119 } catch (MessagingException e) { 120 exists = false; 121 close(false); 122 throw e; 123 } 124 } 125 isOpen()126 public boolean isOpen() { 127 return exists && connection != null; 128 } 129 getMode()130 public String getMode() { 131 return mode; 132 } 133 close(boolean expunge)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 messageCount = -1; 143 synchronized (this) { 144 connection = null; 145 } 146 } 147 getMessageCount()148 public int getMessageCount() { 149 return messageCount; 150 } 151 getSearchUids(List<ImapResponse> responses)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 searchForUids(String searchCriteria)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(connection.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 store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); 185 throw ioExceptionHandler(connection, ioe); 186 } 187 } finally { 188 destroyResponses(); 189 } 190 } 191 192 @Nullable getMessage(String uid)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 isAsciiString(String str)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 getMessages(String[] uids)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 getMessagesInternal(String[] uids)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 fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)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 fetchInternal(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)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 connection.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 = connection.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 store.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 store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); 421 throw ioExceptionHandler(connection, 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 */ decodeBody( Context context, InputStream in, String contentTransferEncoding, int size, MessageRetrievalListener listener)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 getPermanentFlags()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 */ handleUntaggedResponses(List<ImapResponse> responses)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 */ handleUntaggedResponse(ImapResponse response)477 private void handleUntaggedResponse(ImapResponse response) { 478 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 479 messageCount = response.getStringOrEmpty(0).getNumberOrZero(); 480 } 481 } 482 parseBodyStructure(ImapList bs, Part part, String id)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 expunge()660 public Message[] expunge() throws MessagingException { 661 checkOpen(); 662 try { 663 handleUntaggedResponses(connection.executeSimpleCommand(ImapConstants.EXPUNGE)); 664 } catch (IOException ioe) { 665 store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); 666 throw ioExceptionHandler(connection, ioe); 667 } finally { 668 destroyResponses(); 669 } 670 return null; 671 } 672 setFlags(Message[] messages, String[] flags, boolean value)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 connection.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 store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); 705 throw ioExceptionHandler(connection, 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 */ doSelect()715 private void doSelect() throws IOException, MessagingException { 716 final List<ImapResponse> responses = 717 connection.executeSimpleCommand( 718 String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", name)); 719 720 // Assume the folder is opened read-write; unless we are notified otherwise 721 mode = 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 mode = MODE_READ_ONLY; 730 } else if (responseCode.is(ImapConstants.READ_WRITE)) { 731 mode = MODE_READ_WRITE; 732 } 733 } else if (response.isTagged()) { // Not OK 734 store.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 this.messageCount = messageCount; 743 exists = true; 744 } 745 746 public class Quota { 747 748 public final int occupied; 749 public final int total; 750 Quota(int occupied, int total)751 public Quota(int occupied, int total) { 752 this.occupied = occupied; 753 this.total = total; 754 } 755 } 756 getQuota()757 public Quota getQuota() throws MessagingException { 758 try { 759 final List<ImapResponse> responses = 760 connection.executeSimpleCommand( 761 String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", name)); 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 store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); 779 throw ioExceptionHandler(connection, ioe); 780 } finally { 781 destroyResponses(); 782 } 783 return null; 784 } 785 checkOpen()786 private void checkOpen() throws MessagingException { 787 if (!isOpen()) { 788 throw new MessagingException("Folder " + name + " is not open."); 789 } 790 } 791 ioExceptionHandler(ImapConnection connection, IOException ioe)792 private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) { 793 VvmLog.d(TAG, "IO Exception detected: ", ioe); 794 connection.close(); 795 if (connection == this.connection) { 796 this.connection = 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 createMessage(String uid)802 public Message createMessage(String uid) { 803 return new ImapMessage(uid, this); 804 } 805 } 806