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