1 /* 2 * Copyright (C) 2008 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 17 package com.android.email.mail.store; 18 19 import android.content.Context; 20 import android.os.Build; 21 import android.os.Bundle; 22 import android.telephony.TelephonyManager; 23 import android.text.TextUtils; 24 import android.util.Base64; 25 26 import com.android.email.LegacyConversions; 27 import com.android.email.Preferences; 28 import com.android.email.mail.Store; 29 import com.android.email.mail.store.imap.ImapConstants; 30 import com.android.email.mail.store.imap.ImapResponse; 31 import com.android.email.mail.store.imap.ImapString; 32 import com.android.email.mail.transport.MailTransport; 33 import com.android.emailcommon.Logging; 34 import com.android.emailcommon.VendorPolicyLoader; 35 import com.android.emailcommon.internet.MimeMessage; 36 import com.android.emailcommon.mail.AuthenticationFailedException; 37 import com.android.emailcommon.mail.Flag; 38 import com.android.emailcommon.mail.Folder; 39 import com.android.emailcommon.mail.Message; 40 import com.android.emailcommon.mail.MessagingException; 41 import com.android.emailcommon.provider.Account; 42 import com.android.emailcommon.provider.Credential; 43 import com.android.emailcommon.provider.EmailContent; 44 import com.android.emailcommon.provider.HostAuth; 45 import com.android.emailcommon.provider.Mailbox; 46 import com.android.emailcommon.service.EmailServiceProxy; 47 import com.android.emailcommon.utility.Utility; 48 import com.android.mail.utils.LogUtils; 49 import com.beetstra.jutf7.CharsetProvider; 50 import com.google.common.annotations.VisibleForTesting; 51 52 import java.io.IOException; 53 import java.io.InputStream; 54 import java.nio.ByteBuffer; 55 import java.nio.charset.Charset; 56 import java.security.MessageDigest; 57 import java.security.NoSuchAlgorithmException; 58 import java.util.Collection; 59 import java.util.HashMap; 60 import java.util.List; 61 import java.util.Set; 62 import java.util.concurrent.ConcurrentLinkedQueue; 63 import java.util.regex.Pattern; 64 65 66 /** 67 * <pre> 68 * TODO Need to start keeping track of UIDVALIDITY 69 * TODO Need a default response handler for things like folder updates 70 * TODO In fetch(), if we need a ImapMessage and were given 71 * something else we can try to do a pre-fetch first. 72 * TODO Collect ALERT messages and show them to users. 73 * 74 * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for 75 * certain information in a FETCH command, the server may return the requested 76 * information in any order, not necessarily in the order that it was requested. 77 * Further, the server may return the information in separate FETCH responses 78 * and may also return information that was not explicitly requested (to reflect 79 * to the client changes in the state of the subject message). 80 * </pre> 81 */ 82 public class ImapStore extends Store { 83 /** Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. */ 84 private static final Charset MODIFIED_UTF_7_CHARSET = 85 new CharsetProvider().charsetForName("X-RFC-3501"); 86 87 @VisibleForTesting static String sImapId = null; 88 @VisibleForTesting String mPathPrefix; 89 @VisibleForTesting String mPathSeparator; 90 91 private boolean mUseOAuth; 92 93 private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool = 94 new ConcurrentLinkedQueue<ImapConnection>(); 95 96 /** 97 * Static named constructor. 98 */ 99 public static Store newInstance(Account account, Context context) throws MessagingException { 100 return new ImapStore(context, account); 101 } 102 103 /** 104 * Creates a new store for the given account. Always use 105 * {@link #newInstance(Account, Context)} to create an IMAP store. 106 */ 107 private ImapStore(Context context, Account account) throws MessagingException { 108 mContext = context; 109 mAccount = account; 110 111 HostAuth recvAuth = account.getOrCreateHostAuthRecv(context); 112 if (recvAuth == null) { 113 throw new MessagingException("No HostAuth in ImapStore?"); 114 } 115 mTransport = new MailTransport(context, "IMAP", recvAuth); 116 117 String[] userInfo = recvAuth.getLogin(); 118 mUsername = userInfo[0]; 119 mPassword = userInfo[1]; 120 final Credential cred = recvAuth.getCredential(context); 121 mUseOAuth = (cred != null); 122 mPathPrefix = recvAuth.mDomain; 123 } 124 125 boolean getUseOAuth() { 126 return mUseOAuth; 127 } 128 129 String getUsername() { 130 return mUsername; 131 } 132 133 String getPassword() { 134 return mPassword; 135 } 136 137 public boolean canSyncFolderType(final int type) { 138 switch (type) { 139 case Mailbox.TYPE_INBOX: 140 case Mailbox.TYPE_MAIL: 141 case Mailbox.TYPE_SENT: 142 case Mailbox.TYPE_TRASH: 143 case Mailbox.TYPE_JUNK: 144 return true; 145 case Mailbox.TYPE_NONE: 146 case Mailbox.TYPE_PARENT: 147 case Mailbox.TYPE_DRAFTS: 148 case Mailbox.TYPE_OUTBOX: 149 case Mailbox.TYPE_SEARCH: 150 case Mailbox.TYPE_STARRED: 151 case Mailbox.TYPE_UNREAD: 152 default: 153 return false; 154 } 155 } 156 157 @VisibleForTesting 158 Collection<ImapConnection> getConnectionPoolForTest() { 159 return mConnectionPool; 160 } 161 162 /** 163 * For testing only. Injects a different root transport (it will be copied using 164 * newInstanceWithConfiguration() each time IMAP sets up a new channel). The transport 165 * should already be set up and ready to use. Do not use for real code. 166 * @param testTransport The Transport to inject and use for all future communication. 167 */ 168 @VisibleForTesting 169 void setTransportForTest(MailTransport testTransport) { 170 mTransport = testTransport; 171 } 172 173 /** 174 * Return, or create and return, an string suitable for use in an IMAP ID message. 175 * This is constructed similarly to the way the browser sets up its user-agent strings. 176 * See RFC 2971 for more details. The output of this command will be a series of key-value 177 * pairs delimited by spaces (there is no point in returning a structured result because 178 * this will be sent as-is to the IMAP server). No tokens, parenthesis or "ID" are included, 179 * because some connections may append additional values. 180 * 181 * The following IMAP ID keys may be included: 182 * name Android package name of the program 183 * os "android" 184 * os-version "version; model; build-id" 185 * vendor Vendor of the client/server 186 * x-android-device-model Model (only revealed if release build) 187 * x-android-net-operator Mobile network operator (if known) 188 * AGUID A device+account UID 189 * 190 * In addition, a vendor policy .apk can append key/value pairs. 191 * 192 * @param userName the username of the account 193 * @param host the host (server) of the account 194 * @param capabilities a list of the capabilities from the server 195 * @return a String for use in an IMAP ID message. 196 */ 197 public static String getImapId(Context context, String userName, String host, 198 String capabilities) { 199 // The first section is global to all IMAP connections, and generates the fixed 200 // values in any IMAP ID message 201 synchronized (ImapStore.class) { 202 if (sImapId == null) { 203 TelephonyManager tm = 204 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 205 String networkOperator = tm.getNetworkOperatorName(); 206 if (networkOperator == null) networkOperator = ""; 207 208 sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE, 209 Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER, 210 networkOperator); 211 } 212 } 213 214 // This section is per Store, and adds in a dynamic elements like UID's. 215 // We don't cache the result of this work, because the caller does anyway. 216 StringBuilder id = new StringBuilder(sImapId); 217 218 // Optionally add any vendor-supplied id keys 219 String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host, 220 capabilities); 221 if (vendorId != null) { 222 id.append(' '); 223 id.append(vendorId); 224 } 225 226 // Generate a UID that mixes a "stable" device UID with the email address 227 try { 228 String devUID = Preferences.getPreferences(context).getDeviceUID(); 229 MessageDigest messageDigest; 230 messageDigest = MessageDigest.getInstance("SHA-1"); 231 messageDigest.update(userName.getBytes()); 232 messageDigest.update(devUID.getBytes()); 233 byte[] uid = messageDigest.digest(); 234 String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP); 235 id.append(" \"AGUID\" \""); 236 id.append(hexUid); 237 id.append('\"'); 238 } catch (NoSuchAlgorithmException e) { 239 LogUtils.d(Logging.LOG_TAG, "couldn't obtain SHA-1 hash for device UID"); 240 } 241 return id.toString(); 242 } 243 244 /** 245 * Helper function that actually builds the static part of the IMAP ID string. This is 246 * separated from getImapId for testability. There is no escaping or encoding in IMAP ID so 247 * any rogue chars must be filtered here. 248 * 249 * @param packageName context.getPackageName() 250 * @param version Build.VERSION.RELEASE 251 * @param codeName Build.VERSION.CODENAME 252 * @param model Build.MODEL 253 * @param id Build.ID 254 * @param vendor Build.MANUFACTURER 255 * @param networkOperator TelephonyManager.getNetworkOperatorName() 256 * @return the static (never changes) portion of the IMAP ID 257 */ 258 @VisibleForTesting 259 static String makeCommonImapId(String packageName, String version, 260 String codeName, String model, String id, String vendor, String networkOperator) { 261 262 // Before building up IMAP ID string, pre-filter the input strings for "legal" chars 263 // This is using a fairly arbitrary char set intended to pass through most reasonable 264 // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space> 265 // The most important thing is *not* to pass parens, quotes, or CRLF, which would break 266 // the format of the IMAP ID list. 267 Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]"); 268 packageName = p.matcher(packageName).replaceAll(""); 269 version = p.matcher(version).replaceAll(""); 270 codeName = p.matcher(codeName).replaceAll(""); 271 model = p.matcher(model).replaceAll(""); 272 id = p.matcher(id).replaceAll(""); 273 vendor = p.matcher(vendor).replaceAll(""); 274 networkOperator = p.matcher(networkOperator).replaceAll(""); 275 276 // "name" "com.android.email" 277 StringBuilder sb = new StringBuilder("\"name\" \""); 278 sb.append(packageName); 279 sb.append("\""); 280 281 // "os" "android" 282 sb.append(" \"os\" \"android\""); 283 284 // "os-version" "version; build-id" 285 sb.append(" \"os-version\" \""); 286 if (version.length() > 0) { 287 sb.append(version); 288 } else { 289 // default to "1.0" 290 sb.append("1.0"); 291 } 292 // add the build ID or build # 293 if (id.length() > 0) { 294 sb.append("; "); 295 sb.append(id); 296 } 297 sb.append("\""); 298 299 // "vendor" "the vendor" 300 if (vendor.length() > 0) { 301 sb.append(" \"vendor\" \""); 302 sb.append(vendor); 303 sb.append("\""); 304 } 305 306 // "x-android-device-model" the device model (on release builds only) 307 if ("REL".equals(codeName)) { 308 if (model.length() > 0) { 309 sb.append(" \"x-android-device-model\" \""); 310 sb.append(model); 311 sb.append("\""); 312 } 313 } 314 315 // "x-android-mobile-net-operator" "name of network operator" 316 if (networkOperator.length() > 0) { 317 sb.append(" \"x-android-mobile-net-operator\" \""); 318 sb.append(networkOperator); 319 sb.append("\""); 320 } 321 322 return sb.toString(); 323 } 324 325 326 @Override 327 public Folder getFolder(String name) { 328 return new ImapFolder(this, name); 329 } 330 331 /** 332 * Creates a mailbox hierarchy out of the flat data provided by the server. 333 */ 334 @VisibleForTesting 335 static void createHierarchy(HashMap<String, ImapFolder> mailboxes) { 336 Set<String> pathnames = mailboxes.keySet(); 337 for (String path : pathnames) { 338 final ImapFolder folder = mailboxes.get(path); 339 final Mailbox mailbox = folder.mMailbox; 340 int delimiterIdx = mailbox.mServerId.lastIndexOf(mailbox.mDelimiter); 341 long parentKey = Mailbox.NO_MAILBOX; 342 String parentPath = null; 343 if (delimiterIdx != -1) { 344 parentPath = path.substring(0, delimiterIdx); 345 if (ImapConstants.INBOX.equalsIgnoreCase(parentPath)) { 346 // The Inbox is added as a special case, and always in all caps. In reality, 347 // it might not be in all caps, this folder's parent path might have mixed case. 348 parentPath = ImapConstants.INBOX; 349 } 350 final ImapFolder parentFolder = mailboxes.get(parentPath); 351 final Mailbox parentMailbox = (parentFolder == null) ? null : parentFolder.mMailbox; 352 if (parentMailbox != null) { 353 parentKey = parentMailbox.mId; 354 parentMailbox.mFlags 355 |= (Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE); 356 } 357 } 358 mailbox.mParentKey = parentKey; 359 mailbox.mParentServerId = parentPath; 360 } 361 } 362 363 /** 364 * Creates a {@link Folder} and associated {@link Mailbox}. If the folder does not already 365 * exist in the local database, a new row will immediately be created in the mailbox table. 366 * Otherwise, the existing row will be used. Any changes to existing rows, will not be stored 367 * to the database immediately. 368 * @param accountId The ID of the account the mailbox is to be associated with 369 * @param mailboxPath The path of the mailbox to add 370 * @param delimiter A path delimiter. May be {@code null} if there is no delimiter. 371 * @param selectable If {@code true}, the mailbox can be selected and used to store messages. 372 * @param mailbox If not null, mailbox is used instead of querying for the Mailbox. 373 */ 374 private ImapFolder addMailbox(Context context, long accountId, String mailboxPath, 375 char delimiter, boolean selectable, Mailbox mailbox) { 376 // TODO: pass in the mailbox type, or do a proper lookup here 377 final int mailboxType; 378 if (mailbox == null) { 379 mailboxType = LegacyConversions.inferMailboxTypeFromName(context, mailboxPath); 380 mailbox = Mailbox.getMailboxForPath(context, accountId, mailboxPath); 381 } else { 382 mailboxType = mailbox.mType; 383 } 384 final ImapFolder folder = (ImapFolder) getFolder(mailboxPath); 385 if (mailbox.isSaved()) { 386 // existing mailbox 387 // mailbox retrieved from database; save hash _before_ updating fields 388 folder.mHash = mailbox.getHashes(); 389 } 390 updateMailbox(mailbox, accountId, mailboxPath, delimiter, selectable, mailboxType); 391 if (folder.mHash == null) { 392 // new mailbox 393 // save hash after updating. allows tracking changes if the mailbox is saved 394 // outside of #saveMailboxList() 395 folder.mHash = mailbox.getHashes(); 396 // We must save this here to make sure we have a valid ID for later 397 398 // This is a newly created folder from the server. By definition, if it came from 399 // the server, it can be synched. We need to set the uiSyncStatus so that the UI 400 // will not try to display the empty state until the sync completes. 401 mailbox.mUiSyncStatus = EmailContent.SYNC_STATUS_INITIAL_SYNC_NEEDED; 402 mailbox.save(mContext); 403 } 404 folder.mMailbox = mailbox; 405 return folder; 406 } 407 408 /** 409 * Persists the folders in the given list. 410 */ 411 private static void saveMailboxList(Context context, HashMap<String, ImapFolder> folderMap) { 412 for (ImapFolder imapFolder : folderMap.values()) { 413 imapFolder.save(context); 414 } 415 } 416 417 @Override 418 public Folder[] updateFolders() throws MessagingException { 419 // TODO: There is nothing that ever closes this connection. Trouble is, it's not exactly 420 // clear when we should close it, we'd like to keep it open until we're really done 421 // using it. 422 ImapConnection connection = getConnection(); 423 try { 424 final HashMap<String, ImapFolder> mailboxes = new HashMap<String, ImapFolder>(); 425 // Establish a connection to the IMAP server; if necessary 426 // This ensures a valid prefix if the prefix is automatically set by the server 427 connection.executeSimpleCommand(ImapConstants.NOOP); 428 String imapCommand = ImapConstants.LIST + " \"\" \"*\""; 429 if (mPathPrefix != null) { 430 imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\""; 431 } 432 List<ImapResponse> responses = connection.executeSimpleCommand(imapCommand); 433 for (ImapResponse response : responses) { 434 // S: * LIST (\Noselect) "/" ~/Mail/foo 435 if (response.isDataResponse(0, ImapConstants.LIST)) { 436 // Get folder name. 437 ImapString encodedFolder = response.getStringOrEmpty(3); 438 if (encodedFolder.isEmpty()) continue; 439 440 String folderName = decodeFolderName(encodedFolder.getString(), mPathPrefix); 441 442 if (ImapConstants.INBOX.equalsIgnoreCase(folderName)) continue; 443 444 // Parse attributes. 445 boolean selectable = 446 !response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT); 447 String delimiter = response.getStringOrEmpty(2).getString(); 448 char delimiterChar = '\0'; 449 if (!TextUtils.isEmpty(delimiter)) { 450 delimiterChar = delimiter.charAt(0); 451 } 452 ImapFolder folder = addMailbox( 453 mContext, mAccount.mId, folderName, delimiterChar, selectable, null); 454 mailboxes.put(folderName, folder); 455 } 456 } 457 458 // In order to properly map INBOX -> Inbox, handle it as a special case. 459 final Mailbox inbox = 460 Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX); 461 final ImapFolder newFolder = addMailbox( 462 mContext, mAccount.mId, inbox.mServerId, '\0', true /*selectable*/, inbox); 463 mailboxes.put(ImapConstants.INBOX, newFolder); 464 465 createHierarchy(mailboxes); 466 saveMailboxList(mContext, mailboxes); 467 return mailboxes.values().toArray(new Folder[mailboxes.size()]); 468 } catch (IOException ioe) { 469 connection.close(); 470 throw new MessagingException("Unable to get folder list", ioe); 471 } catch (AuthenticationFailedException afe) { 472 // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT 473 // commands to the server 474 connection.destroyResponses(); 475 connection = null; 476 throw afe; 477 } finally { 478 if (connection != null) { 479 // We keep our connection out of the pool as long as we are using it, then 480 // put it back into the pool so it can be reused. 481 poolConnection(connection); 482 } 483 } 484 } 485 486 @Override 487 public Bundle checkSettings() throws MessagingException { 488 int result = MessagingException.NO_ERROR; 489 Bundle bundle = new Bundle(); 490 // TODO: why doesn't this use getConnection()? I guess this is only done during setup, 491 // so there's need to look for a pooled connection? 492 // But then why doesn't it use poolConnection() after it's done? 493 ImapConnection connection = new ImapConnection(this); 494 try { 495 connection.open(); 496 connection.close(); 497 } catch (IOException ioe) { 498 bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage()); 499 result = MessagingException.IOERROR; 500 } finally { 501 connection.destroyResponses(); 502 } 503 bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); 504 return bundle; 505 } 506 507 /** 508 * Returns whether or not the prefix has been set by the user. This can be determined by 509 * the fact that the prefix is set, but, the path separator is not set. 510 */ 511 boolean isUserPrefixSet() { 512 return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix); 513 } 514 515 /** Sets the path separator */ 516 void setPathSeparator(String pathSeparator) { 517 mPathSeparator = pathSeparator; 518 } 519 520 /** Sets the prefix */ 521 void setPathPrefix(String pathPrefix) { 522 mPathPrefix = pathPrefix; 523 } 524 525 /** Gets the context for this store */ 526 Context getContext() { 527 return mContext; 528 } 529 530 /** Returns a clone of the transport associated with this store. */ 531 MailTransport cloneTransport() { 532 return mTransport.clone(); 533 } 534 535 /** 536 * Fixes the path prefix, if necessary. The path prefix must always end with the 537 * path separator. 538 */ 539 void ensurePrefixIsValid() { 540 // Make sure the path prefix ends with the path separator 541 if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) { 542 if (!mPathPrefix.endsWith(mPathSeparator)) { 543 mPathPrefix = mPathPrefix + mPathSeparator; 544 } 545 } 546 } 547 548 /** 549 * Gets a connection if one is available from the pool, or creates a new one if not. 550 */ 551 ImapConnection getConnection() { 552 // TODO Why would we ever have (or need to have) more than one active connection? 553 // TODO We set new username/password each time, but we don't actually close the transport 554 // when we do this. So if that information has changed, this connection will fail. 555 ImapConnection connection; 556 while ((connection = mConnectionPool.poll()) != null) { 557 try { 558 connection.setStore(this); 559 connection.executeSimpleCommand(ImapConstants.NOOP); 560 break; 561 } catch (MessagingException e) { 562 // Fall through 563 } catch (IOException e) { 564 // Fall through 565 } 566 connection.close(); 567 } 568 569 if (connection == null) { 570 connection = new ImapConnection(this); 571 } 572 return connection; 573 } 574 575 /** 576 * Save a {@link ImapConnection} in the pool for reuse. Any responses associated with the 577 * connection are destroyed before adding the connection to the pool. 578 */ 579 void poolConnection(ImapConnection connection) { 580 if (connection != null) { 581 connection.destroyResponses(); 582 mConnectionPool.add(connection); 583 } 584 } 585 586 /** 587 * Prepends the folder name with the given prefix and UTF-7 encodes it. 588 */ 589 static String encodeFolderName(String name, String prefix) { 590 // do NOT add the prefix to the special name "INBOX" 591 if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name; 592 593 // Prepend prefix 594 if (prefix != null) { 595 name = prefix + name; 596 } 597 598 // TODO bypass the conversion if name doesn't have special char. 599 ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name); 600 byte[] b = new byte[bb.limit()]; 601 bb.get(b); 602 603 return Utility.fromAscii(b); 604 } 605 606 /** 607 * UTF-7 decodes the folder name and removes the given path prefix. 608 */ 609 static String decodeFolderName(String name, String prefix) { 610 // TODO bypass the conversion if name doesn't have special char. 611 String folder; 612 folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString(); 613 if ((prefix != null) && folder.startsWith(prefix)) { 614 folder = folder.substring(prefix.length()); 615 } 616 return folder; 617 } 618 619 /** 620 * Returns UIDs of Messages joined with "," as the separator. 621 */ 622 static String joinMessageUids(Message[] messages) { 623 StringBuilder sb = new StringBuilder(); 624 boolean notFirst = false; 625 for (Message m : messages) { 626 if (notFirst) { 627 sb.append(','); 628 } 629 sb.append(m.getUid()); 630 notFirst = true; 631 } 632 return sb.toString(); 633 } 634 635 static class ImapMessage extends MimeMessage { 636 ImapMessage(String uid, ImapFolder folder) { 637 mUid = uid; 638 mFolder = folder; 639 } 640 641 public void setSize(int size) { 642 mSize = size; 643 } 644 645 @Override 646 public void parse(InputStream in) throws IOException, MessagingException { 647 super.parse(in); 648 } 649 650 public void setFlagInternal(Flag flag, boolean set) throws MessagingException { 651 super.setFlag(flag, set); 652 } 653 654 @Override 655 public void setFlag(Flag flag, boolean set) throws MessagingException { 656 super.setFlag(flag, set); 657 mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); 658 } 659 } 660 661 static class ImapException extends MessagingException { 662 private static final long serialVersionUID = 1L; 663 664 private final String mStatus; 665 private final String mAlertText; 666 private final String mResponseCode; 667 668 public ImapException(String message, String status, String alertText, 669 String responseCode) { 670 super(message); 671 mStatus = status; 672 mAlertText = alertText; 673 mResponseCode = responseCode; 674 } 675 676 public String getStatus() { 677 return mStatus; 678 } 679 680 public String getAlertText() { 681 return mAlertText; 682 } 683 684 public String getResponseCode() { 685 return mResponseCode; 686 } 687 } 688 689 public void closeConnections() { 690 ImapConnection connection; 691 while ((connection = mConnectionPool.poll()) != null) { 692 connection.close(); 693 } 694 } 695 } 696