k-9/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java

2968 lines
124 KiB
Java
Raw Normal View History

2014-12-18 03:33:09 -05:00
package com.fsck.k9.mail.store.imap;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
2014-09-28 05:12:19 -04:00
import java.util.Deque;
2014-10-05 07:08:55 -04:00
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
2014-12-17 11:16:18 -05:00
import android.net.ConnectivityManager;
import android.os.PowerManager;
2014-12-11 16:48:22 -05:00
import android.text.TextUtils;
import android.util.Log;
import com.fsck.k9.mail.power.TracingPowerManager;
import com.fsck.k9.mail.power.TracingPowerManager.TracingWakeLock;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
2014-12-16 06:51:52 -05:00
import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.PushReceiver;
import com.fsck.k9.mail.Pusher;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.filter.FixedLengthInputStream;
import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
2014-12-18 03:33:09 -05:00
import com.fsck.k9.mail.store.RemoteStore;
import com.fsck.k9.mail.store.StoreConfig;
import com.fsck.k9.mail.transport.imap.ImapSettings;
2014-12-11 16:48:22 -05:00
import com.beetstra.jutf7.CharsetProvider;
2014-12-16 06:51:52 -05:00
import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
import static com.fsck.k9.mail.K9MailLib.PUSH_WAKE_LOCK_TIMEOUT;
/**
* <pre>
* TODO Need to start keeping track of UIDVALIDITY
* TODO Need a default response handler for things like folder updates
* </pre>
*/
public class ImapStore extends RemoteStore {
public static final String STORE_TYPE = "IMAP";
private static final int IDLE_READ_TIMEOUT_INCREMENT = 5 * 60 * 1000;
private static final int IDLE_FAILURE_COUNT_LIMIT = 10;
2014-12-18 03:33:09 -05:00
private static final int MAX_DELAY_TIME = 5 * 60 * 1000; // 5 minutes
private static final int NORMAL_DELAY_TIME = 5000;
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static final int FETCH_WINDOW_SIZE = 100;
2014-10-05 07:08:55 -04:00
private Set<Flag> mPermanentFlagsIndex = EnumSet.noneOf(Flag.class);
private ConnectivityManager mConnectivityManager;
2014-12-18 03:33:09 -05:00
private String mHost;
private int mPort;
private String mUsername;
private String mPassword;
private String mClientCertificateAlias;
private ConnectionSecurity mConnectionSecurity;
private AuthType mAuthType;
private String mPathPrefix;
private String mCombinedPrefix = null;
private String mPathDelimiter = null;
/**
* Decodes an ImapStore URI.
*
* <p>Possible forms:</p>
* <pre>
Eliminate the 'if available' connection security options These options originated in the AOSP email client from which K-9 Mail was forked. They provide an odd combination of 2 features: 1. Don't bother to authenticate the server's certificate (applies to both SSL/TLS and STARTTLS); i.e., blindly accept all certificates. This is generally a bad security policy which is susceptible to MITM attacks. 2. If STARTTLS is selected but the server doesn't claim to support STARTTLS, then proceed without using encryption. This, too, is a bad security policy which is susceptible to MITM attacks. Since the time that K-9 Mail was forked, a couple things have changed: > K-9 Mail has implemented the ability for users to review and permanently accept individual certificates that would otherwise fail authentication. With this ability, there is no need for a user to subject themselves to the ongoing risks of feature 1. above. Hence, this commit removes feature 1. > The AOSP email client has changed its behavior and no longer permits a security downgrade to an unencrypted connection if the server doesn't claim to support STARTTLS (i.e., they eliminated feature 2. above). K-9 Mail should do the same. It's unlikely that a server is going to provide STARTTLS on an intermittent basis, so providing a contingency for such unusual behavior is an unnecessary risk. Hence, this commit removes that feature as well. Effect on existing users: If the old connection security setting was "SSL/TLS (if available)" (which now gets remapped to "SSL/TLS"), and the server does not provide a certificate that can be authenticated, then a "Certificate error for <account name>" notification is generated telling the user to check their server settings. Tapping the notification takes the user to the relevant server settings, where the user can tap "Next" to review the certificate and choose to permanently accept it. This process would occur during the first syncing of folders after application upgrade or (in the case of SMTP) during the first attempt to send a message. If the connection security setting was "STARTTLS (if available)" (which now gets remapped to "STARTTLS"), and the server does not provide a certificate that can be authenticated, then the same process as above would occur. If the old connection security setting was "STARTTLS (if available)", and the server doesn't claim to support STARTTLS, then the user would get a certificate error notification which would lead them to the server's settings. There they would need to choose a different connection security -- most likely "NONE". If they didn't change anything but instead just tapped "Next", the server settings would be checked again and a dialog would pop up saying, "Cannot connect to server. (STARTTLS connection security not available)". (The implementation of notifications when STARTTLS is not available is not actually included here -- it's in the commit that follows.) Regarding the changes to providers.xml: in cases where the scheme ended with "+ssl", the schemes were simply updated by appending "+". In cases where the scheme ended with "+tls", a check of the server was made to assure that STARTTLS was available before appending "+" to the scheme. Domains paran.com and nate.com failed the check and were removed because no current information could be found. Domains me.com and mac.com also failed and were updated based on http://support.apple.com/kb/ht4864.
2014-02-26 16:50:21 -05:00
* imap://auth:user:password@server:port ConnectionSecurity.NONE
* imap+tls+://auth:user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
* imap+ssl+://auth:user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
* </pre>
*/
public static ImapStoreSettings decodeUri(String uri) {
String host;
int port;
ConnectionSecurity connectionSecurity;
AuthType authenticationType = null;
String username = null;
String password = null;
2014-05-25 16:45:14 -04:00
String clientCertificateAlias = null;
String pathPrefix = null;
boolean autoDetectNamespace = true;
URI imapUri;
try {
imapUri = new URI(uri);
} catch (URISyntaxException use) {
throw new IllegalArgumentException("Invalid ImapStore URI", use);
}
String scheme = imapUri.getScheme();
Eliminate the 'if available' connection security options These options originated in the AOSP email client from which K-9 Mail was forked. They provide an odd combination of 2 features: 1. Don't bother to authenticate the server's certificate (applies to both SSL/TLS and STARTTLS); i.e., blindly accept all certificates. This is generally a bad security policy which is susceptible to MITM attacks. 2. If STARTTLS is selected but the server doesn't claim to support STARTTLS, then proceed without using encryption. This, too, is a bad security policy which is susceptible to MITM attacks. Since the time that K-9 Mail was forked, a couple things have changed: > K-9 Mail has implemented the ability for users to review and permanently accept individual certificates that would otherwise fail authentication. With this ability, there is no need for a user to subject themselves to the ongoing risks of feature 1. above. Hence, this commit removes feature 1. > The AOSP email client has changed its behavior and no longer permits a security downgrade to an unencrypted connection if the server doesn't claim to support STARTTLS (i.e., they eliminated feature 2. above). K-9 Mail should do the same. It's unlikely that a server is going to provide STARTTLS on an intermittent basis, so providing a contingency for such unusual behavior is an unnecessary risk. Hence, this commit removes that feature as well. Effect on existing users: If the old connection security setting was "SSL/TLS (if available)" (which now gets remapped to "SSL/TLS"), and the server does not provide a certificate that can be authenticated, then a "Certificate error for <account name>" notification is generated telling the user to check their server settings. Tapping the notification takes the user to the relevant server settings, where the user can tap "Next" to review the certificate and choose to permanently accept it. This process would occur during the first syncing of folders after application upgrade or (in the case of SMTP) during the first attempt to send a message. If the connection security setting was "STARTTLS (if available)" (which now gets remapped to "STARTTLS"), and the server does not provide a certificate that can be authenticated, then the same process as above would occur. If the old connection security setting was "STARTTLS (if available)", and the server doesn't claim to support STARTTLS, then the user would get a certificate error notification which would lead them to the server's settings. There they would need to choose a different connection security -- most likely "NONE". If they didn't change anything but instead just tapped "Next", the server settings would be checked again and a dialog would pop up saying, "Cannot connect to server. (STARTTLS connection security not available)". (The implementation of notifications when STARTTLS is not available is not actually included here -- it's in the commit that follows.) Regarding the changes to providers.xml: in cases where the scheme ended with "+ssl", the schemes were simply updated by appending "+". In cases where the scheme ended with "+tls", a check of the server was made to assure that STARTTLS was available before appending "+" to the scheme. Domains paran.com and nate.com failed the check and were removed because no current information could be found. Domains me.com and mac.com also failed and were updated based on http://support.apple.com/kb/ht4864.
2014-02-26 16:50:21 -05:00
/*
* Currently available schemes are:
* imap
* imap+tls+
* imap+ssl+
*
* The following are obsolete schemes that may be found in pre-existing
* settings from earlier versions or that may be found when imported. We
* continue to recognize them and re-map them appropriately:
* imap+tls
* imap+ssl
*/
if (scheme.equals("imap")) {
connectionSecurity = ConnectionSecurity.NONE;
port = 143;
Eliminate the 'if available' connection security options These options originated in the AOSP email client from which K-9 Mail was forked. They provide an odd combination of 2 features: 1. Don't bother to authenticate the server's certificate (applies to both SSL/TLS and STARTTLS); i.e., blindly accept all certificates. This is generally a bad security policy which is susceptible to MITM attacks. 2. If STARTTLS is selected but the server doesn't claim to support STARTTLS, then proceed without using encryption. This, too, is a bad security policy which is susceptible to MITM attacks. Since the time that K-9 Mail was forked, a couple things have changed: > K-9 Mail has implemented the ability for users to review and permanently accept individual certificates that would otherwise fail authentication. With this ability, there is no need for a user to subject themselves to the ongoing risks of feature 1. above. Hence, this commit removes feature 1. > The AOSP email client has changed its behavior and no longer permits a security downgrade to an unencrypted connection if the server doesn't claim to support STARTTLS (i.e., they eliminated feature 2. above). K-9 Mail should do the same. It's unlikely that a server is going to provide STARTTLS on an intermittent basis, so providing a contingency for such unusual behavior is an unnecessary risk. Hence, this commit removes that feature as well. Effect on existing users: If the old connection security setting was "SSL/TLS (if available)" (which now gets remapped to "SSL/TLS"), and the server does not provide a certificate that can be authenticated, then a "Certificate error for <account name>" notification is generated telling the user to check their server settings. Tapping the notification takes the user to the relevant server settings, where the user can tap "Next" to review the certificate and choose to permanently accept it. This process would occur during the first syncing of folders after application upgrade or (in the case of SMTP) during the first attempt to send a message. If the connection security setting was "STARTTLS (if available)" (which now gets remapped to "STARTTLS"), and the server does not provide a certificate that can be authenticated, then the same process as above would occur. If the old connection security setting was "STARTTLS (if available)", and the server doesn't claim to support STARTTLS, then the user would get a certificate error notification which would lead them to the server's settings. There they would need to choose a different connection security -- most likely "NONE". If they didn't change anything but instead just tapped "Next", the server settings would be checked again and a dialog would pop up saying, "Cannot connect to server. (STARTTLS connection security not available)". (The implementation of notifications when STARTTLS is not available is not actually included here -- it's in the commit that follows.) Regarding the changes to providers.xml: in cases where the scheme ended with "+ssl", the schemes were simply updated by appending "+". In cases where the scheme ended with "+tls", a check of the server was made to assure that STARTTLS was available before appending "+" to the scheme. Domains paran.com and nate.com failed the check and were removed because no current information could be found. Domains me.com and mac.com also failed and were updated based on http://support.apple.com/kb/ht4864.
2014-02-26 16:50:21 -05:00
} else if (scheme.startsWith("imap+tls")) {
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
port = 143;
Eliminate the 'if available' connection security options These options originated in the AOSP email client from which K-9 Mail was forked. They provide an odd combination of 2 features: 1. Don't bother to authenticate the server's certificate (applies to both SSL/TLS and STARTTLS); i.e., blindly accept all certificates. This is generally a bad security policy which is susceptible to MITM attacks. 2. If STARTTLS is selected but the server doesn't claim to support STARTTLS, then proceed without using encryption. This, too, is a bad security policy which is susceptible to MITM attacks. Since the time that K-9 Mail was forked, a couple things have changed: > K-9 Mail has implemented the ability for users to review and permanently accept individual certificates that would otherwise fail authentication. With this ability, there is no need for a user to subject themselves to the ongoing risks of feature 1. above. Hence, this commit removes feature 1. > The AOSP email client has changed its behavior and no longer permits a security downgrade to an unencrypted connection if the server doesn't claim to support STARTTLS (i.e., they eliminated feature 2. above). K-9 Mail should do the same. It's unlikely that a server is going to provide STARTTLS on an intermittent basis, so providing a contingency for such unusual behavior is an unnecessary risk. Hence, this commit removes that feature as well. Effect on existing users: If the old connection security setting was "SSL/TLS (if available)" (which now gets remapped to "SSL/TLS"), and the server does not provide a certificate that can be authenticated, then a "Certificate error for <account name>" notification is generated telling the user to check their server settings. Tapping the notification takes the user to the relevant server settings, where the user can tap "Next" to review the certificate and choose to permanently accept it. This process would occur during the first syncing of folders after application upgrade or (in the case of SMTP) during the first attempt to send a message. If the connection security setting was "STARTTLS (if available)" (which now gets remapped to "STARTTLS"), and the server does not provide a certificate that can be authenticated, then the same process as above would occur. If the old connection security setting was "STARTTLS (if available)", and the server doesn't claim to support STARTTLS, then the user would get a certificate error notification which would lead them to the server's settings. There they would need to choose a different connection security -- most likely "NONE". If they didn't change anything but instead just tapped "Next", the server settings would be checked again and a dialog would pop up saying, "Cannot connect to server. (STARTTLS connection security not available)". (The implementation of notifications when STARTTLS is not available is not actually included here -- it's in the commit that follows.) Regarding the changes to providers.xml: in cases where the scheme ended with "+ssl", the schemes were simply updated by appending "+". In cases where the scheme ended with "+tls", a check of the server was made to assure that STARTTLS was available before appending "+" to the scheme. Domains paran.com and nate.com failed the check and were removed because no current information could be found. Domains me.com and mac.com also failed and were updated based on http://support.apple.com/kb/ht4864.
2014-02-26 16:50:21 -05:00
} else if (scheme.startsWith("imap+ssl")) {
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
port = 993;
} else {
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
}
host = imapUri.getHost();
if (imapUri.getPort() != -1) {
port = imapUri.getPort();
}
if (imapUri.getUserInfo() != null) {
2014-09-28 06:59:11 -04:00
String userinfo = imapUri.getUserInfo();
String[] userInfoParts = userinfo.split(":");
2014-09-28 06:59:11 -04:00
if (userinfo.endsWith(":")) {
// Password is empty. This can only happen after an account was imported.
authenticationType = AuthType.valueOf(userInfoParts[0]);
2014-12-14 10:54:27 -05:00
username = decodeUtf8(userInfoParts[1]);
2014-09-28 06:59:11 -04:00
} else if (userInfoParts.length == 2) {
authenticationType = AuthType.PLAIN;
2014-12-14 10:54:27 -05:00
username = decodeUtf8(userInfoParts[0]);
password = decodeUtf8(userInfoParts[1]);
2014-09-28 06:59:11 -04:00
} else if (userInfoParts.length == 3) {
authenticationType = AuthType.valueOf(userInfoParts[0]);
2014-12-14 10:54:27 -05:00
username = decodeUtf8(userInfoParts[1]);
2014-05-25 16:45:14 -04:00
2014-09-28 06:59:11 -04:00
if (AuthType.EXTERNAL == authenticationType) {
2014-12-14 10:54:27 -05:00
clientCertificateAlias = decodeUtf8(userInfoParts[2]);
2014-09-28 06:59:11 -04:00
} else {
2014-12-14 10:54:27 -05:00
password = decodeUtf8(userInfoParts[2]);
}
}
}
String path = imapUri.getPath();
if (path != null && path.length() > 1) {
// Strip off the leading "/"
String cleanPath = path.substring(1);
if (cleanPath.length() >= 2 && cleanPath.charAt(1) == '|') {
autoDetectNamespace = cleanPath.charAt(0) == '1';
if (!autoDetectNamespace) {
pathPrefix = cleanPath.substring(2);
}
} else {
if (cleanPath.length() > 0) {
pathPrefix = cleanPath;
autoDetectNamespace = false;
}
}
}
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, username,
2014-05-25 16:45:14 -04:00
password, clientCertificateAlias, autoDetectNamespace, pathPrefix);
}
/**
* Creates an ImapStore URI with the supplied settings.
*
* @param server
* The {@link ServerSettings} object that holds the server settings.
*
* @return An ImapStore URI that holds the same information as the {@code server} parameter.
*
* @see com.fsck.k9.mail.store.StoreConfig#getStoreUri()
* @see ImapStore#decodeUri(String)
*/
public static String createUri(ServerSettings server) {
2014-12-14 10:54:27 -05:00
String userEnc = encodeUtf8(server.username);
2014-09-28 06:59:11 -04:00
String passwordEnc = (server.password != null) ?
2014-12-14 10:54:27 -05:00
encodeUtf8(server.password) : "";
2014-09-28 06:59:11 -04:00
String clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
2014-12-14 10:54:27 -05:00
encodeUtf8(server.clientCertificateAlias) : "";
String scheme;
switch (server.connectionSecurity) {
case SSL_TLS_REQUIRED:
scheme = "imap+ssl+";
break;
case STARTTLS_REQUIRED:
scheme = "imap+tls+";
break;
default:
case NONE:
scheme = "imap";
break;
}
AuthType authType = server.authenticationType;
2014-05-25 16:45:14 -04:00
String userInfo;
if (authType == AuthType.EXTERNAL) {
2014-05-25 16:45:14 -04:00
userInfo = authType.name() + ":" + userEnc + ":" + clientCertificateAliasEnc;
} else {
userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
}
try {
Map<String, String> extra = server.getExtra();
String path;
if (extra != null) {
boolean autoDetectNamespace = Boolean.TRUE.toString().equals(
extra.get(ImapStoreSettings.AUTODETECT_NAMESPACE_KEY));
String pathPrefix = (autoDetectNamespace) ?
null : extra.get(ImapStoreSettings.PATH_PREFIX_KEY);
path = "/" + (autoDetectNamespace ? "1" : "0") + "|" +
((pathPrefix == null) ? "" : pathPrefix);
} else {
path = "/1|";
}
return new URI(scheme, userInfo, server.host, server.port,
path,
null, null).toString();
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Can't create ImapStore URI", e);
}
}
/**
* This class is used to store the decoded contents of an ImapStore URI.
*
* @see ImapStore#decodeUri(String)
*/
public static class ImapStoreSettings extends ServerSettings {
public static final String AUTODETECT_NAMESPACE_KEY = "autoDetectNamespace";
public static final String PATH_PREFIX_KEY = "pathPrefix";
public final boolean autoDetectNamespace;
public final String pathPrefix;
protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
2014-05-25 16:45:14 -04:00
AuthType authenticationType, String username, String password, String clientCertificateAlias,
boolean autodetectNamespace, String pathPrefix) {
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
2014-05-25 16:45:14 -04:00
password, clientCertificateAlias);
this.autoDetectNamespace = autodetectNamespace;
this.pathPrefix = pathPrefix;
}
@Override
public Map<String, String> getExtra() {
Map<String, String> extra = new HashMap<String, String>();
extra.put(AUTODETECT_NAMESPACE_KEY, Boolean.valueOf(autoDetectNamespace).toString());
putIfNotNull(extra, PATH_PREFIX_KEY, pathPrefix);
return extra;
}
@Override
public ServerSettings newPassword(String newPassword) {
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType,
2014-05-25 16:45:14 -04:00
username, newPassword, clientCertificateAlias, autoDetectNamespace, pathPrefix);
}
}
2014-12-18 03:33:09 -05:00
protected static final SimpleDateFormat RFC3501_DATE = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
2014-09-28 05:12:19 -04:00
private final Deque<ImapConnection> mConnections =
new LinkedList<ImapConnection>();
/**
* Charset used for converting folder names to and from UTF-7 as defined by RFC 3501.
*/
private Charset mModifiedUtf7Charset;
/**
* Cache of ImapFolder objects. ImapFolders are attached to a given folder on the server
* and as long as their associated connection remains open they are reusable between
* requests. This cache lets us make sure we always reuse, if possible, for a given
* folder name.
*/
2014-09-28 05:12:19 -04:00
private final Map<String, ImapFolder> mFolderCache = new HashMap<String, ImapFolder>();
public ImapStore(StoreConfig storeConfig,
TrustedSocketFactory trustedSocketFactory,
ConnectivityManager connectivityManager)
throws MessagingException {
super(storeConfig, trustedSocketFactory);
ImapStoreSettings settings;
try {
settings = decodeUri(storeConfig.getStoreUri());
} catch (IllegalArgumentException e) {
throw new MessagingException("Error while decoding store URI", e);
}
mHost = settings.host;
mPort = settings.port;
mConnectionSecurity = settings.connectionSecurity;
mConnectivityManager = connectivityManager;
mAuthType = settings.authenticationType;
mUsername = settings.username;
mPassword = settings.password;
2014-05-25 16:45:14 -04:00
mClientCertificateAlias = settings.clientCertificateAlias;
// Make extra sure mPathPrefix is null if "auto-detect namespace" is configured
mPathPrefix = (settings.autoDetectNamespace) ? null : settings.pathPrefix;
mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501");
}
@Override
public Folder getFolder(String name) {
ImapFolder folder;
synchronized (mFolderCache) {
folder = mFolderCache.get(name);
if (folder == null) {
Complete merge of DAmail functionality into K9mail. Following features are added to K9mail: 1) Show unread message count on each folder 2) Sum unread count of all shown folders in an account to the account display 3) Periodically check selected folders for new mail, not just Inbox 4) Don't refresh folder when opened (unless folder is empty) 5) Show date and time of last sync for each folder 6) Fix timer for automatic periodic sync (use wakelock to assure completion) 7) Optimize local folder queries (speeds up account and folder lists) 8) Show Loading... message in status bar indicating which folder is being synced 9) Eliminate redundant sync of new messages (performance enhancement) 10) Improve notification text for multiple accounts 11) Do not automatically sync folders more often than the account-specific period 12) Use user-configured date and time formats 13) Select which folders are shown, using configurable Classes 14) Select which folders are synced, using configurable Classes 15) Added context (long press) menu to folders, to provide for Refresh and Folder Settings 16) Status light flashes purple when there are unread messages 17) Folder list more quickly eliminates display of deleted and out-of-Class folders. 18) Delete works 19) Mark all messages as read (in the folder context menu) 20) Notifications only for new unread messages 21) One minute synchronization frequency 22) Deleting an unread message decrements unread counter 23) Notifications work for POP3 accounts 24) Message deletes work for POP3 accounts 25) Explicit errors show in folder list 26) Stack traces saved to folder K9mail-errors 27) Clear pending actions (danger, for emergencies only!) 28) Delete policy in Account settings 29) DNS cache in InetAddress disabled 30) Trapped some crash-causing error conditions 31) Eliminate duplicate copies to Sent folder 32) Prevent crashes due to message listener concurrency 33) Empty Trash 34) Nuclear "Mark all messages as read" (marks all messages as read in server-side folder, irrespective of which messages have been downloaded) 35) Forward (alternate) to allow forwarding email through other programs 36) Accept text/plain Intents to allow other programs to send email through K9mail 37) Displays Outbox sending status 38) Manual retry of outbox sending when "Refresh"ing Outbox 39) Folder error status is persisted 40) Ability to log to arbitrary file Fixes K9 issues 11, 23, 24, 65, 69, 71, 79, 81, 82, 83, 87, 101, 104, 107, 120, 148, 154
2008-12-30 22:49:09 -05:00
folder = new ImapFolder(this, name);
mFolderCache.put(name, folder);
}
}
return folder;
}
private String getCombinedPrefix() {
if (mCombinedPrefix == null) {
if (mPathPrefix != null) {
String tmpPrefix = mPathPrefix.trim();
2014-12-18 03:33:09 -05:00
String tmpDelim = (mPathDelimiter != null ? mPathDelimiter.trim() : "");
if (tmpPrefix.endsWith(tmpDelim)) {
mCombinedPrefix = tmpPrefix;
} else if (tmpPrefix.length() > 0) {
mCombinedPrefix = tmpPrefix + tmpDelim;
} else {
mCombinedPrefix = "";
}
} else {
mCombinedPrefix = "";
}
}
return mCombinedPrefix;
}
@Override
public List <? extends Folder > getPersonalNamespaces(boolean forceListAll) throws MessagingException {
ImapConnection connection = getConnection();
try {
List <? extends Folder > allFolders = listFolders(connection, false);
if (forceListAll || !mStoreConfig.subscribedFoldersOnly()) {
return allFolders;
} else {
List<Folder> resultFolders = new LinkedList<Folder>();
2014-02-15 17:48:35 -05:00
Set<String> subscribedFolderNames = new HashSet<String>();
List <? extends Folder > subscribedFolders = listFolders(connection, true);
for (Folder subscribedFolder : subscribedFolders) {
subscribedFolderNames.add(subscribedFolder.getName());
}
for (Folder folder : allFolders) {
if (subscribedFolderNames.contains(folder.getName())) {
resultFolders.add(folder);
}
}
return resultFolders;
}
} catch (IOException ioe) {
connection.close();
throw new MessagingException("Unable to get folder list.", ioe);
} catch (MessagingException me) {
connection.close();
throw new MessagingException("Unable to get folder list.", me);
} finally {
releaseConnection(connection);
}
}
private List <? extends Folder > listFolders(ImapConnection connection, boolean LSUB) throws IOException, MessagingException {
String commandResponse = LSUB ? "LSUB" : "LIST";
LinkedList<Folder> folders = new LinkedList<Folder>();
List<ImapResponse> responses =
connection.executeSimpleCommand(String.format("%s \"\" %s", commandResponse,
encodeString(getCombinedPrefix() + "*")));
for (ImapResponse response : responses) {
if (ImapResponseParser.equalsIgnoreCase(response.get(0), commandResponse)) {
boolean includeFolder = true;
if (response.size() > 4 || !(response.getObject(3) instanceof String)) {
2014-12-16 06:51:52 -05:00
Log.w(LOG_TAG, "Skipping incorrectly parsed " + commandResponse +
" reply: " + response);
continue;
}
String decodedFolderName;
try {
decodedFolderName = decodeFolderName(response.getString(3));
} catch (CharacterCodingException e) {
2014-12-16 06:51:52 -05:00
Log.w(LOG_TAG, "Folder name not correctly encoded with the UTF-7 variant " +
2011-06-01 16:03:56 -04:00
"as defined by RFC 3501: " + response.getString(3), e);
//TODO: Use the raw name returned by the server for all commands that require
// a folder name. Use the decoded name only for showing it to the user.
// We currently just skip folders with malformed names.
continue;
}
String folder = decodedFolderName;
2014-12-18 03:33:09 -05:00
if (mPathDelimiter == null) {
mPathDelimiter = response.getString(2);
mCombinedPrefix = null;
}
if (folder.equalsIgnoreCase(mStoreConfig.getInboxFolderName())) {
continue;
} else if (folder.equals(mStoreConfig.getOutboxFolderName())) {
/*
* There is a folder on the server with the same name as our local
* outbox. Until we have a good plan to deal with this situation
* we simply ignore the folder on the server.
*/
continue;
} else {
int prefixLength = getCombinedPrefix().length();
if (prefixLength > 0) {
// Strip prefix from the folder name
if (folder.length() >= prefixLength) {
folder = folder.substring(prefixLength);
}
if (!decodedFolderName.equalsIgnoreCase(getCombinedPrefix() + folder)) {
includeFolder = false;
}
}
}
ImapList attributes = response.getList(1);
for (int i = 0, count = attributes.size(); i < count; i++) {
String attribute = attributes.getString(i);
if (attribute.equalsIgnoreCase("\\NoSelect")) {
includeFolder = false;
}
}
if (includeFolder) {
folders.add(getFolder(folder));
}
}
}
folders.add(getFolder(mStoreConfig.getInboxFolderName()));
return folders;
}
/**
* Attempt to auto-configure folders by attributes if the server advertises that capability.
*
* The parsing here is essentially the same as
2014-12-17 11:16:18 -05:00
* {@link #listFolders(ImapConnection, boolean)}; we should try to consolidate
* this at some point. :(
* @param connection IMAP Connection
* @throws IOException uh oh!
* @throws MessagingException uh oh!
*/
private void autoconfigureFolders(final ImapConnection connection) throws IOException, MessagingException {
String commandResponse;
String commandOptions = "";
2014-12-17 11:16:18 -05:00
if (connection.getCapabilities().contains("XLIST")) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration: Using XLIST.");
commandResponse = "XLIST";
2014-12-17 11:16:18 -05:00
} else if(connection.getCapabilities().contains("SPECIAL-USE")) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration: Using RFC6154/SPECIAL-USE.");
commandResponse = "LIST";
commandOptions = " (SPECIAL-USE)";
} else {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) Log.d(LOG_TAG, "No detected folder auto-configuration methods.");
return;
}
final List<ImapResponse> responses =
connection.executeSimpleCommand(String.format("%s%s \"\" %s", commandResponse, commandOptions,
encodeString(getCombinedPrefix() + "*")));
for (ImapResponse response : responses) {
if (ImapResponseParser.equalsIgnoreCase(response.get(0), commandResponse)) {
String decodedFolderName;
try {
decodedFolderName = decodeFolderName(response.getString(3));
} catch (CharacterCodingException e) {
2014-12-16 06:51:52 -05:00
Log.w(LOG_TAG, "Folder name not correctly encoded with the UTF-7 variant " +
"as defined by RFC 3501: " + response.getString(3), e);
// We currently just skip folders with malformed names.
continue;
}
2014-12-18 03:33:09 -05:00
if (mPathDelimiter == null) {
mPathDelimiter = response.getString(2);
mCombinedPrefix = null;
}
ImapList attributes = response.getList(1);
for (int i = 0, count = attributes.size(); i < count; i++) {
String attribute = attributes.getString(i);
if (attribute.equals("\\Drafts")) {
mStoreConfig.setDraftsFolderName(decodedFolderName);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration detected draft folder: " + decodedFolderName);
} else if (attribute.equals("\\Sent")) {
mStoreConfig.setSentFolderName(decodedFolderName);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration detected sent folder: " + decodedFolderName);
} else if (attribute.equals("\\Spam") || attribute.equals("\\Junk")) {
//rfc6154 just mentions \Junk
mStoreConfig.setSpamFolderName(decodedFolderName);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration detected spam folder: " + decodedFolderName);
} else if (attribute.equals("\\Trash")) {
mStoreConfig.setTrashFolderName(decodedFolderName);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration detected trash folder: " + decodedFolderName);
}
}
}
}
}
@Override
public void checkSettings() throws MessagingException {
try {
ImapConnection connection = new ImapConnection(
new StoreImapSettings(),
mTrustedSocketFactory,
mConnectivityManager);
connection.open();
autoconfigureFolders(connection);
connection.close();
} catch (IOException ioe) {
throw new MessagingException("Unable to connect", ioe);
}
}
private ImapConnection getConnection() throws MessagingException {
synchronized (mConnections) {
2014-12-17 10:12:55 -05:00
ImapConnection connection;
while ((connection = mConnections.poll()) != null) {
try {
connection.executeSimpleCommand("NOOP");
break;
} catch (IOException ioe) {
connection.close();
}
}
if (connection == null) {
connection = new ImapConnection(new StoreImapSettings(),
mTrustedSocketFactory,
mConnectivityManager);
}
return connection;
}
}
private void releaseConnection(ImapConnection connection) {
if (connection != null && connection.isOpen()) {
synchronized (mConnections) {
mConnections.offer(connection);
}
}
}
/**
* Encode a string to be able to use it in an IMAP command.
*
* "A quoted string is a sequence of zero or more 7-bit characters,
* excluding CR and LF, with double quote (<">) characters at each
2011-01-06 11:55:34 -05:00
* end." - Section 4.3, RFC 3501
*
* Double quotes and backslash are escaped by prepending a backslash.
*
* @param str
* The input string (only 7-bit characters allowed).
* @return
* The string encoded as quoted (IMAP) string.
*/
private static String encodeString(String str) {
return "\"" + str.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
private String encodeFolderName(String name) {
2014-09-28 06:39:32 -04:00
ByteBuffer bb = mModifiedUtf7Charset.encode(name);
byte[] b = new byte[bb.limit()];
bb.get(b);
return new String(b, Charset.forName("US-ASCII"));
}
private String decodeFolderName(String name) throws CharacterCodingException {
/*
* Convert the encoded name to US-ASCII, then pass it through the modified UTF-7
* decoder and return the Unicode String.
*/
2014-09-28 06:39:32 -04:00
// Make sure the decoder throws an exception if it encounters an invalid encoding.
CharsetDecoder decoder = mModifiedUtf7Charset.newDecoder().onMalformedInput(CodingErrorAction.REPORT);
CharBuffer cb = decoder.decode(ByteBuffer.wrap(name.getBytes(Charset.forName("US-ASCII"))));
return cb.toString();
}
@Override
public boolean isMoveCapable() {
return true;
}
@Override
public boolean isCopyCapable() {
return true;
}
@Override
public boolean isPushCapable() {
return true;
}
@Override
public boolean isExpungeCapable() {
return true;
}
protected class ImapFolder extends Folder<ImapMessage> {
private String mName;
protected volatile int mMessageCount = -1;
protected volatile long uidNext = -1L;
protected volatile ImapConnection mConnection;
private int mMode;
private volatile boolean mExists;
Complete merge of DAmail functionality into K9mail. Following features are added to K9mail: 1) Show unread message count on each folder 2) Sum unread count of all shown folders in an account to the account display 3) Periodically check selected folders for new mail, not just Inbox 4) Don't refresh folder when opened (unless folder is empty) 5) Show date and time of last sync for each folder 6) Fix timer for automatic periodic sync (use wakelock to assure completion) 7) Optimize local folder queries (speeds up account and folder lists) 8) Show Loading... message in status bar indicating which folder is being synced 9) Eliminate redundant sync of new messages (performance enhancement) 10) Improve notification text for multiple accounts 11) Do not automatically sync folders more often than the account-specific period 12) Use user-configured date and time formats 13) Select which folders are shown, using configurable Classes 14) Select which folders are synced, using configurable Classes 15) Added context (long press) menu to folders, to provide for Refresh and Folder Settings 16) Status light flashes purple when there are unread messages 17) Folder list more quickly eliminates display of deleted and out-of-Class folders. 18) Delete works 19) Mark all messages as read (in the folder context menu) 20) Notifications only for new unread messages 21) One minute synchronization frequency 22) Deleting an unread message decrements unread counter 23) Notifications work for POP3 accounts 24) Message deletes work for POP3 accounts 25) Explicit errors show in folder list 26) Stack traces saved to folder K9mail-errors 27) Clear pending actions (danger, for emergencies only!) 28) Delete policy in Account settings 29) DNS cache in InetAddress disabled 30) Trapped some crash-causing error conditions 31) Eliminate duplicate copies to Sent folder 32) Prevent crashes due to message listener concurrency 33) Empty Trash 34) Nuclear "Mark all messages as read" (marks all messages as read in server-side folder, irrespective of which messages have been downloaded) 35) Forward (alternate) to allow forwarding email through other programs 36) Accept text/plain Intents to allow other programs to send email through K9mail 37) Displays Outbox sending status 38) Manual retry of outbox sending when "Refresh"ing Outbox 39) Folder error status is persisted 40) Ability to log to arbitrary file Fixes K9 issues 11, 23, 24, 65, 69, 71, 79, 81, 82, 83, 87, 101, 104, 107, 120, 148, 154
2008-12-30 22:49:09 -05:00
private ImapStore store = null;
Map<Long, String> msgSeqUidMap = new ConcurrentHashMap<Long, String>();
private boolean mInSearch = false;
public ImapFolder(ImapStore nStore, String name) {
super();
store = nStore;
this.mName = name;
}
public String getPrefixedName() throws MessagingException {
String prefixedName = "";
if (!mStoreConfig.getInboxFolderName().equalsIgnoreCase(mName)) {
ImapConnection connection;
synchronized (this) {
if (mConnection == null) {
connection = getConnection();
} else {
connection = mConnection;
}
}
try {
connection.open();
} catch (IOException ioe) {
throw new MessagingException("Unable to get IMAP prefix", ioe);
} finally {
if (mConnection == null) {
releaseConnection(connection);
}
}
prefixedName = getCombinedPrefix();
}
prefixedName += mName;
return prefixedName;
}
protected List<ImapResponse> executeSimpleCommand(String command) throws MessagingException, IOException {
return handleUntaggedResponses(mConnection.executeSimpleCommand(command));
}
protected List<ImapResponse> executeSimpleCommand(String command, boolean sensitve, UntaggedHandler untaggedHandler) throws MessagingException, IOException {
return handleUntaggedResponses(mConnection.executeSimpleCommand(command, sensitve, untaggedHandler));
}
@Override
public void open(int mode) throws MessagingException {
internalOpen(mode);
if (mMessageCount == -1) {
throw new MessagingException(
"Did not find message count during open");
}
}
public List<ImapResponse> internalOpen(int mode) throws MessagingException {
if (isOpen() && mMode == mode) {
// Make sure the connection is valid. If it's not we'll close it down and continue
// on to get a new one.
try {
return executeSimpleCommand("NOOP");
} catch (IOException ioe) {
/* don't throw */ ioExceptionHandler(mConnection, ioe);
}
}
releaseConnection(mConnection);
synchronized (this) {
mConnection = getConnection();
}
// * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
// $MDNSent)
// * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
// NonJunk $MDNSent \*)] Flags permitted.
// * 23 EXISTS
// * 0 RECENT
// * OK [UIDVALIDITY 1125022061] UIDs valid
// * OK [UIDNEXT 57576] Predicted next UID
// 2 OK [READ-WRITE] Select completed.
try {
msgSeqUidMap.clear();
String command = String.format("%s %s", mode == OPEN_MODE_RW ? "SELECT"
2012-08-20 22:09:34 -04:00
: "EXAMINE", encodeString(encodeFolderName(getPrefixedName())));
List<ImapResponse> responses = executeSimpleCommand(command);
/*
2012-08-20 22:09:34 -04:00
* If the command succeeds we expect the folder has been opened read-write unless we
* are notified otherwise in the responses.
*/
mMode = mode;
for (ImapResponse response : responses) {
2012-08-20 22:09:34 -04:00
if (response.size() >= 2) {
Object bracketedObj = response.get(1);
2012-08-20 22:09:34 -04:00
if (!(bracketedObj instanceof ImapList)) {
continue;
}
ImapList bracketed = (ImapList) bracketedObj;
if (bracketed.isEmpty()) {
continue;
}
2012-08-20 22:09:34 -04:00
ImapList flags = bracketed.getKeyedList("PERMANENTFLAGS");
if (flags != null) {
// parse: * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted
// \Seen \Draft NonJunk $label1 \*)] Flags permitted.
parseFlags(flags);
2012-08-20 22:09:34 -04:00
} else {
Object keyObj = bracketed.get(0);
if (keyObj instanceof String) {
String key = (String) keyObj;
2014-12-18 05:24:43 -05:00
if (response.getTag() != null) {
if ("READ-ONLY".equalsIgnoreCase(key)) {
mMode = OPEN_MODE_RO;
} else if ("READ-WRITE".equalsIgnoreCase(key)) {
mMode = OPEN_MODE_RW;
}
}
}
}
}
}
mExists = true;
return responses;
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
} catch (MessagingException me) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Unable to open connection for " + getLogId(), me);
throw me;
}
2012-08-20 22:09:34 -04:00
}
2012-08-20 22:09:34 -04:00
/**
* Parses an string like PERMANENTFLAGS (\Answered \Flagged \Deleted // \Seen \Draft NonJunk
* $label1 \*)
*
* the parsed flags are stored in the mPermanentFlagsIndex
2012-08-20 22:09:34 -04:00
* @param flags
* the imapflags as strings
*/
private void parseFlags(ImapList flags) {
2012-08-20 22:09:34 -04:00
for (Object flag : flags) {
flag = flag.toString().toLowerCase(Locale.US);
if (flag.equals("\\deleted")) {
mPermanentFlagsIndex.add(Flag.DELETED);
2012-08-20 22:09:34 -04:00
} else if (flag.equals("\\answered")) {
mPermanentFlagsIndex.add(Flag.ANSWERED);
2012-08-20 22:09:34 -04:00
} else if (flag.equals("\\seen")) {
mPermanentFlagsIndex.add(Flag.SEEN);
2012-08-20 22:09:34 -04:00
} else if (flag.equals("\\flagged")) {
mPermanentFlagsIndex.add(Flag.FLAGGED);
2012-08-20 22:09:34 -04:00
} else if (flag.equals("$forwarded")) {
mPermanentFlagsIndex.add(Flag.FORWARDED);
2012-08-20 22:09:34 -04:00
} else if (flag.equals("\\*")) {
mCanCreateKeywords = true;
}
}
}
@Override
public boolean isOpen() {
return mConnection != null;
}
@Override
public int getMode() {
return mMode;
}
@Override
public void close() {
if (mMessageCount != -1) {
mMessageCount = -1;
}
if (!isOpen()) {
return;
}
2010-01-02 20:50:51 -05:00
synchronized (this) {
// If we are mid-search and we get a close request, we gotta trash the connection.
if (mInSearch && mConnection != null) {
2014-12-16 06:51:52 -05:00
Log.i(LOG_TAG, "IMAP search was aborted, shutting down connection.");
mConnection.close();
} else {
releaseConnection(mConnection);
}
mConnection = null;
}
}
@Override
public String getName() {
return mName;
}
/**
* Check if a given folder exists on the server.
*
* @param folderName
* The name of the folder encoded as quoted string.
* See {@link ImapStore#encodeString}
*
* @return
* {@code True}, if the folder exists. {@code False}, otherwise.
*/
private boolean exists(String folderName) throws MessagingException {
try {
// Since we don't care about RECENT, we'll use that for the check, because we're checking
// a folder other than ourself, and don't want any untagged responses to cause a change
// in our own fields
mConnection.executeSimpleCommand(String.format("STATUS %s (RECENT)", folderName));
return true;
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
} catch (ImapException ie) {
// We got a response, but it was not "OK"
return false;
}
}
@Override
public boolean exists() throws MessagingException {
if (mExists) {
return true;
}
/*
* This method needs to operate in the unselected mode as well as the selected mode
* so we must get the connection ourselves if it's not there. We are specifically
* not calling checkOpen() since we don't care if the folder is open.
*/
ImapConnection connection;
synchronized (this) {
if (mConnection == null) {
connection = getConnection();
} else {
connection = mConnection;
}
}
try {
connection.executeSimpleCommand(String.format("STATUS %s (UIDVALIDITY)",
encodeString(encodeFolderName(getPrefixedName()))));
mExists = true;
return true;
} catch (ImapException ie) {
// We got a response, but it was not "OK"
return false;
} catch (IOException ioe) {
throw ioExceptionHandler(connection, ioe);
} finally {
if (mConnection == null) {
releaseConnection(connection);
}
}
}
@Override
public boolean create(FolderType type) throws MessagingException {
/*
* This method needs to operate in the unselected mode as well as the selected mode
* so we must get the connection ourselves if it's not there. We are specifically
* not calling checkOpen() since we don't care if the folder is open.
*/
ImapConnection connection;
synchronized (this) {
if (mConnection == null) {
connection = getConnection();
} else {
connection = mConnection;
}
}
try {
connection.executeSimpleCommand(String.format("CREATE %s",
encodeString(encodeFolderName(getPrefixedName()))));
return true;
} catch (ImapException ie) {
// We got a response, but it was not "OK"
return false;
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
} finally {
if (mConnection == null) {
releaseConnection(connection);
}
}
}
/**
* Copies the given messages to the specified folder.
*
* <p>
* <strong>Note:</strong>
* Only the UIDs of the given {@link Message} instances are used. It is assumed that all
* UIDs represent valid messages in this folder.
* </p>
*
* @param messages
* The messages to copy to the specfied folder.
* @param folder
* The name of the target folder.
*
* @return The mapping of original message UIDs to the new server UIDs.
*/
@Override
public Map<String, String> copyMessages(List<? extends Message> messages, Folder folder)
throws MessagingException {
if (!(folder instanceof ImapFolder)) {
throw new MessagingException("ImapFolder.copyMessages passed non-ImapFolder");
}
if (messages.isEmpty()) {
return null;
}
ImapFolder iFolder = (ImapFolder)folder;
checkOpen(); //only need READ access
String[] uids = new String[messages.size()];
for (int i = 0, count = messages.size(); i < count; i++) {
uids[i] = messages.get(i).getUid();
}
try {
String remoteDestName = encodeString(encodeFolderName(iFolder.getPrefixedName()));
//TODO: Try to copy/move the messages first and only create the folder if the
// operation fails. This will save a roundtrip if the folder already exists.
if (!exists(remoteDestName)) {
/*
* If the remote folder doesn't exist we try to create it.
*/
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) {
Log.i(LOG_TAG, "ImapFolder.copyMessages: attempting to create remote " +
"folder '" + remoteDestName + "' for " + getLogId());
}
iFolder.create(FolderType.HOLDS_MESSAGES);
}
//TODO: Split this into multiple commands if the command exceeds a certain length.
List<ImapResponse> responses = executeSimpleCommand(String.format("UID COPY %s %s",
combine(uids, ','),
remoteDestName));
// Get the tagged response for the UID COPY command
ImapResponse response = responses.get(responses.size() - 1);
Map<String, String> uidMap = null;
if (response.size() > 1) {
/*
* If the server supports UIDPLUS, then along with the COPY response it will
* return an COPYUID response code, e.g.
*
* 24 OK [COPYUID 38505 304,319:320 3956:3958] Success
*
* COPYUID is followed by UIDVALIDITY, the set of UIDs of copied messages from
* the source folder and the set of corresponding UIDs assigned to them in the
* destination folder.
*
* We can use the new UIDs included in this response to update our records.
*/
Object responseList = response.get(1);
if (responseList instanceof ImapList) {
final ImapList copyList = (ImapList) responseList;
if (copyList.size() >= 4 && copyList.getString(0).equals("COPYUID")) {
List<String> srcUids = ImapUtility.getImapSequenceValues(
copyList.getString(2));
List<String> destUids = ImapUtility.getImapSequenceValues(
copyList.getString(3));
if (srcUids != null && destUids != null) {
if (srcUids.size() == destUids.size()) {
Iterator<String> srcUidsIterator = srcUids.iterator();
Iterator<String> destUidsIterator = destUids.iterator();
uidMap = new HashMap<String, String>();
while (srcUidsIterator.hasNext() &&
destUidsIterator.hasNext()) {
String srcUid = srcUidsIterator.next();
String destUid = destUidsIterator.next();
uidMap.put(srcUid, destUid);
}
} else {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) {
Log.v(LOG_TAG, "Parse error: size of source UIDs " +
"list is not the same as size of destination " +
"UIDs list.");
}
}
} else {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) {
Log.v(LOG_TAG, "Parsing of the sequence set failed.");
}
}
}
}
}
return uidMap;
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
}
@Override
public Map<String, String> moveMessages(List<? extends Message> messages, Folder folder) throws MessagingException {
if (messages.isEmpty())
return null;
Map<String, String> uidMap = copyMessages(messages, folder);
setFlags(messages, Collections.singleton(Flag.DELETED), true);
return uidMap;
}
@Override
public void delete(List<? extends Message> messages, String trashFolderName) throws MessagingException {
if (messages.isEmpty())
return;
if (trashFolderName == null || getName().equalsIgnoreCase(trashFolderName)) {
setFlags(messages, Collections.singleton(Flag.DELETED), true);
} else {
ImapFolder remoteTrashFolder = (ImapFolder)getStore().getFolder(trashFolderName);
String remoteTrashName = encodeString(encodeFolderName(remoteTrashFolder.getPrefixedName()));
2010-01-17 19:11:02 -05:00
if (!exists(remoteTrashName)) {
/*
* If the remote trash folder doesn't exist we try to create it.
*/
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "IMAPMessage.delete: attempting to create remote '" + trashFolderName + "' folder for " + getLogId());
remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
}
if (exists(remoteTrashName)) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "IMAPMessage.delete: copying remote " + messages.size() + " messages to '" + trashFolderName + "' for " + getLogId());
2010-01-02 21:00:20 -05:00
moveMessages(messages, remoteTrashFolder);
} else {
throw new MessagingException("IMAPMessage.delete: remote Trash folder " + trashFolderName + " does not exist and could not be created for " + getLogId()
, true);
}
}
}
@Override
public int getMessageCount() {
return mMessageCount;
}
private int getRemoteMessageCount(String criteria) throws MessagingException {
checkOpen(); //only need READ access
try {
int count = 0;
2010-07-11 11:39:26 -04:00
int start = 1;
List<ImapResponse> responses = executeSimpleCommand(String.format(Locale.US, "SEARCH %d:* %s", start, criteria));
for (ImapResponse response : responses) {
if (ImapResponseParser.equalsIgnoreCase(response.get(0), "SEARCH")) {
count += response.size() - 1;
}
}
return count;
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
}
@Override
public int getUnreadMessageCount() throws MessagingException {
return getRemoteMessageCount("UNSEEN NOT DELETED");
}
@Override
public int getFlaggedMessageCount() throws MessagingException {
return getRemoteMessageCount("FLAGGED NOT DELETED");
}
protected long getHighestUid() {
try {
ImapSearcher searcher = new ImapSearcher() {
@Override
public List<ImapResponse> search() throws IOException, MessagingException {
return executeSimpleCommand("UID SEARCH *:*");
}
};
List<? extends Message> messages = search(searcher, null);
if (messages.size() > 0) {
return Long.parseLong(messages.get(0).getUid());
}
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Unable to find highest UID in folder " + getName(), e);
}
return -1L;
}
@Override
public void delete(boolean recurse) throws MessagingException {
throw new Error("ImapStore.delete() not yet implemented");
}
@Override
public ImapMessage getMessage(String uid) throws MessagingException {
return new ImapMessage(uid, this);
}
@Override
public List<ImapMessage> getMessages(int start, int end, Date earliestDate, MessageRetrievalListener<ImapMessage> listener)
throws MessagingException {
return getMessages(start, end, earliestDate, false, listener);
}
protected List<ImapMessage> getMessages(final int start, final int end, Date earliestDate, final boolean includeDeleted, final MessageRetrievalListener<ImapMessage> listener)
throws MessagingException {
if (start < 1 || end < 1 || end < start) {
throw new MessagingException(
String.format(Locale.US, "Invalid message set %d %d",
start, end));
}
final StringBuilder dateSearchString = new StringBuilder();
if (earliestDate != null) {
dateSearchString.append(" SINCE ");
synchronized (RFC3501_DATE) {
dateSearchString.append(RFC3501_DATE.format(earliestDate));
}
}
ImapSearcher searcher = new ImapSearcher() {
@Override
public List<ImapResponse> search() throws IOException, MessagingException {
return executeSimpleCommand(String.format(Locale.US, "UID SEARCH %d:%d%s%s", start, end, dateSearchString, includeDeleted ? "" : " NOT DELETED"));
}
};
return search(searcher, listener);
}
2014-12-18 03:33:09 -05:00
protected List<ImapMessage> getMessages(final List<Long> mesgSeqs,
final boolean includeDeleted,
final MessageRetrievalListener<ImapMessage> listener)
throws MessagingException {
ImapSearcher searcher = new ImapSearcher() {
@Override
public List<ImapResponse> search() throws IOException, MessagingException {
return executeSimpleCommand(String.format("UID SEARCH %s%s", combine(mesgSeqs.toArray(), ','), includeDeleted ? "" : " NOT DELETED"));
}
};
return search(searcher, listener);
}
2014-12-18 03:33:09 -05:00
protected List<? extends Message> getMessagesFromUids(final List<String> mesgUids,
final boolean includeDeleted,
final MessageRetrievalListener<ImapMessage> listener) throws MessagingException {
ImapSearcher searcher = new ImapSearcher() {
@Override
public List<ImapResponse> search() throws IOException, MessagingException {
return executeSimpleCommand(String.format("UID SEARCH UID %s%s", combine(mesgUids.toArray(), ','), includeDeleted ? "" : " NOT DELETED"));
}
};
return search(searcher, listener);
}
2014-12-18 03:33:09 -05:00
protected List<ImapMessage> search(ImapSearcher searcher, MessageRetrievalListener<ImapMessage> listener) throws MessagingException {
checkOpen(); //only need READ access
2014-12-18 03:33:09 -05:00
List<ImapMessage> messages = new ArrayList<ImapMessage>();
try {
2014-10-04 06:45:45 -04:00
List<Long> uids = new ArrayList<Long>();
List<ImapResponse> responses = searcher.search(); //
for (ImapResponse response : responses) {
2014-12-18 05:24:43 -05:00
if (response.getTag() == null) {
if (ImapResponseParser.equalsIgnoreCase(response.get(0), "SEARCH")) {
for (int i = 1, count = response.size(); i < count; i++) {
uids.add(response.getLong(i));
}
}
}
}
// Sort the uids in numerically decreasing order
// By doing it in decreasing order, we ensure newest messages are dealt with first
// This makes the most sense when a limit is imposed, and also prevents UI from going
// crazy adding stuff at the top.
Collections.sort(uids, Collections.reverseOrder());
for (int i = 0, count = uids.size(); i < count; i++) {
String uid = uids.get(i).toString();
if (listener != null) {
listener.messageStarted(uid, i, count);
}
ImapMessage message = new ImapMessage(uid, this);
messages.add(message);
if (listener != null) {
listener.messageFinished(message, i, count);
}
}
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
return messages;
}
@Override
2014-12-18 03:33:09 -05:00
public List<ImapMessage> getMessages(MessageRetrievalListener<ImapMessage> listener) throws MessagingException {
return getMessages(null, listener);
}
@Override
2014-12-18 03:33:09 -05:00
public List<ImapMessage> getMessages(String[] uids, MessageRetrievalListener<ImapMessage> listener)
throws MessagingException {
checkOpen(); //only need READ access
2014-12-18 03:33:09 -05:00
List<ImapMessage> messages = new ArrayList<ImapMessage>();
try {
if (uids == null) {
List<ImapResponse> responses = executeSimpleCommand("UID SEARCH 1:* NOT DELETED");
2014-10-04 06:45:45 -04:00
List<String> tempUids = new ArrayList<String>();
for (ImapResponse response : responses) {
if (ImapResponseParser.equalsIgnoreCase(response.get(0), "SEARCH")) {
for (int i = 1, count = response.size(); i < count; i++) {
tempUids.add(response.getString(i));
}
}
}
uids = tempUids.toArray(EMPTY_STRING_ARRAY);
}
for (int i = 0, count = uids.length; i < count; i++) {
if (listener != null) {
listener.messageStarted(uids[i], i, count);
}
ImapMessage message = new ImapMessage(uids[i], this);
messages.add(message);
if (listener != null) {
listener.messageFinished(message, i, count);
}
}
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
return messages;
}
@Override
2014-12-19 03:54:17 -05:00
public void fetch(List<ImapMessage> messages, FetchProfile fp, MessageRetrievalListener<ImapMessage> listener)
throws MessagingException {
if (messages == null || messages.isEmpty()) {
return;
}
checkOpen(); //only need READ access
List<String> uids = new ArrayList<String>(messages.size());
HashMap<String, Message> messageMap = new HashMap<String, Message>();
2014-02-15 16:32:00 -05:00
for (Message msg : messages) {
String uid = msg.getUid();
uids.add(uid);
2014-02-15 16:32:00 -05:00
messageMap.put(uid, msg);
}
/*
* Figure out what command we are going to run:
* Flags - UID FETCH (FLAGS)
* Envelope - UID FETCH ([FLAGS] INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc)])
*
*/
2014-02-15 17:48:35 -05:00
Set<String> fetchFields = new LinkedHashSet<String>();
fetchFields.add("UID");
if (fp.contains(FetchProfile.Item.FLAGS)) {
fetchFields.add("FLAGS");
}
if (fp.contains(FetchProfile.Item.ENVELOPE)) {
fetchFields.add("INTERNALDATE");
fetchFields.add("RFC822.SIZE");
fetchFields.add("BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc " +
2014-12-16 06:51:52 -05:00
"reply-to message-id references in-reply-to " + K9MailLib.IDENTITY_HEADER + ")]");
}
if (fp.contains(FetchProfile.Item.STRUCTURE)) {
fetchFields.add("BODYSTRUCTURE");
}
if (fp.contains(FetchProfile.Item.BODY_SANE)) {
// If the user wants to download unlimited-size messages, don't go only for the truncated body
if (mStoreConfig.getMaximumAutoDownloadMessageSize() > 0) {
fetchFields.add(String.format(Locale.US, "BODY.PEEK[]<0.%d>", mStoreConfig.getMaximumAutoDownloadMessageSize()));
} else {
fetchFields.add("BODY.PEEK[]");
}
}
if (fp.contains(FetchProfile.Item.BODY)) {
fetchFields.add("BODY.PEEK[]");
}
for (int windowStart = 0; windowStart < messages.size(); windowStart += (FETCH_WINDOW_SIZE)) {
List<String> uidWindow = uids.subList(windowStart, Math.min((windowStart + FETCH_WINDOW_SIZE), messages.size()));
try {
mConnection.sendCommand(String.format("UID FETCH %s (%s)",
combine(uidWindow.toArray(new String[uidWindow.size()]), ','),
combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
), false);
ImapResponse response;
int messageNumber = 0;
2014-12-18 05:24:43 -05:00
ImapResponseCallback callback = null;
if (fp.contains(FetchProfile.Item.BODY) || fp.contains(FetchProfile.Item.BODY_SANE)) {
callback = new FetchBodyCallback(messageMap);
}
2010-01-02 21:00:20 -05:00
do {
response = mConnection.readResponse(callback);
2014-12-18 05:24:43 -05:00
if (response.getTag() == null && ImapResponseParser.equalsIgnoreCase(response.get(1), "FETCH")) {
ImapList fetchList = (ImapList)response.getKeyedValue("FETCH");
String uid = fetchList.getKeyedString("UID");
long msgSeq = response.getLong(0);
if (uid != null) {
try {
msgSeqUidMap.put(msgSeq, uid);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) {
Log.v(LOG_TAG, "Stored uid '" + uid + "' for msgSeq " + msgSeq + " into map " /*+ msgSeqUidMap.toString() */);
}
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Unable to store uid '" + uid + "' for msgSeq " + msgSeq);
}
}
Message message = messageMap.get(uid);
if (message == null) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Do not have message in messageMap for UID " + uid + " for " + getLogId());
2010-01-02 21:00:20 -05:00
handleUntaggedResponse(response);
continue;
}
if (listener != null) {
listener.messageStarted(uid, messageNumber++, messageMap.size());
}
ImapMessage imapMessage = (ImapMessage) message;
Object literal = handleFetchResponse(imapMessage, fetchList);
if (literal != null) {
if (literal instanceof String) {
String bodyString = (String)literal;
InputStream bodyStream = new ByteArrayInputStream(bodyString.getBytes());
imapMessage.parse(bodyStream);
} else if (literal instanceof Integer) {
// All the work was done in FetchBodyCallback.foundLiteral()
} else {
// This shouldn't happen
throw new MessagingException("Got FETCH response with bogus parameters");
}
}
if (listener != null) {
listener.messageFinished(imapMessage, messageNumber, messageMap.size());
}
} else {
handleUntaggedResponse(response);
}
2014-12-18 05:24:43 -05:00
} while (response.getTag() == null);
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
}
}
@Override
2014-12-18 03:33:09 -05:00
public void fetchPart(Message message, Part part, MessageRetrievalListener<Message> listener)
throws MessagingException {
checkOpen(); //only need READ access
String[] parts = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
if (parts == null) {
return;
}
String fetch;
String partId = parts[0];
if ("TEXT".equalsIgnoreCase(partId)) {
fetch = String.format(Locale.US, "BODY.PEEK[TEXT]<0.%d>",
mStoreConfig.getMaximumAutoDownloadMessageSize());
} else {
fetch = String.format("BODY.PEEK[%s]", partId);
}
try {
mConnection.sendCommand(
String.format("UID FETCH %s (UID %s)", message.getUid(), fetch),
false);
ImapResponse response;
int messageNumber = 0;
2014-12-18 05:24:43 -05:00
ImapResponseCallback callback = new FetchPartCallback(part);
do {
response = mConnection.readResponse(callback);
2014-12-18 05:24:43 -05:00
if ((response.getTag() == null) &&
(ImapResponseParser.equalsIgnoreCase(response.get(1), "FETCH"))) {
ImapList fetchList = (ImapList)response.getKeyedValue("FETCH");
String uid = fetchList.getKeyedString("UID");
if (!message.getUid().equals(uid)) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Did not ask for UID " + uid + " for " + getLogId());
handleUntaggedResponse(response);
continue;
}
if (listener != null) {
listener.messageStarted(uid, messageNumber++, 1);
}
ImapMessage imapMessage = (ImapMessage) message;
2010-02-14 10:51:09 -05:00
Object literal = handleFetchResponse(imapMessage, fetchList);
if (literal != null) {
if (literal instanceof Body) {
// Most of the work was done in FetchAttchmentCallback.foundLiteral()
part.setBody((Body)literal);
} else if (literal instanceof String) {
2010-02-14 10:51:09 -05:00
String bodyString = (String)literal;
InputStream bodyStream = new ByteArrayInputStream(bodyString.getBytes());
Recursively convert attachments of type message/rfc822 to 7bit if necessary. The preceding commit resulted in attachments of type message/rfc822 being sent with 8bit encoding even when the SMTP server did not support 8BITMIME. This commit assures that messages will be converted to 7bit when necessary. A new interface CompositeBody was created that extends Body, and classes Message and Multipart were changed from implementing Body to CompositeBody. Additional classes BinaryTempFileMessageBody and LocalAttachmentMessageBody were created (by extending BinaryTempFileBody and LocalAttachmentBody, respectively), and they too implement CompositeBody. A CompositeBody is a Body containing a composite-type that can contain subparts that may require recursive processing when converting from 8bit to 7bit. The Part to which a CompositeBody belongs is only permitted to use 8bit or 7bit encoding for the CompositeBody. Previously, a Message was created so that it was 7bit clean by default (even though that meant base64 encoding all attachments, including messages). Then, if the SMTP server supported 8BITMIME, Message.setEncoding("8bit") was called so that bodies of type TextBody would been transmitted using 8bit encoding rather than quoted-printable. Now, messages are created with 8bit encoding by default. Then, if the SMTP server does not support 8BITMIME, Message.setUsing7bitTransport is called to recursively convert the message and its subparts to 7bit. The method setUsing7bitTransport was added to the interfaces Part and CompositeBody. setEncoding no longer iterates over parts in Multipart. That task belongs to setUsing7bitTransport, which may in turn call setEncoding on the parts. MimeUtility.getEncodingforType was created as a helper function for choosing a default encoding that should be used for a given MIME type when an attachment is added to a message (either while composing or when retrieving from LocalStore). setEncoding was implemented in MimeBodyPart to assure that the encoding set in the Part's headers was the same as set for the Part's Body. (The method already existed in MimeMessage, which has similarities with MimeBodyPart.) MimeMessage.parse(InputStream in, boolean recurse) was implemented so that the parser could be told to recursively process nested messages read from the InputStream, thus giving access to all subparts at any level that may need to be converted from 8bit to 7bit.
2013-09-02 23:49:28 -04:00
String contentTransferEncoding = part
.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0];
String contentType = part
.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0];
part.setBody(MimeUtility.decodeBody(bodyStream,
contentTransferEncoding, contentType));
} else {
// This shouldn't happen
throw new MessagingException("Got FETCH response with bogus parameters");
}
}
if (listener != null) {
listener.messageFinished(message, messageNumber, 1);
}
} else {
handleUntaggedResponse(response);
}
2014-12-18 05:24:43 -05:00
} while (response.getTag() == null);
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
}
// Returns value of body field
private Object handleFetchResponse(ImapMessage message, ImapList fetchList) throws MessagingException {
Object result = null;
if (fetchList.containsKey("FLAGS")) {
ImapList flags = fetchList.getKeyedList("FLAGS");
if (flags != null) {
for (int i = 0, count = flags.size(); i < count; i++) {
String flag = flags.getString(i);
if (flag.equalsIgnoreCase("\\Deleted")) {
message.setFlagInternal(Flag.DELETED, true);
} else if (flag.equalsIgnoreCase("\\Answered")) {
message.setFlagInternal(Flag.ANSWERED, true);
} else if (flag.equalsIgnoreCase("\\Seen")) {
message.setFlagInternal(Flag.SEEN, true);
} else if (flag.equalsIgnoreCase("\\Flagged")) {
message.setFlagInternal(Flag.FLAGGED, true);
2012-08-20 22:09:34 -04:00
} else if (flag.equalsIgnoreCase("$Forwarded")) {
message.setFlagInternal(Flag.FORWARDED, true);
/* a message contains FORWARDED FLAG -> so we can also create them */
mPermanentFlagsIndex.add(Flag.FORWARDED);
}
}
}
}
if (fetchList.containsKey("INTERNALDATE")) {
Date internalDate = fetchList.getKeyedDate("INTERNALDATE");
message.setInternalDate(internalDate);
}
if (fetchList.containsKey("RFC822.SIZE")) {
int size = fetchList.getKeyedNumber("RFC822.SIZE");
message.setSize(size);
}
if (fetchList.containsKey("BODYSTRUCTURE")) {
ImapList bs = fetchList.getKeyedList("BODYSTRUCTURE");
if (bs != null) {
try {
parseBodyStructure(bs, message, "TEXT");
} catch (MessagingException e) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Error handling message for " + getLogId(), e);
message.setBody(null);
}
}
}
if (fetchList.containsKey("BODY")) {
int index = fetchList.getKeyIndex("BODY") + 2;
int size = fetchList.size();
if (index < size) {
result = fetchList.getObject(index);
// Check if there's an origin octet
if (result instanceof String) {
String originOctet = (String) result;
if (originOctet.startsWith("<") && (index + 1) < size) {
result = fetchList.getObject(index + 1);
}
}
}
}
return result;
}
/**
* Handle any untagged responses that the caller doesn't care to handle themselves.
*/
protected List<ImapResponse> handleUntaggedResponses(List<ImapResponse> responses) {
for (ImapResponse response : responses) {
handleUntaggedResponse(response);
}
return responses;
}
protected void handlePossibleUidNext(ImapResponse response) {
if (ImapResponseParser.equalsIgnoreCase(response.get(0), "OK") && response.size() > 1) {
Object bracketedObj = response.get(1);
if (bracketedObj instanceof ImapList) {
ImapList bracketed = (ImapList)bracketedObj;
if (bracketed.size() > 1) {
Object keyObj = bracketed.get(0);
if (keyObj instanceof String) {
String key = (String)keyObj;
if ("UIDNEXT".equalsIgnoreCase(key)) {
uidNext = bracketed.getLong(1);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Got UidNext = " + uidNext + " for " + getLogId());
}
}
}
}
}
}
/**
* Handle an untagged response that the caller doesn't care to handle themselves.
*/
protected void handleUntaggedResponse(ImapResponse response) {
2014-12-18 05:24:43 -05:00
if (response.getTag() == null && response.size() > 1) {
if (ImapResponseParser.equalsIgnoreCase(response.get(1), "EXISTS")) {
mMessageCount = response.getNumber(0);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Got untagged EXISTS with value " + mMessageCount + " for " + getLogId());
}
handlePossibleUidNext(response);
if (ImapResponseParser.equalsIgnoreCase(response.get(1), "EXPUNGE") && mMessageCount > 0) {
mMessageCount--;
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Got untagged EXPUNGE with mMessageCount " + mMessageCount + " for " + getLogId());
}
2009-11-07 15:27:30 -05:00
// if (response.size() > 1) {
// Object bracketedObj = response.get(1);
// if (bracketedObj instanceof ImapList)
// {
// ImapList bracketed = (ImapList)bracketedObj;
//
2011-10-06 12:28:14 -04:00
// if (!bracketed.isEmpty())
2009-11-07 15:27:30 -05:00
// {
// Object keyObj = bracketed.get(0);
// if (keyObj instanceof String)
// {
// String key = (String)keyObj;
// if ("ALERT".equalsIgnoreCase(key))
2009-11-07 15:27:30 -05:00
// {
// StringBuilder sb = new StringBuilder();
2009-11-07 15:27:30 -05:00
// for (int i = 2, count = response.size(); i < count; i++) {
// sb.append(response.get(i).toString());
// sb.append(' ');
// }
//
2014-12-16 06:51:52 -05:00
// Log.w(LOG_TAG, "ALERT: " + sb.toString() + " for " + getLogId());
2009-11-07 15:27:30 -05:00
// }
// }
// }
//
//
2009-11-07 15:27:30 -05:00
// }
// }
}
2014-12-16 06:51:52 -05:00
//Log.i(LOG_TAG, "mMessageCount = " + mMessageCount + " for " + getLogId());
}
private void parseBodyStructure(ImapList bs, Part part, String id)
throws MessagingException {
if (bs.get(0) instanceof ImapList) {
/*
* This is a multipart/*
*/
MimeMultipart mp = new MimeMultipart();
for (int i = 0, count = bs.size(); i < count; i++) {
if (bs.get(i) instanceof ImapList) {
/*
* For each part in the message we're going to add a new BodyPart and parse
* into it.
*/
MimeBodyPart bp = new MimeBodyPart();
if (id.equalsIgnoreCase("TEXT")) {
parseBodyStructure(bs.getList(i), bp, Integer.toString(i + 1));
} else {
parseBodyStructure(bs.getList(i), bp, id + "." + (i + 1));
}
mp.addBodyPart(bp);
} else {
/*
* We've got to the end of the children of the part, so now we can find out
* what type it is and bail out.
*/
String subType = bs.getString(i);
mp.setSubType(subType.toLowerCase(Locale.US));
break;
}
}
part.setBody(mp);
} else {
/*
* This is a body. We need to add as much information as we can find out about
* it to the Part.
*/
/*
* 0| 0 body type
* 1| 1 body subtype
* 2| 2 body parameter parenthesized list
* 3| 3 body id (unused)
* 4| 4 body description (unused)
* 5| 5 body encoding
* 6| 6 body size
* -| 7 text lines (only for type TEXT, unused)
* Extensions (optional):
* 7| 8 body MD5 (unused)
* 8| 9 body disposition
* 9|10 body language (unused)
* 10|11 body location (unused)
*/
String type = bs.getString(0);
String subType = bs.getString(1);
String mimeType = (type + "/" + subType).toLowerCase(Locale.US);
ImapList bodyParams = null;
if (bs.get(2) instanceof ImapList) {
bodyParams = bs.getList(2);
}
String encoding = bs.getString(5);
int size = bs.getNumber(6);
if (MimeUtility.mimeTypeMatches(mimeType, "message/rfc822")) {
// A body type of type MESSAGE and subtype RFC822
// contains, immediately after the basic fields, the
// envelope structure, body structure, and size in
// text lines of the encapsulated message.
// [MESSAGE, RFC822, [NAME, Fwd: [#HTR-517941]: update plans at 1am Friday - Memory allocation - displayware.eml], NIL, NIL, 7BIT, 5974, NIL, [INLINE, [FILENAME*0, Fwd: [#HTR-517941]: update plans at 1am Friday - Memory all, FILENAME*1, ocation - displayware.eml]], NIL]
/*
* This will be caught by fetch and handled appropriately.
*/
throw new MessagingException("BODYSTRUCTURE message/rfc822 not yet supported.");
}
/*
* Set the content type with as much information as we know right now.
*/
StringBuilder contentType = new StringBuilder();
contentType.append(mimeType);
if (bodyParams != null) {
/*
* If there are body params we might be able to get some more information out
* of them.
*/
for (int i = 0, count = bodyParams.size(); i < count; i += 2) {
contentType.append(String.format(";\r\n %s=\"%s\"",
bodyParams.getString(i),
bodyParams.getString(i + 1)));
}
}
part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
// Extension items
ImapList bodyDisposition = null;
if (("text".equalsIgnoreCase(type))
&& (bs.size() > 9)
&& (bs.get(9) instanceof ImapList)) {
bodyDisposition = bs.getList(9);
} else if (!("text".equalsIgnoreCase(type))
&& (bs.size() > 8)
&& (bs.get(8) instanceof ImapList)) {
bodyDisposition = bs.getList(8);
}
StringBuilder contentDisposition = new StringBuilder();
2011-10-06 12:28:14 -04:00
if (bodyDisposition != null && !bodyDisposition.isEmpty()) {
if (!"NIL".equalsIgnoreCase(bodyDisposition.getString(0))) {
contentDisposition.append(bodyDisposition.getString(0).toLowerCase(Locale.US));
}
if ((bodyDisposition.size() > 1)
&& (bodyDisposition.get(1) instanceof ImapList)) {
ImapList bodyDispositionParams = bodyDisposition.getList(1);
/*
* If there is body disposition information we can pull some more information
* about the attachment out.
*/
for (int i = 0, count = bodyDispositionParams.size(); i < count; i += 2) {
contentDisposition.append(String.format(";\r\n %s=\"%s\"",
bodyDispositionParams.getString(i).toLowerCase(Locale.US),
bodyDispositionParams.getString(i + 1)));
}
}
}
if (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") == null) {
contentDisposition.append(String.format(Locale.US, ";\r\n size=%d", size));
}
/*
* Set the content disposition containing at least the size. Attachment
* handling code will use this down the road.
*/
part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition.toString());
/*
* Set the Content-Transfer-Encoding header. Attachment code will use this
* to parse the body.
*/
part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding);
if (part instanceof ImapMessage) {
((ImapMessage) part).setSize(size);
}
part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
}
}
/**
* Appends the given messages to the selected folder.
*
* <p>
* This implementation also determines the new UIDs of the given messages on the IMAP
* server and changes the messages' UIDs to the new server UIDs.
* </p>
*
* @param messages
* The messages to append to the folder.
*
* @return The mapping of original message UIDs to the new server UIDs.
*/
@Override
public Map<String, String> appendMessages(List<? extends Message> messages) throws MessagingException {
open(OPEN_MODE_RW);
checkOpen();
try {
Map<String, String> uidMap = new HashMap<String, String>();
for (Message message : messages) {
mConnection.sendCommand(
String.format(Locale.US, "APPEND %s (%s) {%d}",
encodeString(encodeFolderName(getPrefixedName())),
combineFlags(message.getFlags()),
message.calculateSize()), false);
ImapResponse response;
do {
response = mConnection.readResponse();
handleUntaggedResponse(response);
2014-12-18 05:24:43 -05:00
if (response.isContinuationRequested()) {
2014-12-17 11:16:18 -05:00
EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(mConnection.getOutputStream());
message.writeTo(eolOut);
eolOut.write('\r');
eolOut.write('\n');
eolOut.flush();
}
2014-12-18 05:24:43 -05:00
} while (response.getTag() == null);
if (response.size() > 1) {
/*
* If the server supports UIDPLUS, then along with the APPEND response it
* will return an APPENDUID response code, e.g.
*
* 11 OK [APPENDUID 2 238268] APPEND completed
*
* We can use the UID included in this response to update our records.
*/
Object responseList = response.get(1);
if (responseList instanceof ImapList) {
ImapList appendList = (ImapList) responseList;
if (appendList.size() >= 3 &&
appendList.getString(0).equals("APPENDUID")) {
String newUid = appendList.getString(2);
2014-12-11 16:48:22 -05:00
if (!TextUtils.isEmpty(newUid)) {
message.setUid(newUid);
uidMap.put(message.getUid(), newUid);
continue;
}
}
}
}
/*
* This part is executed in case the server does not support UIDPLUS or does
* not implement the APPENDUID response code.
*/
Complete merge of DAmail functionality into K9mail. Following features are added to K9mail: 1) Show unread message count on each folder 2) Sum unread count of all shown folders in an account to the account display 3) Periodically check selected folders for new mail, not just Inbox 4) Don't refresh folder when opened (unless folder is empty) 5) Show date and time of last sync for each folder 6) Fix timer for automatic periodic sync (use wakelock to assure completion) 7) Optimize local folder queries (speeds up account and folder lists) 8) Show Loading... message in status bar indicating which folder is being synced 9) Eliminate redundant sync of new messages (performance enhancement) 10) Improve notification text for multiple accounts 11) Do not automatically sync folders more often than the account-specific period 12) Use user-configured date and time formats 13) Select which folders are shown, using configurable Classes 14) Select which folders are synced, using configurable Classes 15) Added context (long press) menu to folders, to provide for Refresh and Folder Settings 16) Status light flashes purple when there are unread messages 17) Folder list more quickly eliminates display of deleted and out-of-Class folders. 18) Delete works 19) Mark all messages as read (in the folder context menu) 20) Notifications only for new unread messages 21) One minute synchronization frequency 22) Deleting an unread message decrements unread counter 23) Notifications work for POP3 accounts 24) Message deletes work for POP3 accounts 25) Explicit errors show in folder list 26) Stack traces saved to folder K9mail-errors 27) Clear pending actions (danger, for emergencies only!) 28) Delete policy in Account settings 29) DNS cache in InetAddress disabled 30) Trapped some crash-causing error conditions 31) Eliminate duplicate copies to Sent folder 32) Prevent crashes due to message listener concurrency 33) Empty Trash 34) Nuclear "Mark all messages as read" (marks all messages as read in server-side folder, irrespective of which messages have been downloaded) 35) Forward (alternate) to allow forwarding email through other programs 36) Accept text/plain Intents to allow other programs to send email through K9mail 37) Displays Outbox sending status 38) Manual retry of outbox sending when "Refresh"ing Outbox 39) Folder error status is persisted 40) Ability to log to arbitrary file Fixes K9 issues 11, 23, 24, 65, 69, 71, 79, 81, 82, 83, 87, 101, 104, 107, 120, 148, 154
2008-12-30 22:49:09 -05:00
String newUid = getUidFromMessageId(message);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) {
Log.d(LOG_TAG, "Got UID " + newUid + " for message for " + getLogId());
}
2014-12-11 16:48:22 -05:00
if (!TextUtils.isEmpty(newUid)) {
uidMap.put(message.getUid(), newUid);
message.setUid(newUid);
}
}
/*
* We need uidMap to be null if new UIDs are not available to maintain consistency
* with the behavior of other similar methods (copyMessages, moveMessages) which
* return null.
*/
return (uidMap.isEmpty()) ? null : uidMap;
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
}
@Override
public String getUidFromMessageId(Message message) throws MessagingException {
try {
/*
* Try to find the UID of the message we just appended using the
* Message-ID header.
*/
String[] messageIdHeader = message.getHeader("Message-ID");
if (messageIdHeader == null || messageIdHeader.length == 0) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Did not get a message-id in order to search for UID for " + getLogId());
return null;
}
String messageId = messageIdHeader[0];
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Looking for UID for message with message-id " + messageId + " for " + getLogId());
List<ImapResponse> responses =
executeSimpleCommand(
String.format("UID SEARCH HEADER MESSAGE-ID %s", encodeString(messageId)));
for (ImapResponse response1 : responses) {
2014-12-18 05:24:43 -05:00
if (response1.getTag() == null && ImapResponseParser.equalsIgnoreCase(response1.get(0), "SEARCH")
&& response1.size() > 1) {
return response1.getString(1);
}
}
return null;
} catch (IOException ioe) {
throw new MessagingException("Could not find UID for message based on Message-ID", ioe);
}
}
@Override
public void expunge() throws MessagingException {
open(OPEN_MODE_RW);
checkOpen();
try {
executeSimpleCommand("EXPUNGE");
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
}
private String combineFlags(Iterable<Flag> flags) {
2014-10-04 06:45:45 -04:00
List<String> flagNames = new ArrayList<String>();
for (Flag flag : flags) {
if (flag == Flag.SEEN) {
flagNames.add("\\Seen");
} else if (flag == Flag.DELETED) {
flagNames.add("\\Deleted");
} else if (flag == Flag.ANSWERED) {
flagNames.add("\\Answered");
} else if (flag == Flag.FLAGGED) {
flagNames.add("\\Flagged");
2012-08-20 22:09:34 -04:00
} else if (flag == Flag.FORWARDED
&& (mCanCreateKeywords || mPermanentFlagsIndex.contains(Flag.FORWARDED))) {
flagNames.add("$Forwarded");
}
}
return combine(flagNames.toArray(new String[flagNames.size()]), ' ');
}
@Override
public void setFlags(Set<Flag> flags, boolean value)
throws MessagingException {
open(OPEN_MODE_RW);
checkOpen();
try {
executeSimpleCommand(String.format("UID STORE 1:* %sFLAGS.SILENT (%s)",
value ? "+" : "-", combineFlags(flags)));
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
}
@Override
public String getNewPushState(String oldPushStateS, Message message) {
try {
String messageUidS = message.getUid();
long messageUid = Long.parseLong(messageUidS);
ImapPushState oldPushState = ImapPushState.parse(oldPushStateS);
if (messageUid >= oldPushState.uidNext) {
long uidNext = messageUid + 1;
ImapPushState newPushState = new ImapPushState(uidNext);
return newPushState.toString();
} else {
return null;
}
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Exception while updated push state for " + getLogId(), e);
return null;
}
}
Complete merge of DAmail functionality into K9mail. Following features are added to K9mail: 1) Show unread message count on each folder 2) Sum unread count of all shown folders in an account to the account display 3) Periodically check selected folders for new mail, not just Inbox 4) Don't refresh folder when opened (unless folder is empty) 5) Show date and time of last sync for each folder 6) Fix timer for automatic periodic sync (use wakelock to assure completion) 7) Optimize local folder queries (speeds up account and folder lists) 8) Show Loading... message in status bar indicating which folder is being synced 9) Eliminate redundant sync of new messages (performance enhancement) 10) Improve notification text for multiple accounts 11) Do not automatically sync folders more often than the account-specific period 12) Use user-configured date and time formats 13) Select which folders are shown, using configurable Classes 14) Select which folders are synced, using configurable Classes 15) Added context (long press) menu to folders, to provide for Refresh and Folder Settings 16) Status light flashes purple when there are unread messages 17) Folder list more quickly eliminates display of deleted and out-of-Class folders. 18) Delete works 19) Mark all messages as read (in the folder context menu) 20) Notifications only for new unread messages 21) One minute synchronization frequency 22) Deleting an unread message decrements unread counter 23) Notifications work for POP3 accounts 24) Message deletes work for POP3 accounts 25) Explicit errors show in folder list 26) Stack traces saved to folder K9mail-errors 27) Clear pending actions (danger, for emergencies only!) 28) Delete policy in Account settings 29) DNS cache in InetAddress disabled 30) Trapped some crash-causing error conditions 31) Eliminate duplicate copies to Sent folder 32) Prevent crashes due to message listener concurrency 33) Empty Trash 34) Nuclear "Mark all messages as read" (marks all messages as read in server-side folder, irrespective of which messages have been downloaded) 35) Forward (alternate) to allow forwarding email through other programs 36) Accept text/plain Intents to allow other programs to send email through K9mail 37) Displays Outbox sending status 38) Manual retry of outbox sending when "Refresh"ing Outbox 39) Folder error status is persisted 40) Ability to log to arbitrary file Fixes K9 issues 11, 23, 24, 65, 69, 71, 79, 81, 82, 83, 87, 101, 104, 107, 120, 148, 154
2008-12-30 22:49:09 -05:00
@Override
public void setFlags(List<? extends Message> messages, final Set<Flag> flags, boolean value)
throws MessagingException {
open(OPEN_MODE_RW);
checkOpen();
String[] uids = new String[messages.size()];
for (int i = 0, count = messages.size(); i < count; i++) {
uids[i] = messages.get(i).getUid();
}
try {
executeSimpleCommand(String.format("UID STORE %s %sFLAGS.SILENT (%s)",
combine(uids, ','),
value ? "+" : "-",
2011-09-29 23:39:36 -04:00
combineFlags(flags)));
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
}
private void checkOpen() throws MessagingException {
if (!isOpen()) {
throw new MessagingException("Folder " + getPrefixedName() + " is not open.");
}
}
private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "IOException for " + getLogId(), ioe);
if (connection != null) {
connection.close();
}
close();
return new MessagingException("IO Error", ioe);
}
@Override
public boolean equals(Object o) {
if (o instanceof ImapFolder) {
return ((ImapFolder)o).getName().equalsIgnoreCase(getName());
}
return super.equals(o);
}
@Override
public int hashCode() {
return getName().hashCode();
}
Complete merge of DAmail functionality into K9mail. Following features are added to K9mail: 1) Show unread message count on each folder 2) Sum unread count of all shown folders in an account to the account display 3) Periodically check selected folders for new mail, not just Inbox 4) Don't refresh folder when opened (unless folder is empty) 5) Show date and time of last sync for each folder 6) Fix timer for automatic periodic sync (use wakelock to assure completion) 7) Optimize local folder queries (speeds up account and folder lists) 8) Show Loading... message in status bar indicating which folder is being synced 9) Eliminate redundant sync of new messages (performance enhancement) 10) Improve notification text for multiple accounts 11) Do not automatically sync folders more often than the account-specific period 12) Use user-configured date and time formats 13) Select which folders are shown, using configurable Classes 14) Select which folders are synced, using configurable Classes 15) Added context (long press) menu to folders, to provide for Refresh and Folder Settings 16) Status light flashes purple when there are unread messages 17) Folder list more quickly eliminates display of deleted and out-of-Class folders. 18) Delete works 19) Mark all messages as read (in the folder context menu) 20) Notifications only for new unread messages 21) One minute synchronization frequency 22) Deleting an unread message decrements unread counter 23) Notifications work for POP3 accounts 24) Message deletes work for POP3 accounts 25) Explicit errors show in folder list 26) Stack traces saved to folder K9mail-errors 27) Clear pending actions (danger, for emergencies only!) 28) Delete policy in Account settings 29) DNS cache in InetAddress disabled 30) Trapped some crash-causing error conditions 31) Eliminate duplicate copies to Sent folder 32) Prevent crashes due to message listener concurrency 33) Empty Trash 34) Nuclear "Mark all messages as read" (marks all messages as read in server-side folder, irrespective of which messages have been downloaded) 35) Forward (alternate) to allow forwarding email through other programs 36) Accept text/plain Intents to allow other programs to send email through K9mail 37) Displays Outbox sending status 38) Manual retry of outbox sending when "Refresh"ing Outbox 39) Folder error status is persisted 40) Ability to log to arbitrary file Fixes K9 issues 11, 23, 24, 65, 69, 71, 79, 81, 82, 83, 87, 101, 104, 107, 120, 148, 154
2008-12-30 22:49:09 -05:00
protected ImapStore getStore() {
return store;
}
protected String getLogId() {
String id = mStoreConfig.toString() + ":" + getName() + "/" + Thread.currentThread().getName();
if (mConnection != null) {
id += "/" + mConnection.getLogId();
}
return id;
}
/**
* Search the remote ImapFolder.
* @param queryString String to query for.
* @param requiredFlags Mandatory flags
* @param forbiddenFlags Flags to exclude
* @return List of messages found
* @throws MessagingException On any error.
*/
@Override
2014-12-18 03:33:09 -05:00
public List<ImapMessage> search(final String queryString, final Set<Flag> requiredFlags, final Set<Flag> forbiddenFlags)
throws MessagingException {
if (!mStoreConfig.allowRemoteSearch()) {
throw new MessagingException("Your settings do not allow remote searching of this account");
}
// Setup the searcher
final ImapSearcher searcher = new ImapSearcher() {
@Override
public List<ImapResponse> search() throws IOException, MessagingException {
String imapQuery = "UID SEARCH ";
if (requiredFlags != null) {
for (Flag f : requiredFlags) {
switch (f) {
case DELETED:
imapQuery += "DELETED ";
break;
case SEEN:
imapQuery += "SEEN ";
break;
case ANSWERED:
imapQuery += "ANSWERED ";
break;
case FLAGGED:
imapQuery += "FLAGGED ";
break;
case DRAFT:
imapQuery += "DRAFT ";
break;
case RECENT:
imapQuery += "RECENT ";
break;
default:
break;
}
}
}
if (forbiddenFlags != null) {
for (Flag f : forbiddenFlags) {
switch (f) {
case DELETED:
imapQuery += "UNDELETED ";
break;
case SEEN:
imapQuery += "UNSEEN ";
break;
case ANSWERED:
imapQuery += "UNANSWERED ";
break;
case FLAGGED:
imapQuery += "UNFLAGGED ";
break;
case DRAFT:
imapQuery += "UNDRAFT ";
break;
case RECENT:
imapQuery += "UNRECENT ";
break;
default:
break;
}
}
}
final String encodedQry = encodeString(queryString);
if (mStoreConfig.isRemoteSearchFullText()) {
imapQuery += "TEXT " + encodedQry;
} else {
imapQuery += "OR SUBJECT " + encodedQry + " FROM " + encodedQry;
}
return executeSimpleCommand(imapQuery);
}
};
// Execute the search
try {
open(OPEN_MODE_RO);
checkOpen();
mInSearch = true;
// don't pass listener--we don't want to add messages until we've downloaded them
return search(searcher, null);
} finally {
mInSearch = false;
}
}
}
protected static class ImapMessage extends MimeMessage {
ImapMessage(String uid, Folder folder) {
this.mUid = uid;
this.mFolder = folder;
}
public void setSize(int size) {
this.mSize = size;
}
public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
super.setFlag(flag, set);
}
@Override
public void setFlag(Flag flag, boolean set) throws MessagingException {
super.setFlag(flag, set);
mFolder.setFlags(Collections.singletonList(this), Collections.singleton(flag), set);
}
Complete merge of DAmail functionality into K9mail. Following features are added to K9mail: 1) Show unread message count on each folder 2) Sum unread count of all shown folders in an account to the account display 3) Periodically check selected folders for new mail, not just Inbox 4) Don't refresh folder when opened (unless folder is empty) 5) Show date and time of last sync for each folder 6) Fix timer for automatic periodic sync (use wakelock to assure completion) 7) Optimize local folder queries (speeds up account and folder lists) 8) Show Loading... message in status bar indicating which folder is being synced 9) Eliminate redundant sync of new messages (performance enhancement) 10) Improve notification text for multiple accounts 11) Do not automatically sync folders more often than the account-specific period 12) Use user-configured date and time formats 13) Select which folders are shown, using configurable Classes 14) Select which folders are synced, using configurable Classes 15) Added context (long press) menu to folders, to provide for Refresh and Folder Settings 16) Status light flashes purple when there are unread messages 17) Folder list more quickly eliminates display of deleted and out-of-Class folders. 18) Delete works 19) Mark all messages as read (in the folder context menu) 20) Notifications only for new unread messages 21) One minute synchronization frequency 22) Deleting an unread message decrements unread counter 23) Notifications work for POP3 accounts 24) Message deletes work for POP3 accounts 25) Explicit errors show in folder list 26) Stack traces saved to folder K9mail-errors 27) Clear pending actions (danger, for emergencies only!) 28) Delete policy in Account settings 29) DNS cache in InetAddress disabled 30) Trapped some crash-causing error conditions 31) Eliminate duplicate copies to Sent folder 32) Prevent crashes due to message listener concurrency 33) Empty Trash 34) Nuclear "Mark all messages as read" (marks all messages as read in server-side folder, irrespective of which messages have been downloaded) 35) Forward (alternate) to allow forwarding email through other programs 36) Accept text/plain Intents to allow other programs to send email through K9mail 37) Displays Outbox sending status 38) Manual retry of outbox sending when "Refresh"ing Outbox 39) Folder error status is persisted 40) Ability to log to arbitrary file Fixes K9 issues 11, 23, 24, 65, 69, 71, 79, 81, 82, 83, 87, 101, 104, 107, 120, 148, 154
2008-12-30 22:49:09 -05:00
@Override
public void delete(String trashFolderName) throws MessagingException {
getFolder().delete(Collections.singletonList(this), trashFolderName);
}
}
protected class ImapFolderPusher extends ImapFolder implements UntaggedHandler {
private final PushReceiver receiver;
private Thread listeningThread = null;
private final AtomicBoolean stop = new AtomicBoolean(false);
private final AtomicBoolean idling = new AtomicBoolean(false);
private final AtomicBoolean doneSent = new AtomicBoolean(false);
private final AtomicInteger delayTime = new AtomicInteger(NORMAL_DELAY_TIME);
private final AtomicInteger idleFailureCount = new AtomicInteger(0);
private final AtomicBoolean needsPoll = new AtomicBoolean(false);
private List<ImapResponse> storedUntaggedResponses = new ArrayList<ImapResponse>();
private TracingWakeLock wakeLock = null;
public ImapFolderPusher(ImapStore store, String name, PushReceiver nReceiver) {
super(store, name);
receiver = nReceiver;
TracingPowerManager pm = TracingPowerManager.getPowerManager(receiver.getContext());
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ImapFolderPusher " + mStoreConfig.toString() + ":" + getName());
wakeLock.setReferenceCounted(false);
}
public void refresh() throws IOException, MessagingException {
if (idling.get()) {
2014-12-16 06:51:52 -05:00
wakeLock.acquire(PUSH_WAKE_LOCK_TIMEOUT);
sendDone();
}
}
private void sendDone() throws IOException, MessagingException {
if (doneSent.compareAndSet(false, true)) {
ImapConnection conn = mConnection;
if (conn != null) {
conn.setReadTimeout(SOCKET_READ_TIMEOUT);
sendContinuation("DONE");
}
}
}
private void sendContinuation(String continuation)
throws IOException {
ImapConnection conn = mConnection;
if (conn != null) {
conn.sendContinuation(continuation);
}
}
public void start() {
Runnable runner = new Runnable() {
@Override
public void run() {
2014-12-16 06:51:52 -05:00
wakeLock.acquire(PUSH_WAKE_LOCK_TIMEOUT);
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "Pusher starting for " + getLogId());
2010-01-02 21:00:20 -05:00
long lastUidNext = -1L;
while (!stop.get()) {
try {
long oldUidNext = -1L;
try {
String pushStateS = receiver.getPushState(getName());
ImapPushState pushState = ImapPushState.parse(pushStateS);
oldUidNext = pushState.uidNext;
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "Got oldUidNext " + oldUidNext + " for " + getLogId());
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Unable to get oldUidNext for " + getLogId(), e);
}
/*
* This makes sure 'oldUidNext' is never smaller than 'UIDNEXT' from
* the last loop iteration. This way we avoid looping endlessly causing
* the battery to drain.
*
* See issue 4907
*/
if (oldUidNext < lastUidNext) {
oldUidNext = lastUidNext;
}
ImapConnection oldConnection = mConnection;
internalOpen(OPEN_MODE_RO);
ImapConnection conn = mConnection;
if (conn == null) {
receiver.pushError("Could not establish connection for IDLE", null);
throw new MessagingException("Could not establish connection for IDLE");
}
if (!conn.isIdleCapable()) {
stop.set(true);
receiver.pushError("IMAP server is not IDLE capable: " + conn.toString(), null);
throw new MessagingException("IMAP server is not IDLE capable:" + conn.toString());
}
if (!stop.get() && mStoreConfig.isPushPollOnConnect() && (conn != oldConnection || needsPoll.getAndSet(false))) {
List<ImapResponse> untaggedResponses = new ArrayList<ImapResponse>(storedUntaggedResponses);
storedUntaggedResponses.clear();
processUntaggedResponses(untaggedResponses);
if (mMessageCount == -1) {
throw new MessagingException("Message count = -1 for idling");
}
receiver.syncFolder(ImapFolderPusher.this);
}
if (stop.get()) {
continue;
}
long startUid = oldUidNext;
long newUidNext = uidNext;
if (newUidNext == -1) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) {
Log.d(LOG_TAG, "uidNext is -1, using search to find highest UID");
}
long highestUid = getHighestUid();
if (highestUid != -1L) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "highest UID = " + highestUid);
newUidNext = highestUid + 1;
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "highest UID = " + highestUid
2010-05-11 22:51:59 -04:00
+ ", set newUidNext to " + newUidNext);
}
}
if (startUid < newUidNext - mStoreConfig.getDisplayCount()) {
startUid = newUidNext - mStoreConfig.getDisplayCount();
}
if (startUid < 1) {
startUid = 1;
}
lastUidNext = newUidNext;
if (newUidNext > startUid) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "Needs sync from uid " + startUid + " to " + newUidNext + " for " + getLogId());
List<Message> messages = new ArrayList<Message>();
for (long uid = startUid; uid < newUidNext; uid++) {
ImapMessage message = new ImapMessage("" + uid, ImapFolderPusher.this);
messages.add(message);
}
2011-10-06 12:28:14 -04:00
if (!messages.isEmpty()) {
pushMessages(messages, true);
}
} else {
2014-12-18 03:33:09 -05:00
List<ImapResponse> untaggedResponses;
2011-10-06 12:28:14 -04:00
while (!storedUntaggedResponses.isEmpty()) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "Processing " + storedUntaggedResponses.size() + " untagged responses from previous commands for " + getLogId());
untaggedResponses = new ArrayList<ImapResponse>(storedUntaggedResponses);
storedUntaggedResponses.clear();
processUntaggedResponses(untaggedResponses);
}
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "About to IDLE for " + getLogId());
receiver.setPushActive(getName(), true);
idling.set(true);
doneSent.set(false);
conn.setReadTimeout((mStoreConfig.getIdleRefreshMinutes() * 60 * 1000) + IDLE_READ_TIMEOUT_INCREMENT);
executeSimpleCommand(ImapCommands.COMMAND_IDLE, false, ImapFolderPusher.this);
idling.set(false);
delayTime.set(NORMAL_DELAY_TIME);
idleFailureCount.set(0);
}
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
wakeLock.acquire(PUSH_WAKE_LOCK_TIMEOUT);
storedUntaggedResponses.clear();
idling.set(false);
receiver.setPushActive(getName(), false);
try {
close();
} catch (Exception me) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Got exception while closing for exception for " + getLogId(), me);
}
if (stop.get()) {
2014-12-16 06:51:52 -05:00
Log.i(LOG_TAG, "Got exception while idling, but stop is set for " + getLogId());
} else {
receiver.pushError("Push error for " + getName(), e);
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Got exception while idling for " + getLogId(), e);
int delayTimeInt = delayTime.get();
receiver.sleep(wakeLock, delayTimeInt);
delayTimeInt *= 2;
if (delayTimeInt > MAX_DELAY_TIME) {
delayTimeInt = MAX_DELAY_TIME;
}
delayTime.set(delayTimeInt);
if (idleFailureCount.incrementAndGet() > IDLE_FAILURE_COUNT_LIMIT) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Disabling pusher for " + getLogId() + " after " + idleFailureCount.get() + " consecutive errors");
receiver.pushError("Push disabled for " + getName() + " after " + idleFailureCount.get() + " consecutive errors", e);
stop.set(true);
}
}
}
}
receiver.setPushActive(getName(), false);
try {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "Pusher for " + getLogId() + " is exiting");
close();
} catch (Exception me) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Got exception while closing for " + getLogId(), me);
} finally {
wakeLock.release();
}
}
};
listeningThread = new Thread(runner);
listeningThread.start();
}
@Override
protected void handleUntaggedResponse(ImapResponse response) {
2014-12-18 05:24:43 -05:00
if (response.getTag() == null && response.size() > 1) {
Object responseType = response.get(1);
if (ImapResponseParser.equalsIgnoreCase(responseType, "FETCH")
|| ImapResponseParser.equalsIgnoreCase(responseType, "EXPUNGE")
|| ImapResponseParser.equalsIgnoreCase(responseType, "EXISTS")) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Storing response " + response + " for later processing");
2010-01-02 21:00:20 -05:00
storedUntaggedResponses.add(response);
}
handlePossibleUidNext(response);
}
}
protected void processUntaggedResponses(List<ImapResponse> responses) throws MessagingException {
boolean skipSync = false;
int oldMessageCount = mMessageCount;
if (oldMessageCount == -1) {
skipSync = true;
}
List<Long> flagSyncMsgSeqs = new ArrayList<Long>();
List<String> removeMsgUids = new LinkedList<String>();
for (ImapResponse response : responses) {
oldMessageCount += processUntaggedResponse(oldMessageCount, response, flagSyncMsgSeqs, removeMsgUids);
}
if (!skipSync) {
if (oldMessageCount < 0) {
oldMessageCount = 0;
}
if (mMessageCount > oldMessageCount) {
syncMessages(mMessageCount, true);
}
}
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "UIDs for messages needing flag sync are " + flagSyncMsgSeqs + " for " + getLogId());
2010-01-02 21:00:20 -05:00
2011-10-06 12:28:14 -04:00
if (!flagSyncMsgSeqs.isEmpty()) {
syncMessages(flagSyncMsgSeqs);
}
2011-10-06 12:28:14 -04:00
if (!removeMsgUids.isEmpty()) {
removeMessages(removeMsgUids);
}
}
private void syncMessages(int end, boolean newArrivals) throws MessagingException {
long oldUidNext = -1L;
try {
String pushStateS = receiver.getPushState(getName());
ImapPushState pushState = ImapPushState.parse(pushStateS);
oldUidNext = pushState.uidNext;
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "Got oldUidNext " + oldUidNext + " for " + getLogId());
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Unable to get oldUidNext for " + getLogId(), e);
}
List<? extends Message> messageList = getMessages(end, end, null, true, null);
if (messageList != null && messageList.size() > 0) {
long newUid = Long.parseLong(messageList.get(0).getUid());
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "Got newUid " + newUid + " for message " + end + " on " + getLogId());
long startUid = oldUidNext;
if (startUid < newUid - 10) {
startUid = newUid - 10;
}
if (startUid < 1) {
startUid = 1;
}
if (newUid >= startUid) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "Needs sync from uid " + startUid + " to " + newUid + " for " + getLogId());
List<Message> messages = new ArrayList<Message>();
for (long uid = startUid; uid <= newUid; uid++) {
ImapMessage message = new ImapMessage(Long.toString(uid), ImapFolderPusher.this);
messages.add(message);
}
2011-10-06 12:28:14 -04:00
if (!messages.isEmpty()) {
pushMessages(messages, true);
}
}
}
}
private void syncMessages(List<Long> flagSyncMsgSeqs) {
try {
List<? extends Message> messageList = getMessages(flagSyncMsgSeqs, true, null);
List<Message> messages = new ArrayList<Message>();
messages.addAll(messageList);
pushMessages(messages, false);
} catch (Exception e) {
receiver.pushError("Exception while processing Push untagged responses", e);
}
}
private void removeMessages(List<String> removeUids) {
List<Message> messages = new ArrayList<Message>(removeUids.size());
try {
List<? extends Message> existingMessages = getMessagesFromUids(removeUids, true, null);
for (Message existingMessage : existingMessages) {
needsPoll.set(true);
msgSeqUidMap.clear();
String existingUid = existingMessage.getUid();
2014-12-16 06:51:52 -05:00
Log.w(LOG_TAG, "Message with UID " + existingUid + " still exists on server, not expunging");
removeUids.remove(existingUid);
}
for (String uid : removeUids) {
ImapMessage message = new ImapMessage(uid, this);
try {
message.setFlagInternal(Flag.DELETED, true);
} catch (MessagingException me) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Unable to set DELETED flag on message " + message.getUid());
}
messages.add(message);
}
receiver.messagesRemoved(this, messages);
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Cannot remove EXPUNGEd messages", e);
}
}
protected int processUntaggedResponse(long oldMessageCount, ImapResponse response, List<Long> flagSyncMsgSeqs, List<String> removeMsgUids) {
super.handleUntaggedResponse(response);
int messageCountDelta = 0;
2014-12-18 05:24:43 -05:00
if (response.getTag() == null && response.size() > 1) {
try {
Object responseType = response.get(1);
if (ImapResponseParser.equalsIgnoreCase(responseType, "FETCH")) {
2014-12-16 06:51:52 -05:00
Log.i(LOG_TAG, "Got FETCH " + response);
long msgSeq = response.getLong(0);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Got untagged FETCH for msgseq " + msgSeq + " for " + getLogId());
2010-01-02 21:00:20 -05:00
if (!flagSyncMsgSeqs.contains(msgSeq)) {
flagSyncMsgSeqs.add(msgSeq);
}
}
if (ImapResponseParser.equalsIgnoreCase(responseType, "EXPUNGE")) {
long msgSeq = response.getLong(0);
if (msgSeq <= oldMessageCount) {
messageCountDelta = -1;
}
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Got untagged EXPUNGE for msgseq " + msgSeq + " for " + getLogId());
2010-01-02 21:00:20 -05:00
List<Long> newSeqs = new ArrayList<Long>();
Iterator<Long> flagIter = flagSyncMsgSeqs.iterator();
while (flagIter.hasNext()) {
2012-07-07 09:41:55 -04:00
long flagMsg = flagIter.next();
if (flagMsg >= msgSeq) {
flagIter.remove();
if (flagMsg > msgSeq) {
newSeqs.add(flagMsg--);
}
}
}
flagSyncMsgSeqs.addAll(newSeqs);
List<Long> msgSeqs = new ArrayList<Long>(msgSeqUidMap.keySet());
Collections.sort(msgSeqs); // Have to do comparisons in order because of msgSeq reductions
for (long msgSeqNum : msgSeqs) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) {
Log.v(LOG_TAG, "Comparing EXPUNGEd msgSeq " + msgSeq + " to " + msgSeqNum);
}
if (msgSeqNum == msgSeq) {
String uid = msgSeqUidMap.get(msgSeqNum);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) {
Log.d(LOG_TAG, "Scheduling removal of UID " + uid + " because msgSeq " + msgSeqNum + " was expunged");
}
removeMsgUids.add(uid);
msgSeqUidMap.remove(msgSeqNum);
} else if (msgSeqNum > msgSeq) {
String uid = msgSeqUidMap.get(msgSeqNum);
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) {
Log.d(LOG_TAG, "Reducing msgSeq for UID " + uid + " from " + msgSeqNum + " to " + (msgSeqNum - 1));
}
msgSeqUidMap.remove(msgSeqNum);
msgSeqUidMap.put(msgSeqNum - 1, uid);
}
}
}
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Could not handle untagged FETCH for " + getLogId(), e);
}
}
return messageCountDelta;
}
private void pushMessages(List<Message> messages, boolean newArrivals) {
RuntimeException holdException = null;
try {
if (newArrivals) {
receiver.messagesArrived(this, messages);
} else {
receiver.messagesFlagsChanged(this, messages);
}
} catch (RuntimeException e) {
holdException = e;
}
if (holdException != null) {
throw holdException;
}
}
public void stop() {
stop.set(true);
if (listeningThread != null) {
listeningThread.interrupt();
}
ImapConnection conn = mConnection;
if (conn != null) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.v(LOG_TAG, "Closing mConnection to stop pushing for " + getLogId());
conn.close();
} else {
2014-12-16 06:51:52 -05:00
Log.w(LOG_TAG, "Attempt to interrupt null mConnection to stop pushing on folderPusher for " + getLogId());
}
}
@Override
public void handleAsyncUntaggedResponse(ImapResponse response) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.v(LOG_TAG, "Got async response: " + response);
2010-01-02 21:00:20 -05:00
if (stop.get()) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Got async untagged response: " + response + ", but stop is set for " + getLogId());
2010-01-02 21:00:20 -05:00
try {
sendDone();
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Exception while sending DONE for " + getLogId(), e);
}
} else {
2014-12-18 05:24:43 -05:00
if (response.getTag() == null) {
if (response.size() > 1) {
boolean started = false;
Object responseType = response.get(1);
if (ImapResponseParser.equalsIgnoreCase(responseType, "EXISTS") || ImapResponseParser.equalsIgnoreCase(responseType, "EXPUNGE") ||
ImapResponseParser.equalsIgnoreCase(responseType, "FETCH")) {
if (!started) {
2014-12-16 06:51:52 -05:00
wakeLock.acquire(PUSH_WAKE_LOCK_TIMEOUT);
started = true;
}
2010-01-02 21:00:20 -05:00
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Got useful async untagged response: " + response + " for " + getLogId());
2010-01-02 21:00:20 -05:00
try {
sendDone();
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Exception while sending DONE for " + getLogId(), e);
}
}
2014-12-18 05:24:43 -05:00
} else if (response.isContinuationRequested()) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.d(LOG_TAG, "Idling " + getLogId());
2010-01-02 21:00:20 -05:00
wakeLock.release();
}
}
}
}
}
@Override
public Pusher getPusher(PushReceiver receiver) {
return new ImapPusher(this, receiver);
}
public class ImapPusher implements Pusher {
private final ImapStore mStore;
final PushReceiver mReceiver;
private long lastRefresh = -1;
2014-09-28 05:12:19 -04:00
final Map<String, ImapFolderPusher> folderPushers = new HashMap<String, ImapFolderPusher>();
public ImapPusher(ImapStore store, PushReceiver receiver) {
mStore = store;
mReceiver = receiver;
}
@Override
public void start(List<String> folderNames) {
stop();
synchronized (folderPushers) {
setLastRefresh(System.currentTimeMillis());
for (String folderName : folderNames) {
ImapFolderPusher pusher = folderPushers.get(folderName);
if (pusher == null) {
pusher = new ImapFolderPusher(mStore, folderName, mReceiver);
folderPushers.put(folderName, pusher);
pusher.start();
}
}
}
}
@Override
public void refresh() {
synchronized (folderPushers) {
for (ImapFolderPusher folderPusher : folderPushers.values()) {
try {
folderPusher.refresh();
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Got exception while refreshing for " + folderPusher.getName(), e);
}
}
}
}
@Override
public void stop() {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "Requested stop of IMAP pusher");
2010-01-02 21:00:20 -05:00
synchronized (folderPushers) {
for (ImapFolderPusher folderPusher : folderPushers.values()) {
try {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug())
Log.i(LOG_TAG, "Requesting stop of IMAP folderPusher " + folderPusher.getName());
folderPusher.stop();
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Got exception while stopping " + folderPusher.getName(), e);
}
}
folderPushers.clear();
}
}
@Override
public int getRefreshInterval() {
return (mStoreConfig.getIdleRefreshMinutes() * 60 * 1000);
}
@Override
public long getLastRefresh() {
return lastRefresh;
}
@Override
public void setLastRefresh(long lastRefresh) {
this.lastRefresh = lastRefresh;
}
}
protected static class ImapPushState {
protected long uidNext;
protected ImapPushState(long nUidNext) {
uidNext = nUidNext;
}
protected static ImapPushState parse(String pushState) {
long newUidNext = -1L;
if (pushState != null) {
StringTokenizer tokenizer = new StringTokenizer(pushState, ";");
while (tokenizer.hasMoreTokens()) {
StringTokenizer thisState = new StringTokenizer(tokenizer.nextToken(), "=");
if (thisState.hasMoreTokens()) {
String key = thisState.nextToken();
if ("uidNext".equalsIgnoreCase(key) && thisState.hasMoreTokens()) {
String value = thisState.nextToken();
try {
newUidNext = Long.parseLong(value);
} catch (NumberFormatException e) {
2014-12-16 06:51:52 -05:00
Log.e(LOG_TAG, "Unable to part uidNext value " + value, e);
}
}
}
}
}
return new ImapPushState(newUidNext);
}
@Override
public String toString() {
return "uidNext=" + uidNext;
}
}
2014-12-18 03:33:09 -05:00
protected interface ImapSearcher {
List<ImapResponse> search() throws IOException, MessagingException;
}
2014-12-18 05:24:43 -05:00
private static class FetchBodyCallback implements ImapResponseCallback {
2014-10-04 06:45:45 -04:00
private Map<String, Message> mMessageMap;
2014-12-18 03:33:09 -05:00
FetchBodyCallback(Map<String, Message> messageMap) {
mMessageMap = messageMap;
}
@Override
public Object foundLiteral(ImapResponse response,
2014-12-18 05:24:43 -05:00
FixedLengthInputStream literal) throws MessagingException, IOException {
if (response.getTag() == null &&
ImapResponseParser.equalsIgnoreCase(response.get(1), "FETCH")) {
ImapList fetchList = (ImapList)response.getKeyedValue("FETCH");
String uid = fetchList.getKeyedString("UID");
ImapMessage message = (ImapMessage) mMessageMap.get(uid);
message.parse(literal);
// Return placeholder object
2014-12-18 05:24:43 -05:00
return 1;
}
return null;
}
}
2014-12-18 05:24:43 -05:00
private static class FetchPartCallback implements ImapResponseCallback {
private Part mPart;
FetchPartCallback(Part part) {
mPart = part;
}
@Override
public Object foundLiteral(ImapResponse response,
2014-12-18 05:24:43 -05:00
FixedLengthInputStream literal) throws MessagingException, IOException {
if (response.getTag() == null &&
ImapResponseParser.equalsIgnoreCase(response.get(1), "FETCH")) {
//TODO: check for correct UID
Recursively convert attachments of type message/rfc822 to 7bit if necessary. The preceding commit resulted in attachments of type message/rfc822 being sent with 8bit encoding even when the SMTP server did not support 8BITMIME. This commit assures that messages will be converted to 7bit when necessary. A new interface CompositeBody was created that extends Body, and classes Message and Multipart were changed from implementing Body to CompositeBody. Additional classes BinaryTempFileMessageBody and LocalAttachmentMessageBody were created (by extending BinaryTempFileBody and LocalAttachmentBody, respectively), and they too implement CompositeBody. A CompositeBody is a Body containing a composite-type that can contain subparts that may require recursive processing when converting from 8bit to 7bit. The Part to which a CompositeBody belongs is only permitted to use 8bit or 7bit encoding for the CompositeBody. Previously, a Message was created so that it was 7bit clean by default (even though that meant base64 encoding all attachments, including messages). Then, if the SMTP server supported 8BITMIME, Message.setEncoding("8bit") was called so that bodies of type TextBody would been transmitted using 8bit encoding rather than quoted-printable. Now, messages are created with 8bit encoding by default. Then, if the SMTP server does not support 8BITMIME, Message.setUsing7bitTransport is called to recursively convert the message and its subparts to 7bit. The method setUsing7bitTransport was added to the interfaces Part and CompositeBody. setEncoding no longer iterates over parts in Multipart. That task belongs to setUsing7bitTransport, which may in turn call setEncoding on the parts. MimeUtility.getEncodingforType was created as a helper function for choosing a default encoding that should be used for a given MIME type when an attachment is added to a message (either while composing or when retrieving from LocalStore). setEncoding was implemented in MimeBodyPart to assure that the encoding set in the Part's headers was the same as set for the Part's Body. (The method already existed in MimeMessage, which has similarities with MimeBodyPart.) MimeMessage.parse(InputStream in, boolean recurse) was implemented so that the parser could be told to recursively process nested messages read from the InputStream, thus giving access to all subparts at any level that may need to be converted from 8bit to 7bit.
2013-09-02 23:49:28 -04:00
String contentTransferEncoding = mPart
.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0];
String contentType = mPart
.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0];
Recursively convert attachments of type message/rfc822 to 7bit if necessary. The preceding commit resulted in attachments of type message/rfc822 being sent with 8bit encoding even when the SMTP server did not support 8BITMIME. This commit assures that messages will be converted to 7bit when necessary. A new interface CompositeBody was created that extends Body, and classes Message and Multipart were changed from implementing Body to CompositeBody. Additional classes BinaryTempFileMessageBody and LocalAttachmentMessageBody were created (by extending BinaryTempFileBody and LocalAttachmentBody, respectively), and they too implement CompositeBody. A CompositeBody is a Body containing a composite-type that can contain subparts that may require recursive processing when converting from 8bit to 7bit. The Part to which a CompositeBody belongs is only permitted to use 8bit or 7bit encoding for the CompositeBody. Previously, a Message was created so that it was 7bit clean by default (even though that meant base64 encoding all attachments, including messages). Then, if the SMTP server supported 8BITMIME, Message.setEncoding("8bit") was called so that bodies of type TextBody would been transmitted using 8bit encoding rather than quoted-printable. Now, messages are created with 8bit encoding by default. Then, if the SMTP server does not support 8BITMIME, Message.setUsing7bitTransport is called to recursively convert the message and its subparts to 7bit. The method setUsing7bitTransport was added to the interfaces Part and CompositeBody. setEncoding no longer iterates over parts in Multipart. That task belongs to setUsing7bitTransport, which may in turn call setEncoding on the parts. MimeUtility.getEncodingforType was created as a helper function for choosing a default encoding that should be used for a given MIME type when an attachment is added to a message (either while composing or when retrieving from LocalStore). setEncoding was implemented in MimeBodyPart to assure that the encoding set in the Part's headers was the same as set for the Part's Body. (The method already existed in MimeMessage, which has similarities with MimeBodyPart.) MimeMessage.parse(InputStream in, boolean recurse) was implemented so that the parser could be told to recursively process nested messages read from the InputStream, thus giving access to all subparts at any level that may need to be converted from 8bit to 7bit.
2013-09-02 23:49:28 -04:00
return MimeUtility.decodeBody(literal, contentTransferEncoding,
contentType);
}
return null;
}
}
private static String combine(Object[] parts, char separator) {
if (parts == null) {
return null;
}
return TextUtils.join(String.valueOf(separator), parts);
}
2014-12-18 03:33:09 -05:00
private class StoreImapSettings implements ImapSettings {
@Override
public String getHost() {
return mHost;
}
@Override
public int getPort() {
return mPort;
}
@Override
public ConnectionSecurity getConnectionSecurity() {
return mConnectionSecurity;
}
@Override
public AuthType getAuthType() {
return mAuthType;
}
@Override
public String getUsername() {
return mUsername;
}
@Override
public String getPassword() {
return mPassword;
}
@Override
public String getClientCertificateAlias() {
return mClientCertificateAlias;
}
@Override
public boolean useCompression(final int type) {
return mStoreConfig.useCompression(type);
}
@Override
public String getPathPrefix() {
return mPathPrefix;
}
@Override
public void setPathPrefix(String prefix) {
mPathPrefix = prefix;
}
@Override
public String getPathDelimiter() {
2014-12-18 03:33:09 -05:00
return mPathDelimiter;
}
@Override
public void setPathDelimiter(String delimiter) {
mPathDelimiter = delimiter;
2014-12-18 03:33:09 -05:00
}
@Override
public String getCombinedPrefix() {
return mCombinedPrefix;
}
@Override
public void setCombinedPrefix(String prefix) {
mCombinedPrefix = prefix;
}
}
}