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