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