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