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