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 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      */
     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      */
    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
    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
    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
    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
    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
    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
    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      */
    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      */
    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
    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
    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      */
    465     boolean isUserPrefixSet() {
    466         return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix);
    467     }
    468 
    469     /** Sets the path separator */
    470     void setPathSeparator(String pathSeparator) {
    471         mPathSeparator = pathSeparator;
    472     }
    473 
    474     /** Sets the prefix */
    475     void setPathPrefix(String pathPrefix) {
    476         mPathPrefix = pathPrefix;
    477     }
    478 
    479     /** Gets the context for this store */
    480     Context getContext() {
    481         return mContext;
    482     }
    483 
    484     /** Returns a clone of the transport associated with this store. */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 {
    587         ImapMessage(String uid, ImapFolder folder) {
    588             mUid = uid;
    589             mFolder = folder;
    590         }
    591 
    592         public void setSize(int size) {
    593             mSize = size;
    594         }
    595 
    596         @Override
    597         public void parse(InputStream in) throws IOException, MessagingException {
    598             super.parse(in);
    599         }
    600 
    601         public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
    602             super.setFlag(flag, set);
    603         }
    604 
    605         @Override
    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 
    617         public ImapException(String message, String alertText, Throwable throwable) {
    618             super(message, throwable);
    619             mAlertText = alertText;
    620         }
    621 
    622         public ImapException(String message, String alertText) {
    623             super(message);
    624             mAlertText = alertText;
    625         }
    626 
    627         public String getAlertText() {
    628             return mAlertText;
    629         }
    630 
    631         public void setAlertText(String alertText) {
    632             mAlertText = alertText;
    633         }
    634     }
    635 }
    636