• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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