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 */ newInstance(Account account, Context context)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 */ ImapStore(Context context, Account account)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 getUseOAuth()125 boolean getUseOAuth() { 126 return mUseOAuth; 127 } 128 getUsername()129 String getUsername() { 130 return mUsername; 131 } 132 getPassword()133 String getPassword() { 134 return mPassword; 135 } 136 canSyncFolderType(final int type)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 getConnectionPoolForTest()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 setTransportForTest(MailTransport testTransport)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 */ getImapId(Context context, String userName, String host, String capabilities)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 makeCommonImapId(String packageName, String version, String codeName, String model, String id, String vendor, String networkOperator)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 getFolder(String name)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 createHierarchy(HashMap<String, ImapFolder> mailboxes)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 */ addMailbox(Context context, long accountId, String mailboxPath, char delimiter, boolean selectable, Mailbox mailbox)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 */ saveMailboxList(Context context, HashMap<String, ImapFolder> folderMap)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 updateFolders()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 checkSettings()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 */ isUserPrefixSet()511 boolean isUserPrefixSet() { 512 return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix); 513 } 514 515 /** Sets the path separator */ setPathSeparator(String pathSeparator)516 void setPathSeparator(String pathSeparator) { 517 mPathSeparator = pathSeparator; 518 } 519 520 /** Sets the prefix */ setPathPrefix(String pathPrefix)521 void setPathPrefix(String pathPrefix) { 522 mPathPrefix = pathPrefix; 523 } 524 525 /** Gets the context for this store */ getContext()526 Context getContext() { 527 return mContext; 528 } 529 530 /** Returns a clone of the transport associated with this store. */ cloneTransport()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 */ ensurePrefixIsValid()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 */ getConnection()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 */ poolConnection(ImapConnection connection)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 */ encodeFolderName(String name, String prefix)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 */ decodeFolderName(String name, String prefix)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 */ joinMessageUids(Message[] messages)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 { ImapMessage(String uid, ImapFolder folder)636 ImapMessage(String uid, ImapFolder folder) { 637 mUid = uid; 638 mFolder = folder; 639 } 640 setSize(int size)641 public void setSize(int size) { 642 mSize = size; 643 } 644 645 @Override parse(InputStream in)646 public void parse(InputStream in) throws IOException, MessagingException { 647 super.parse(in); 648 } 649 setFlagInternal(Flag flag, boolean set)650 public void setFlagInternal(Flag flag, boolean set) throws MessagingException { 651 super.setFlag(flag, set); 652 } 653 654 @Override setFlag(Flag flag, boolean set)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 ImapException(String message, String status, String alertText, String responseCode)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 getStatus()676 public String getStatus() { 677 return mStatus; 678 } 679 getAlertText()680 public String getAlertText() { 681 return mAlertText; 682 } 683 getResponseCode()684 public String getResponseCode() { 685 return mResponseCode; 686 } 687 } 688 closeConnections()689 public void closeConnections() { 690 ImapConnection connection; 691 while ((connection = mConnectionPool.poll()) != null) { 692 connection.close(); 693 } 694 } 695 } 696