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