Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a57f127a authored by cketti's avatar cketti
Browse files

Convert `RealImapStore` to Kotlin

parent 33fa9d2b
Loading
Loading
Loading
Loading
+226 −315
Original line number Diff line number Diff line
package com.fsck.k9.mail.store.imap;


import java.io.IOException;
import java.nio.charset.CharacterCodingException;
import java.util.ArrayList;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;


/**
 * <pre>
 * 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;
    private int port;
    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;


    public RealImapStore(ServerSettings serverSettings, ImapStoreConfig config,
            TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oauthTokenProvider) {
        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);
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

internal open class RealImapStore(
    private val serverSettings: ServerSettings,
    private val config: ImapStoreConfig,
    private val trustedSocketFactory: TrustedSocketFactory,
    private val oauthTokenProvider: OAuth2TokenProvider?
) : ImapStore, ImapConnectionManager, InternalImapStore {
    private val folderNameCodec: FolderNameCodec = FolderNameCodec.newInstance()

    private val host: String = checkNotNull(serverSettings.host)

    private var pathPrefix: String?
    private var combinedPrefix: String? = null
    private var pathDelimiter: String? = null

    private val permanentFlagsIndex: MutableSet<Flag> = mutableSetOf()
    private val connections: Deque<ImapConnection> = LinkedList()

    @Volatile
    private var connectionGeneration = 1

    init {
        val autoDetectNamespace = serverSettings.autoDetectNamespace
        val pathPrefixSetting = serverSettings.pathPrefix

        // Make extra sure pathPrefix is null if "auto-detect namespace" is configured
        pathPrefix = autoDetectNamespace ? null : pathPrefixSetting;

        folderNameCodec = FolderNameCodec.newInstance();
        pathPrefix = if (autoDetectNamespace) null else pathPrefixSetting
    }

    public ImapFolder getFolder(String name) {
        return new RealImapFolder(this, this, name, folderNameCodec);
    override fun getFolder(name: String): ImapFolder {
        return RealImapFolder(
            internalImapStore = this,
            connectionManager = this,
            serverId = name,
            folderNameCodec = folderNameCodec
        )
    }

    @Override
    @NotNull
    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 = "";
    override fun getCombinedPrefix(): String {
        return combinedPrefix ?: buildCombinedPrefix().also { combinedPrefix = it }
    }

    private fun buildCombinedPrefix(): String {
        val pathPrefix = pathPrefix ?: return ""

        val trimmedPathPrefix = pathPrefix.trim { it <= ' ' }
        val trimmedPathDelimiter = pathDelimiter?.trim { it <= ' ' }.orEmpty()

        return if (trimmedPathPrefix.endsWith(trimmedPathDelimiter)) {
            trimmedPathPrefix
        } else if (trimmedPathPrefix.isNotEmpty()) {
            trimmedPathPrefix + trimmedPathDelimiter
        } else {
                combinedPrefix = "";
            }
            ""
        }

        return combinedPrefix;
    }

    public List<FolderListItem> getFolders() throws MessagingException {
        ImapConnection connection = getConnection();

        try {
            List<FolderListItem> folders = listFolders(connection, false);
    @Throws(MessagingException::class)
    override fun getFolders(): List<FolderListItem> {
        val connection = getConnection()

        return try {
            val folders = listFolders(connection, false)
            if (!config.isSubscribedFoldersOnly()) {
                return folders;
            }

            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);
                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);
            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());
    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 }
    }

        List<FolderListItem> filteredFolders = new ArrayList<>();
        for (FolderListItem folder : folders) {
            if (subscribedFolderNames.contains(folder.getServerId())) {
                filteredFolders.add(folder);
    @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"
            }

        return filteredFolders;
        }

    private List<FolderListItem> listFolders(ImapConnection connection, boolean subscribedOnly) throws IOException,
            MessagingException {
        val encodedListPrefix = ImapUtility.encodeString(getCombinedPrefix() + "*")
        val responses = connection.executeSimpleCommand(String.format(commandFormat, encodedListPrefix))

        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)";
        val listResponses = if (subscribedOnly) {
            ListResponse.parseLsub(responses)
        } else {
            commandFormat = "LIST \"\" %s";
            ListResponse.parseList(responses)
        }

        String encodedListPrefix = ImapUtility.encodeString(getCombinedPrefix() + "*");
        List<ImapResponse> responses = connection.executeSimpleCommand(String.format(commandFormat, encodedListPrefix));

        List<ListResponse> listResponses = (subscribedOnly) ?
                ListResponse.parseLsub(responses) :
                ListResponse.parseList(responses);

        Map<String, FolderListItem> folderMap = new HashMap<>(listResponses.size());
        for (ListResponse listResponse : listResponses) {
            String serverId = listResponse.getName();
        val folderMap = mutableMapOf<String, FolderListItem>()
        for (listResponse in listResponses) {
            val serverId = listResponse.name

            if (pathDelimiter == null) {
                pathDelimiter = listResponse.getHierarchyDelimiter();
                combinedPrefix = null;
                pathDelimiter = listResponse.hierarchyDelimiter
                combinedPrefix = null
            }

            if (RealImapFolder.INBOX.equalsIgnoreCase(serverId)) {
                continue;
            if (RealImapFolder.INBOX.equals(serverId, ignoreCase = true)) {
                continue
            } else if (listResponse.hasAttribute("\\NoSelect")) {
                continue;
            }

            String name = getFolderDisplayName(serverId);
            String oldServerId = getOldServerId(serverId);

            FolderType type;
            if (listResponse.hasAttribute("\\Archive") || listResponse.hasAttribute("\\All")) {
                type = FolderType.ARCHIVE;
            } else if (listResponse.hasAttribute("\\Drafts")) {
                type = FolderType.DRAFTS;
            } else if (listResponse.hasAttribute("\\Sent")) {
                type = FolderType.SENT;
            } else if (listResponse.hasAttribute("\\Junk")) {
                type = FolderType.SPAM;
            } else if (listResponse.hasAttribute("\\Trash")) {
                type = FolderType.TRASH;
            } else {
                type = FolderType.REGULAR;
                continue
            }

            FolderListItem existingItem = folderMap.get(serverId);
            if (existingItem == null || existingItem.getType() == FolderType.REGULAR) {
                folderMap.put(serverId, new FolderListItem(serverId, name, type, oldServerId));
            }
        }
            val name = getFolderDisplayName(serverId)
            val oldServerId = getOldServerId(serverId)

        List<FolderListItem> folders = new ArrayList<>(folderMap.size() + 1);
        folders.add(new FolderListItem(RealImapFolder.INBOX, RealImapFolder.INBOX, FolderType.INBOX, RealImapFolder.INBOX));
        folders.addAll(folderMap.values());
            val type = when {
                listResponse.hasAttribute("\\Archive") -> FolderType.ARCHIVE
                listResponse.hasAttribute("\\All") -> FolderType.ARCHIVE
                listResponse.hasAttribute("\\Drafts") -> FolderType.DRAFTS
                listResponse.hasAttribute("\\Sent") -> FolderType.SENT
                listResponse.hasAttribute("\\Junk") -> FolderType.SPAM
                listResponse.hasAttribute("\\Trash") -> FolderType.TRASH
                else -> FolderType.REGULAR
            }

        return folders;
            val existingItem = folderMap[serverId]
            if (existingItem == null || existingItem.type == FolderType.REGULAR) {
                folderMap[serverId] = FolderListItem(serverId, name, type, oldServerId)
            }
        }

    private String getFolderDisplayName(String serverId) {
        String decodedFolderName;
        try {
            decodedFolderName = folderNameCodec.decode(serverId);
        } catch (CharacterCodingException e) {
            Timber.w(e, "Folder name not correctly encoded with the UTF-7 variant as defined by RFC 3501: %s",
                    serverId);
        return buildList {
            add(FolderListItem(RealImapFolder.INBOX, RealImapFolder.INBOX, FolderType.INBOX, RealImapFolder.INBOX))
            addAll(folderMap.values)
        }
    }

            decodedFolderName = serverId;
    private fun getFolderDisplayName(serverId: String): String {
        val decodedFolderName = try {
            folderNameCodec.decode(serverId)
        } catch (e: CharacterCodingException) {
            Timber.w(e, "Folder name not correctly encoded with the UTF-7 variant as defined by RFC 3501: %s", serverId)
            serverId
        }

        String folderNameWithoutPrefix = removePrefixFromFolderName(decodedFolderName);
        return folderNameWithoutPrefix != null ? folderNameWithoutPrefix : decodedFolderName;
        val folderNameWithoutPrefix = removePrefixFromFolderName(decodedFolderName)
        return folderNameWithoutPrefix ?: decodedFolderName
    }

    @Nullable
    private String getOldServerId(String serverId) {
        String decodedFolderName;
        try {
            decodedFolderName = folderNameCodec.decode(serverId);
        } catch (CharacterCodingException e) {
    private fun getOldServerId(serverId: String): String? {
        val decodedFolderName = try {
            folderNameCodec.decode(serverId)
        } catch (e: CharacterCodingException) {
            // 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 String removePrefixFromFolderName(String folderName) {
        String prefix = getCombinedPrefix();
        int prefixLength = prefix.length();
    private fun removePrefixFromFolderName(folderName: String): String? {
        val prefix = getCombinedPrefix()
        val prefixLength = prefix.length
        if (prefixLength == 0) {
            return folderName;
            return folderName
        }

        if (!folderName.startsWith(prefix)) {
            // 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.
            return null;
            return null
        }

        return folderName.substring(prefixLength);
        return folderName.substring(prefixLength)
    }

    public void checkSettings() throws MessagingException {
    @Throws(MessagingException::class)
    override fun checkSettings() {
        try {
            ImapConnection connection = createImapConnection();
            val connection = createImapConnection()

            connection.open();
            connection.close();
        } catch (IOException ioe) {
            throw new MessagingException("Unable to connect", ioe);
            connection.open()
            connection.close()
        } catch (e: IOException) {
            throw MessagingException("Unable to connect", e)
        }
    }

    @Override
    @NotNull
    public ImapConnection getConnection() throws MessagingException {
        ImapConnection connection;
        while ((connection = pollConnection()) != null) {
    @Throws(MessagingException::class)
    override fun getConnection(): ImapConnection {
        while (true) {
            val connection = pollConnection() ?: return createImapConnection()

            try {
                connection.executeSimpleCommand(Commands.NOOP);
                break;
            } catch (IOException ioe) {
                connection.close();
            }
        }
                connection.executeSimpleCommand(Commands.NOOP)

        if (connection == null) {
            connection = createImapConnection();
                // If the command completes without an error this connection is still usable.
                return connection
            } catch (ioe: IOException) {
                connection.close()
            }
        }

        return connection;
    }

    private ImapConnection pollConnection() {
        synchronized (connections) {
            return connections.poll();
    private fun pollConnection(): ImapConnection? {
        return synchronized(connections) {
            connections.poll()
        }
    }

    @Override
    public void releaseConnection(ImapConnection connection) {
        if (connection != null && connection.isConnected()) {
            if (connection.getConnectionGeneration() == connectionGeneration) {
    override fun releaseConnection(connection: ImapConnection?) {
        if (connection != null && connection.isConnected) {
            if (connection.connectionGeneration == connectionGeneration) {
                synchronized(connections) {
                    connections.offer(connection);
                    connections.offer(connection)
                }
            } else {
                connection.close();
                connection.close()
            }
        }
    }

    @Override
    public void closeAllConnections() {
        Timber.v("ImapStore.closeAllConnections()");
    override fun closeAllConnections() {
        Timber.v("ImapStore.closeAllConnections()")

        List<ImapConnection> connectionsToClose;
        synchronized (connections) {
            connectionGeneration++;
            connectionsToClose = new ArrayList<>(connections);
            connections.clear();
        }
        val connectionsToClose = synchronized(connections) {
            val connectionsToClose = connections.toList()

        for (ImapConnection connection : connectionsToClose) {
            connection.close();
        }
    }
            connectionGeneration++
            connections.clear()

    ImapConnection createImapConnection() {
        return new RealImapConnection(
                new StoreImapSettings(),
                trustedSocketFactory,
                oauthTokenProvider,
                connectionGeneration);
            connectionsToClose
        }

    @Override
    @NotNull
    public String getLogLabel() {
        return config.getLogLabel();
        for (connection in connectionsToClose) {
            connection.close()
        }

    @Override
    @NotNull
    public Set<Flag> getPermanentFlagsIndex() {
        return permanentFlagsIndex;
    }


    private class StoreImapSettings implements ImapSettings {
        @Override
        public String getHost() {
            return host;
        }

        @Override
        public int getPort() {
            return port;
        }

        @Override
        public ConnectionSecurity getConnectionSecurity() {
            return connectionSecurity;
    open fun createImapConnection(): ImapConnection {
        return RealImapConnection(
            StoreImapSettings(),
            trustedSocketFactory,
            oauthTokenProvider,
            connectionGeneration
        )
    }

        @Override
        public AuthType getAuthType() {
            return authType;
        }
    override val logLabel: String
        get() = config.logLabel

        @Override
        public String getUsername() {
            return username;
    override fun getPermanentFlagsIndex(): MutableSet<Flag> {
        return permanentFlagsIndex
    }

        @Override
        public String getPassword() {
            return password;
        }
    private inner class StoreImapSettings : ImapSettings {
        override val host: String = this@RealImapStore.host
        override val port: Int = serverSettings.port
        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
        public String getClientCertificateAlias() {
            return clientCertificateAlias;
        override fun useCompression(): Boolean {
            return this@RealImapStore.config.useCompression()
        }

        @Override
        public boolean useCompression() {
            return config.useCompression();
        override var pathPrefix: String?
            get() = this@RealImapStore.pathPrefix
            set(value) {
                this@RealImapStore.pathPrefix = value
            }

        @Override
        public String getPathPrefix() {
            return pathPrefix;
        override var pathDelimiter: String?
            get() = this@RealImapStore.pathDelimiter
            set(value) {
                this@RealImapStore.pathDelimiter = value
            }

        @Override
        public void setPathPrefix(String prefix) {
            pathPrefix = prefix;
        override fun setCombinedPrefix(prefix: String?) {
            combinedPrefix = 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)
+1 −1
Original line number Diff line number Diff line
@@ -465,7 +465,7 @@ public class RealImapStoreTest {
        }

        @Override
        ImapConnection createImapConnection() {
        public ImapConnection createImapConnection() {
            if (imapConnections.isEmpty()) {
                throw new AssertionError("Unexpectedly tried to create an ImapConnection instance");
            }