Convert RealImapStore to Kotlin

This commit is contained in:
cketti 2022-08-10 16:19:21 +02:00
parent 33fa9d2b7c
commit a57f127a11
2 changed files with 230 additions and 319 deletions

View file

@ -1,411 +1,322 @@
package com.fsck.k9.mail.store.imap; package com.fsck.k9.mail.store.imap
import com.fsck.k9.logging.Timber
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.AuthenticationFailedException
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.FolderType
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.ssl.TrustedSocketFactory
import com.fsck.k9.mail.store.imap.ImapStoreSettings.autoDetectNamespace
import com.fsck.k9.mail.store.imap.ImapStoreSettings.pathPrefix
import java.io.IOException
import java.util.Deque
import java.util.LinkedList
import java.io.IOException; internal open class RealImapStore(
import java.nio.charset.CharacterCodingException; private val serverSettings: ServerSettings,
import java.util.ArrayList; private val config: ImapStoreConfig,
import java.util.Deque; private val trustedSocketFactory: TrustedSocketFactory,
import java.util.EnumSet; private val oauthTokenProvider: OAuth2TokenProvider?
import java.util.HashMap; ) : ImapStore, ImapConnectionManager, InternalImapStore {
import java.util.HashSet; private val folderNameCodec: FolderNameCodec = FolderNameCodec.newInstance()
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fsck.k9.logging.Timber; private val host: String = checkNotNull(serverSettings.host)
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.FolderType;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
private var pathPrefix: String?
private var combinedPrefix: String? = null
private var pathDelimiter: String? = null
/** private val permanentFlagsIndex: MutableSet<Flag> = mutableSetOf()
* <pre> private val connections: Deque<ImapConnection> = LinkedList()
* TODO Need a default response handler for things like folder updates
* </pre>
*/
class RealImapStore implements ImapStore, ImapConnectionManager, InternalImapStore {
private final ImapStoreConfig config;
private final TrustedSocketFactory trustedSocketFactory;
private Set<Flag> permanentFlagsIndex = EnumSet.noneOf(Flag.class);
private OAuth2TokenProvider oauthTokenProvider;
private String host; @Volatile
private int port; private var connectionGeneration = 1
private String username;
private String password;
private String clientCertificateAlias;
private ConnectionSecurity connectionSecurity;
private AuthType authType;
private String pathPrefix;
private String combinedPrefix = null;
private String pathDelimiter = null;
private final Deque<ImapConnection> connections = new LinkedList<>();
private FolderNameCodec folderNameCodec;
private volatile int connectionGeneration = 1;
init {
public RealImapStore(ServerSettings serverSettings, ImapStoreConfig config, val autoDetectNamespace = serverSettings.autoDetectNamespace
TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oauthTokenProvider) { val pathPrefixSetting = serverSettings.pathPrefix
this.config = config;
this.trustedSocketFactory = trustedSocketFactory;
host = serverSettings.host;
port = serverSettings.port;
connectionSecurity = serverSettings.connectionSecurity;
this.oauthTokenProvider = oauthTokenProvider;
authType = serverSettings.authenticationType;
username = serverSettings.username;
password = serverSettings.password;
clientCertificateAlias = serverSettings.clientCertificateAlias;
boolean autoDetectNamespace = ImapStoreSettings.getAutoDetectNamespace(serverSettings);
String pathPrefixSetting = ImapStoreSettings.getPathPrefix(serverSettings);
// Make extra sure pathPrefix is null if "auto-detect namespace" is configured // Make extra sure pathPrefix is null if "auto-detect namespace" is configured
pathPrefix = autoDetectNamespace ? null : pathPrefixSetting; pathPrefix = if (autoDetectNamespace) null else pathPrefixSetting
folderNameCodec = FolderNameCodec.newInstance();
} }
public ImapFolder getFolder(String name) { override fun getFolder(name: String): ImapFolder {
return new RealImapFolder(this, this, name, folderNameCodec); return RealImapFolder(
internalImapStore = this,
connectionManager = this,
serverId = name,
folderNameCodec = folderNameCodec
)
} }
@Override override fun getCombinedPrefix(): String {
@NotNull return combinedPrefix ?: buildCombinedPrefix().also { combinedPrefix = it }
public String getCombinedPrefix() {
if (combinedPrefix == null) {
if (pathPrefix != null) {
String tmpPrefix = pathPrefix.trim();
String tmpDelim = (pathDelimiter != null ? pathDelimiter.trim() : "");
if (tmpPrefix.endsWith(tmpDelim)) {
combinedPrefix = tmpPrefix;
} else if (tmpPrefix.length() > 0) {
combinedPrefix = tmpPrefix + tmpDelim;
} else {
combinedPrefix = "";
}
} else {
combinedPrefix = "";
}
}
return combinedPrefix;
} }
public List<FolderListItem> getFolders() throws MessagingException { private fun buildCombinedPrefix(): String {
ImapConnection connection = getConnection(); val pathPrefix = pathPrefix ?: return ""
try { val trimmedPathPrefix = pathPrefix.trim { it <= ' ' }
List<FolderListItem> folders = listFolders(connection, false); val trimmedPathDelimiter = pathDelimiter?.trim { it <= ' ' }.orEmpty()
if (!config.isSubscribedFoldersOnly()) { return if (trimmedPathPrefix.endsWith(trimmedPathDelimiter)) {
return folders; trimmedPathPrefix
} } else if (trimmedPathPrefix.isNotEmpty()) {
trimmedPathPrefix + trimmedPathDelimiter
List<FolderListItem> subscribedFolders = listFolders(connection, true);
return limitToSubscribedFolders(folders, subscribedFolders);
} catch (AuthenticationFailedException e) {
connection.close();
throw e;
} catch (IOException | MessagingException ioe) {
connection.close();
throw new MessagingException("Unable to get folder list.", ioe);
} finally {
releaseConnection(connection);
}
}
private List<FolderListItem> limitToSubscribedFolders(List<FolderListItem> folders,
List<FolderListItem> subscribedFolders) {
Set<String> subscribedFolderNames = new HashSet<>(subscribedFolders.size());
for (FolderListItem subscribedFolder : subscribedFolders) {
subscribedFolderNames.add(subscribedFolder.getServerId());
}
List<FolderListItem> filteredFolders = new ArrayList<>();
for (FolderListItem folder : folders) {
if (subscribedFolderNames.contains(folder.getServerId())) {
filteredFolders.add(folder);
}
}
return filteredFolders;
}
private List<FolderListItem> listFolders(ImapConnection connection, boolean subscribedOnly) throws IOException,
MessagingException {
String commandFormat;
if (subscribedOnly) {
commandFormat = "LSUB \"\" %s";
} else if (connection.hasCapability(Capabilities.SPECIAL_USE) &&
connection.hasCapability(Capabilities.LIST_EXTENDED)) {
commandFormat = "LIST \"\" %s RETURN (SPECIAL-USE)";
} else { } else {
commandFormat = "LIST \"\" %s"; ""
}
}
@Throws(MessagingException::class)
override fun getFolders(): List<FolderListItem> {
val connection = getConnection()
return try {
val folders = listFolders(connection, false)
if (!config.isSubscribedFoldersOnly()) {
return folders
}
val subscribedFolders = listFolders(connection, true)
limitToSubscribedFolders(folders, subscribedFolders)
} catch (e: AuthenticationFailedException) {
connection.close()
throw e
} catch (e: IOException) {
connection.close()
throw MessagingException("Unable to get folder list.", e)
} catch (e: MessagingException) {
connection.close()
throw MessagingException("Unable to get folder list.", e)
} finally {
releaseConnection(connection)
}
}
private fun limitToSubscribedFolders(
folders: List<FolderListItem>,
subscribedFolders: List<FolderListItem>
): List<FolderListItem> {
val subscribedFolderServerIds = subscribedFolders.map { it.serverId }.toSet()
return folders.filter { it.serverId in subscribedFolderServerIds }
}
@Throws(IOException::class, MessagingException::class)
private fun listFolders(connection: ImapConnection, subscribedOnly: Boolean): List<FolderListItem> {
val commandFormat = when {
subscribedOnly -> {
"LSUB \"\" %s"
}
connection.supportsListExtended -> {
"LIST \"\" %s RETURN (SPECIAL-USE)"
}
else -> {
"LIST \"\" %s"
}
} }
String encodedListPrefix = ImapUtility.encodeString(getCombinedPrefix() + "*"); val encodedListPrefix = ImapUtility.encodeString(getCombinedPrefix() + "*")
List<ImapResponse> responses = connection.executeSimpleCommand(String.format(commandFormat, encodedListPrefix)); val responses = connection.executeSimpleCommand(String.format(commandFormat, encodedListPrefix))
List<ListResponse> listResponses = (subscribedOnly) ? val listResponses = if (subscribedOnly) {
ListResponse.parseLsub(responses) : ListResponse.parseLsub(responses)
ListResponse.parseList(responses); } else {
ListResponse.parseList(responses)
}
Map<String, FolderListItem> folderMap = new HashMap<>(listResponses.size()); val folderMap = mutableMapOf<String, FolderListItem>()
for (ListResponse listResponse : listResponses) { for (listResponse in listResponses) {
String serverId = listResponse.getName(); val serverId = listResponse.name
if (pathDelimiter == null) { if (pathDelimiter == null) {
pathDelimiter = listResponse.getHierarchyDelimiter(); pathDelimiter = listResponse.hierarchyDelimiter
combinedPrefix = null; combinedPrefix = null
} }
if (RealImapFolder.INBOX.equalsIgnoreCase(serverId)) { if (RealImapFolder.INBOX.equals(serverId, ignoreCase = true)) {
continue; continue
} else if (listResponse.hasAttribute("\\NoSelect")) { } else if (listResponse.hasAttribute("\\NoSelect")) {
continue; continue
} }
String name = getFolderDisplayName(serverId); val name = getFolderDisplayName(serverId)
String oldServerId = getOldServerId(serverId); val oldServerId = getOldServerId(serverId)
FolderType type; val type = when {
if (listResponse.hasAttribute("\\Archive") || listResponse.hasAttribute("\\All")) { listResponse.hasAttribute("\\Archive") -> FolderType.ARCHIVE
type = FolderType.ARCHIVE; listResponse.hasAttribute("\\All") -> FolderType.ARCHIVE
} else if (listResponse.hasAttribute("\\Drafts")) { listResponse.hasAttribute("\\Drafts") -> FolderType.DRAFTS
type = FolderType.DRAFTS; listResponse.hasAttribute("\\Sent") -> FolderType.SENT
} else if (listResponse.hasAttribute("\\Sent")) { listResponse.hasAttribute("\\Junk") -> FolderType.SPAM
type = FolderType.SENT; listResponse.hasAttribute("\\Trash") -> FolderType.TRASH
} else if (listResponse.hasAttribute("\\Junk")) { else -> FolderType.REGULAR
type = FolderType.SPAM;
} else if (listResponse.hasAttribute("\\Trash")) {
type = FolderType.TRASH;
} else {
type = FolderType.REGULAR;
} }
FolderListItem existingItem = folderMap.get(serverId); val existingItem = folderMap[serverId]
if (existingItem == null || existingItem.getType() == FolderType.REGULAR) { if (existingItem == null || existingItem.type == FolderType.REGULAR) {
folderMap.put(serverId, new FolderListItem(serverId, name, type, oldServerId)); folderMap[serverId] = FolderListItem(serverId, name, type, oldServerId)
} }
} }
List<FolderListItem> folders = new ArrayList<>(folderMap.size() + 1); return buildList {
folders.add(new FolderListItem(RealImapFolder.INBOX, RealImapFolder.INBOX, FolderType.INBOX, RealImapFolder.INBOX)); add(FolderListItem(RealImapFolder.INBOX, RealImapFolder.INBOX, FolderType.INBOX, RealImapFolder.INBOX))
folders.addAll(folderMap.values()); addAll(folderMap.values)
}
return folders;
} }
private String getFolderDisplayName(String serverId) { private fun getFolderDisplayName(serverId: String): String {
String decodedFolderName; val decodedFolderName = try {
try { folderNameCodec.decode(serverId)
decodedFolderName = folderNameCodec.decode(serverId); } catch (e: CharacterCodingException) {
} catch (CharacterCodingException e) { Timber.w(e, "Folder name not correctly encoded with the UTF-7 variant as defined by RFC 3501: %s", serverId)
Timber.w(e, "Folder name not correctly encoded with the UTF-7 variant as defined by RFC 3501: %s", serverId
serverId);
decodedFolderName = serverId;
} }
String folderNameWithoutPrefix = removePrefixFromFolderName(decodedFolderName); val folderNameWithoutPrefix = removePrefixFromFolderName(decodedFolderName)
return folderNameWithoutPrefix != null ? folderNameWithoutPrefix : decodedFolderName; return folderNameWithoutPrefix ?: decodedFolderName
} }
@Nullable private fun getOldServerId(serverId: String): String? {
private String getOldServerId(String serverId) { val decodedFolderName = try {
String decodedFolderName; folderNameCodec.decode(serverId)
try { } catch (e: CharacterCodingException) {
decodedFolderName = folderNameCodec.decode(serverId);
} catch (CharacterCodingException e) {
// Previous versions of K-9 Mail ignored folders with invalid UTF-7 encoding // Previous versions of K-9 Mail ignored folders with invalid UTF-7 encoding
return null; return null
} }
return removePrefixFromFolderName(decodedFolderName); return removePrefixFromFolderName(decodedFolderName)
} }
@Nullable private fun removePrefixFromFolderName(folderName: String): String? {
private String removePrefixFromFolderName(String folderName) { val prefix = getCombinedPrefix()
String prefix = getCombinedPrefix(); val prefixLength = prefix.length
int prefixLength = prefix.length();
if (prefixLength == 0) { if (prefixLength == 0) {
return folderName; return folderName
} }
if (!folderName.startsWith(prefix)) { if (!folderName.startsWith(prefix)) {
// Folder name doesn't start with our configured prefix. But right now when building commands we prefix all // Folder name doesn't start with our configured prefix. But right now when building commands we prefix all
// folders except the INBOX with the prefix. So we won't be able to use this folder. // folders except the INBOX with the prefix. So we won't be able to use this folder.
return null; return null
} }
return folderName.substring(prefixLength); return folderName.substring(prefixLength)
} }
public void checkSettings() throws MessagingException { @Throws(MessagingException::class)
override fun checkSettings() {
try { try {
ImapConnection connection = createImapConnection(); val connection = createImapConnection()
connection.open(); connection.open()
connection.close(); connection.close()
} catch (IOException ioe) { } catch (e: IOException) {
throw new MessagingException("Unable to connect", ioe); throw MessagingException("Unable to connect", e)
} }
} }
@Override @Throws(MessagingException::class)
@NotNull override fun getConnection(): ImapConnection {
public ImapConnection getConnection() throws MessagingException { while (true) {
ImapConnection connection; val connection = pollConnection() ?: return createImapConnection()
while ((connection = pollConnection()) != null) {
try { try {
connection.executeSimpleCommand(Commands.NOOP); connection.executeSimpleCommand(Commands.NOOP)
break;
} catch (IOException ioe) { // If the command completes without an error this connection is still usable.
connection.close(); return connection
} catch (ioe: IOException) {
connection.close()
} }
} }
if (connection == null) {
connection = createImapConnection();
}
return connection;
} }
private ImapConnection pollConnection() { private fun pollConnection(): ImapConnection? {
synchronized (connections) { return synchronized(connections) {
return connections.poll(); connections.poll()
} }
} }
@Override override fun releaseConnection(connection: ImapConnection?) {
public void releaseConnection(ImapConnection connection) { if (connection != null && connection.isConnected) {
if (connection != null && connection.isConnected()) { if (connection.connectionGeneration == connectionGeneration) {
if (connection.getConnectionGeneration() == connectionGeneration) { synchronized(connections) {
synchronized (connections) { connections.offer(connection)
connections.offer(connection);
} }
} else { } else {
connection.close(); connection.close()
} }
} }
} }
@Override override fun closeAllConnections() {
public void closeAllConnections() { Timber.v("ImapStore.closeAllConnections()")
Timber.v("ImapStore.closeAllConnections()");
List<ImapConnection> connectionsToClose; val connectionsToClose = synchronized(connections) {
synchronized (connections) { val connectionsToClose = connections.toList()
connectionGeneration++;
connectionsToClose = new ArrayList<>(connections); connectionGeneration++
connections.clear(); connections.clear()
connectionsToClose
} }
for (ImapConnection connection : connectionsToClose) { for (connection in connectionsToClose) {
connection.close(); connection.close()
} }
} }
ImapConnection createImapConnection() { open fun createImapConnection(): ImapConnection {
return new RealImapConnection( return RealImapConnection(
new StoreImapSettings(), StoreImapSettings(),
trustedSocketFactory, trustedSocketFactory,
oauthTokenProvider, oauthTokenProvider,
connectionGeneration); connectionGeneration
)
} }
@Override override val logLabel: String
@NotNull get() = config.logLabel
public String getLogLabel() {
return config.getLogLabel(); override fun getPermanentFlagsIndex(): MutableSet<Flag> {
return permanentFlagsIndex
} }
@Override private inner class StoreImapSettings : ImapSettings {
@NotNull override val host: String = this@RealImapStore.host
public Set<Flag> getPermanentFlagsIndex() { override val port: Int = serverSettings.port
return permanentFlagsIndex; override val connectionSecurity: ConnectionSecurity = serverSettings.connectionSecurity
} override val authType: AuthType = serverSettings.authenticationType
override val username: String = serverSettings.username
override val password: String? = serverSettings.password
override val clientCertificateAlias: String? = serverSettings.clientCertificateAlias
override fun useCompression(): Boolean {
private class StoreImapSettings implements ImapSettings { return this@RealImapStore.config.useCompression()
@Override
public String getHost() {
return host;
} }
@Override override var pathPrefix: String?
public int getPort() { get() = this@RealImapStore.pathPrefix
return port; set(value) {
} this@RealImapStore.pathPrefix = value
}
@Override override var pathDelimiter: String?
public ConnectionSecurity getConnectionSecurity() { get() = this@RealImapStore.pathDelimiter
return connectionSecurity; set(value) {
} this@RealImapStore.pathDelimiter = value
}
@Override override fun setCombinedPrefix(prefix: String?) {
public AuthType getAuthType() { combinedPrefix = prefix
return authType;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getClientCertificateAlias() {
return clientCertificateAlias;
}
@Override
public boolean useCompression() {
return config.useCompression();
}
@Override
public String getPathPrefix() {
return pathPrefix;
}
@Override
public void setPathPrefix(String prefix) {
pathPrefix = prefix;
}
@Override
public String getPathDelimiter() {
return pathDelimiter;
}
@Override
public void setPathDelimiter(String delimiter) {
pathDelimiter = delimiter;
}
@Override
public void setCombinedPrefix(String prefix) {
combinedPrefix = prefix;
} }
} }
} }
private val ImapConnection.supportsListExtended: Boolean
get() = hasCapability(Capabilities.SPECIAL_USE) && hasCapability(Capabilities.LIST_EXTENDED)

View file

@ -465,7 +465,7 @@ public class RealImapStoreTest {
} }
@Override @Override
ImapConnection createImapConnection() { public ImapConnection createImapConnection() {
if (imapConnections.isEmpty()) { if (imapConnections.isEmpty()) {
throw new AssertionError("Unexpectedly tried to create an ImapConnection instance"); throw new AssertionError("Unexpectedly tried to create an ImapConnection instance");
} }