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_TRUNCATED - 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_TRUNCATED)) { 281 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_TRUNCATED); 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) 367 || fp.contains(FetchProfile.Item.BODY_TRUNCATED)) { 368 // Body is keyed by "BODY[]...". 369 // Previously used "BODY[..." but this can be confused with "BODY[HEADER..." 370 // TODO Should we accept "RFC822" as well?? 371 ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true); 372 InputStream bodyStream = body.getAsStream(); 373 try { 374 message.parse(bodyStream); 375 } catch (Exception e) { 376 VvmLog.e(TAG, "Error parsing body %s", e); 377 } 378 } 379 if (fetchPart != null) { 380 InputStream bodyStream = fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream(); 381 String[] encodings = fetchPart.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING); 382 383 String contentTransferEncoding = null; 384 if (encodings != null && encodings.length > 0) { 385 contentTransferEncoding = encodings[0]; 386 } else { 387 // According to http://tools.ietf.org/html/rfc2045#section-6.1 388 // "7bit" is the default. 389 contentTransferEncoding = "7bit"; 390 } 391 392 try { 393 // TODO Don't create 2 temp files. 394 // decodeBody creates BinaryTempFileBody, but we could avoid this 395 // if we implement ImapStringBody. 396 // (We'll need to share a temp file. Protect it with a ref-count.) 397 message.setBody( 398 decodeBody( 399 store.getContext(), 400 bodyStream, 401 contentTransferEncoding, 402 fetchPart.getSize(), 403 listener)); 404 } catch (Exception e) { 405 // TODO: Figure out what kinds of exceptions might actually be thrown 406 // from here. This blanket catch-all is because we're not sure what to 407 // do if we don't have a contentTransferEncoding, and we don't have 408 // time to figure out what exceptions might be thrown. 409 VvmLog.e(TAG, "Error fetching body %s", e); 410 } 411 } 412 413 if (listener != null) { 414 listener.messageRetrieved(message); 415 } 416 } finally { 417 destroyResponses(); 418 } 419 } while (!response.isTagged()); 420 } catch (IOException ioe) { 421 store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); 422 throw ioExceptionHandler(connection, ioe); 423 } 424 } 425 426 /** 427 * Removes any content transfer encoding from the stream and returns a Body. This code is 428 * taken/condensed from MimeUtility.decodeBody 429 */ decodeBody( Context context, InputStream in, String contentTransferEncoding, int size, MessageRetrievalListener listener)430 private static Body decodeBody( 431 Context context, 432 InputStream in, 433 String contentTransferEncoding, 434 int size, 435 MessageRetrievalListener listener) 436 throws IOException { 437 // Get a properly wrapped input stream 438 in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding); 439 BinaryTempFileBody tempBody = new BinaryTempFileBody(); 440 OutputStream out = tempBody.getOutputStream(); 441 try { 442 byte[] buffer = new byte[COPY_BUFFER_SIZE]; 443 int n = 0; 444 int count = 0; 445 while (-1 != (n = in.read(buffer))) { 446 out.write(buffer, 0, n); 447 count += n; 448 } 449 } catch (Base64DataException bde) { 450 String warning = "\n\nThere was an error while decoding the message."; 451 out.write(warning.getBytes()); 452 } finally { 453 out.close(); 454 } 455 return tempBody; 456 } 457 getPermanentFlags()458 public String[] getPermanentFlags() { 459 return PERMANENT_FLAGS; 460 } 461 462 /** 463 * Handle any untagged responses that the caller doesn't care to handle themselves. 464 * 465 * @param responses 466 */ handleUntaggedResponses(List<ImapResponse> responses)467 private void handleUntaggedResponses(List<ImapResponse> responses) { 468 for (ImapResponse response : responses) { 469 handleUntaggedResponse(response); 470 } 471 } 472 473 /** 474 * Handle an untagged response that the caller doesn't care to handle themselves. 475 * 476 * @param response 477 */ handleUntaggedResponse(ImapResponse response)478 private void handleUntaggedResponse(ImapResponse response) { 479 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 480 messageCount = response.getStringOrEmpty(0).getNumberOrZero(); 481 } 482 } 483 parseBodyStructure(ImapList bs, Part part, String id)484 private static void parseBodyStructure(ImapList bs, Part part, String id) 485 throws MessagingException { 486 if (bs.getElementOrNone(0).isList()) { 487 /* 488 * This is a multipart/* 489 */ 490 MimeMultipart mp = new MimeMultipart(); 491 for (int i = 0, count = bs.size(); i < count; i++) { 492 ImapElement e = bs.getElementOrNone(i); 493 if (e.isList()) { 494 /* 495 * For each part in the message we're going to add a new BodyPart and parse 496 * into it. 497 */ 498 MimeBodyPart bp = new MimeBodyPart(); 499 if (id.equals(ImapConstants.TEXT)) { 500 parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1)); 501 502 } else { 503 parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1)); 504 } 505 mp.addBodyPart(bp); 506 507 } else { 508 if (e.isString()) { 509 mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US)); 510 } 511 break; // Ignore the rest of the list. 512 } 513 } 514 part.setBody(mp); 515 } else { 516 /* 517 * This is a body. We need to add as much information as we can find out about 518 * it to the Part. 519 */ 520 521 /* 522 body type 523 body subtype 524 body parameter parenthesized list 525 body id 526 body description 527 body encoding 528 body size 529 */ 530 531 final ImapString type = bs.getStringOrEmpty(0); 532 final ImapString subType = bs.getStringOrEmpty(1); 533 final String mimeType = (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US); 534 535 final ImapList bodyParams = bs.getListOrEmpty(2); 536 final ImapString cid = bs.getStringOrEmpty(3); 537 final ImapString encoding = bs.getStringOrEmpty(5); 538 final int size = bs.getStringOrEmpty(6).getNumberOrZero(); 539 540 if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) { 541 // A body type of type MESSAGE and subtype RFC822 542 // contains, immediately after the basic fields, the 543 // envelope structure, body structure, and size in 544 // text lines of the encapsulated message. 545 // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL, 546 // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL] 547 /* 548 * This will be caught by fetch and handled appropriately. 549 */ 550 throw new MessagingException( 551 "BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 + " not yet supported."); 552 } 553 554 /* 555 * Set the content type with as much information as we know right now. 556 */ 557 final StringBuilder contentType = new StringBuilder(mimeType); 558 559 /* 560 * If there are body params we might be able to get some more information out 561 * of them. 562 */ 563 for (int i = 1, count = bodyParams.size(); i < count; i += 2) { 564 565 // TODO We need to convert " into %22, but 566 // because MimeUtility.getHeaderParameter doesn't recognize it, 567 // we can't fix it for now. 568 contentType.append( 569 String.format( 570 ";\n %s=\"%s\"", 571 bodyParams.getStringOrEmpty(i - 1).getString(), 572 bodyParams.getStringOrEmpty(i).getString())); 573 } 574 575 part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString()); 576 577 // Extension items 578 final ImapList bodyDisposition; 579 580 if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) { 581 // If media-type is TEXT, 9th element might be: [body-fld-lines] := number 582 // So, if it's not a list, use 10th element. 583 // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.) 584 bodyDisposition = bs.getListOrEmpty(9); 585 } else { 586 bodyDisposition = bs.getListOrEmpty(8); 587 } 588 589 final StringBuilder contentDisposition = new StringBuilder(); 590 591 if (bodyDisposition.size() > 0) { 592 final String bodyDisposition0Str = 593 bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US); 594 if (!TextUtils.isEmpty(bodyDisposition0Str)) { 595 contentDisposition.append(bodyDisposition0Str); 596 } 597 598 final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1); 599 if (!bodyDispositionParams.isEmpty()) { 600 /* 601 * If there is body disposition information we can pull some more 602 * information about the attachment out. 603 */ 604 for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) { 605 606 // TODO We need to convert " into %22. See above. 607 contentDisposition.append( 608 String.format( 609 Locale.US, 610 ";\n %s=\"%s\"", 611 bodyDispositionParams 612 .getStringOrEmpty(i - 1) 613 .getString() 614 .toLowerCase(Locale.US), 615 bodyDispositionParams.getStringOrEmpty(i).getString())); 616 } 617 } 618 } 619 620 if ((size > 0) 621 && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") == null)) { 622 contentDisposition.append(String.format(Locale.US, ";\n size=%d", size)); 623 } 624 625 if (contentDisposition.length() > 0) { 626 /* 627 * Set the content disposition containing at least the size. Attachment 628 * handling code will use this down the road. 629 */ 630 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, 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, encoding.getString()); 639 } 640 641 /* 642 * Set the Content-ID header. 643 */ 644 if (!cid.isEmpty()) { 645 part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString()); 646 } 647 648 if (size > 0) { 649 if (part instanceof ImapMessage) { 650 ((ImapMessage) part).setSize(size); 651 } else if (part instanceof MimeBodyPart) { 652 ((MimeBodyPart) part).setSize(size); 653 } else { 654 throw new MessagingException("Unknown part type " + part.toString()); 655 } 656 } 657 part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); 658 } 659 } 660 expunge()661 public Message[] expunge() throws MessagingException { 662 checkOpen(); 663 try { 664 handleUntaggedResponses(connection.executeSimpleCommand(ImapConstants.EXPUNGE)); 665 } catch (IOException ioe) { 666 store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); 667 throw ioExceptionHandler(connection, ioe); 668 } finally { 669 destroyResponses(); 670 } 671 return null; 672 } 673 setFlags(Message[] messages, String[] flags, boolean value)674 public void setFlags(Message[] messages, String[] flags, boolean value) 675 throws MessagingException { 676 checkOpen(); 677 678 String allFlags = ""; 679 if (flags.length > 0) { 680 StringBuilder flagList = new StringBuilder(); 681 for (int i = 0, count = flags.length; i < count; i++) { 682 String flag = flags[i]; 683 if (flag == Flag.SEEN) { 684 flagList.append(" " + ImapConstants.FLAG_SEEN); 685 } else if (flag == Flag.DELETED) { 686 flagList.append(" " + ImapConstants.FLAG_DELETED); 687 } else if (flag == Flag.FLAGGED) { 688 flagList.append(" " + ImapConstants.FLAG_FLAGGED); 689 } else if (flag == Flag.ANSWERED) { 690 flagList.append(" " + ImapConstants.FLAG_ANSWERED); 691 } 692 } 693 allFlags = flagList.substring(1); 694 } 695 try { 696 connection.executeSimpleCommand( 697 String.format( 698 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 store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); 706 throw ioExceptionHandler(connection, ioe); 707 } finally { 708 destroyResponses(); 709 } 710 } 711 712 /** 713 * Selects the folder for use. Before performing any operations on this folder, it must be 714 * selected. 715 */ doSelect()716 private void doSelect() throws IOException, MessagingException { 717 final List<ImapResponse> responses = 718 connection.executeSimpleCommand( 719 String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", name)); 720 721 // Assume the folder is opened read-write; unless we are notified otherwise 722 mode = MODE_READ_WRITE; 723 int messageCount = -1; 724 for (ImapResponse response : responses) { 725 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 726 messageCount = response.getStringOrEmpty(0).getNumberOrZero(); 727 } else if (response.isOk()) { 728 final ImapString responseCode = response.getResponseCodeOrEmpty(); 729 if (responseCode.is(ImapConstants.READ_ONLY)) { 730 mode = MODE_READ_ONLY; 731 } else if (responseCode.is(ImapConstants.READ_WRITE)) { 732 mode = MODE_READ_WRITE; 733 } 734 } else if (response.isTagged()) { // Not OK 735 store.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED); 736 throw new MessagingException( 737 "Can't open mailbox: " + response.getStatusResponseTextOrEmpty()); 738 } 739 } 740 if (messageCount == -1) { 741 throw new MessagingException("Did not find message count during select"); 742 } 743 this.messageCount = messageCount; 744 exists = true; 745 } 746 747 public class Quota { 748 749 public final int occupied; 750 public final int total; 751 Quota(int occupied, int total)752 public Quota(int occupied, int total) { 753 this.occupied = occupied; 754 this.total = total; 755 } 756 } 757 getQuota()758 public Quota getQuota() throws MessagingException { 759 try { 760 final List<ImapResponse> responses = 761 connection.executeSimpleCommand( 762 String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", name)); 763 764 for (ImapResponse response : responses) { 765 if (!response.isDataResponse(0, ImapConstants.QUOTA)) { 766 continue; 767 } 768 ImapList list = response.getListOrEmpty(2); 769 for (int i = 0; i < list.size(); i += 3) { 770 if (!list.getStringOrEmpty(i).is("voice")) { 771 continue; 772 } 773 return new Quota( 774 list.getStringOrEmpty(i + 1).getNumber(-1), 775 list.getStringOrEmpty(i + 2).getNumber(-1)); 776 } 777 } 778 } catch (IOException ioe) { 779 store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); 780 throw ioExceptionHandler(connection, ioe); 781 } finally { 782 destroyResponses(); 783 } 784 return null; 785 } 786 checkOpen()787 private void checkOpen() throws MessagingException { 788 if (!isOpen()) { 789 throw new MessagingException("Folder " + name + " is not open."); 790 } 791 } 792 ioExceptionHandler(ImapConnection connection, IOException ioe)793 private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) { 794 VvmLog.d(TAG, "IO Exception detected: ", ioe); 795 connection.close(); 796 if (connection == this.connection) { 797 this.connection = null; // To prevent close() from returning the connection to the pool. 798 close(false); 799 } 800 return new MessagingException(MessagingException.IOERROR, "IO Error", ioe); 801 } 802 createMessage(String uid)803 public Message createMessage(String uid) { 804 return new ImapMessage(uid, this); 805 } 806 } 807