diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapCommandSplitter.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapCommandSplitter.java deleted file mode 100644 index 91bbcd715b8..00000000000 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapCommandSplitter.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.fsck.k9.mail.store.imap; - - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - - -class ImapCommandSplitter { - static List splitCommand(String prefix, String suffix, GroupedIds groupedIds, int lengthLimit) { - List commands = new ArrayList<>(); - Set workingIdSet = new TreeSet<>(groupedIds.ids); - List workingIdGroups = new ArrayList<>(groupedIds.idGroups); - - int suffixLength = suffix.length(); - int staticCommandLength = prefix.length() + suffixLength + 2; - while (!workingIdSet.isEmpty() || !workingIdGroups.isEmpty()) { - StringBuilder commandBuilder = new StringBuilder(prefix).append(' '); - int length = staticCommandLength; - while (length < lengthLimit) { - if (!workingIdSet.isEmpty()) { - Long id = workingIdSet.iterator().next(); - String idString = Long.toString(id); - - length += idString.length() + 1; - if (length >= lengthLimit) { - break; - } - - commandBuilder.append(idString).append(','); - workingIdSet.remove(id); - } else if (!workingIdGroups.isEmpty()) { - ContiguousIdGroup idGroup = workingIdGroups.iterator().next(); - String idGroupString = idGroup.toString(); - - length += idGroupString.length() + 1; - if (length >= lengthLimit) { - break; - } - - commandBuilder.append(idGroupString).append(','); - workingIdGroups.remove(idGroup); - } else { - break; - } - } - - if (suffixLength != 0) { - // Replace the last comma with a space - commandBuilder.setCharAt(commandBuilder.length() - 1, ' '); - commandBuilder.append(suffix); - } else { - // Remove last comma - commandBuilder.setLength(commandBuilder.length() - 1); - } - - String command = commandBuilder.toString(); - commands.add(command); - } - - return commands; - } -} diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapCommandSplitter.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapCommandSplitter.kt new file mode 100644 index 00000000000..97e1f77e1a1 --- /dev/null +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapCommandSplitter.kt @@ -0,0 +1,71 @@ +package com.fsck.k9.mail.store.imap + +internal object ImapCommandSplitter { + fun splitCommand(prefix: String, suffix: String, groupedIds: GroupedIds, lengthLimit: Int): List { + val commands = mutableListOf() + val workingIdSet = sortedSetOf().apply { addAll(groupedIds.ids) } + val workingIdGroups = groupedIds.idGroups.toMutableList() + val suffixLength = suffix.length + val staticCommandLength = prefix.length + suffixLength + 2 + while (workingIdSet.isNotEmpty() || workingIdGroups.isNotEmpty()) { + val commandBuilder = StringBuilder(prefix).append(' ') + var length = staticCommandLength + while (length < lengthLimit) { + val (appended, newLength) = appendNextItem( + commandBuilder, + workingIdSet, + workingIdGroups, + length, + lengthLimit, + ) + length = newLength + if (!appended) break + } + if (suffixLength != 0) { + // Replace the last comma with a space + commandBuilder.setCharAt(commandBuilder.length - 1, ' ') + commandBuilder.append(suffix) + } else { + // Remove last comma + commandBuilder.setLength(commandBuilder.length - 1) + } + commands.add(commandBuilder.toString()) + } + return commands + } + private fun appendNextItem( + commandBuilder: StringBuilder, + workingIdSet: MutableSet, + workingIdGroups: MutableList, + currentLength: Int, + lengthLimit: Int, + ): Pair { + return when { + workingIdSet.isNotEmpty() -> { + val id = workingIdSet.first() + val idString = id.toString() + val newLength = currentLength + idString.length + 1 + if (newLength >= lengthLimit) { + false to currentLength + } else { + commandBuilder.append(idString).append(',') + workingIdSet.remove(id) + true to newLength + } + } + workingIdGroups.isNotEmpty() -> { + val idGroup = workingIdGroups.first() + val idGroupString = idGroup.toString() + val newLength = currentLength + idGroupString.length + 1 + if (newLength >= lengthLimit) { + false to currentLength + } else { + commandBuilder.append(idGroupString).append(',') + workingIdGroups.remove(idGroup) + true to newLength + } + } + else -> false to currentLength + } + } +} diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseCallback.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseCallback.kt similarity index 76% rename from mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseCallback.java rename to mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseCallback.kt index 967d0aea915..95bdbaca5a0 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseCallback.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseCallback.kt @@ -1,8 +1,8 @@ -package com.fsck.k9.mail.store.imap; +package com.fsck.k9.mail.store.imap -import com.fsck.k9.mail.filter.FixedLengthInputStream; +import com.fsck.k9.mail.filter.FixedLengthInputStream -public interface ImapResponseCallback { +internal fun interface ImapResponseCallback { /** * Callback method that is called by the parser when a literal string * is found in an IMAP response. @@ -20,5 +20,6 @@ public interface ImapResponseCallback { * and the exception will be thrown after the * complete IMAP response has been parsed. */ - Object foundLiteral(ImapResponse response, FixedLengthInputStream literal) throws Exception; + @Throws(Exception::class) + fun foundLiteral(response: ImapResponse, literal: FixedLengthInputStream): Any? } diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapUtility.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapUtility.java deleted file mode 100644 index b284d3e316d..00000000000 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapUtility.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2012 The K-9 Dog Walkers - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.fsck.k9.mail.store.imap; - - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import net.thunderbird.core.logging.legacy.Log; -import net.thunderbird.core.common.mail.Flag; - - -/** - * Utility methods for use with IMAP. - */ -class ImapUtility { - /** - * Gets all of the values in a sequence set per RFC 3501. - * - *

- * Any ranges are expanded into a list of individual numbers. - *

- * - *
-     * sequence-number = nz-number / "*"
-     * sequence-range  = sequence-number ":" sequence-number
-     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
-     * 
- * - * @param set - * The sequence set string as received by the server. - * - * @return The list of IDs as strings in this sequence set. If the set is invalid, an empty - * list is returned. - */ - public static List getImapSequenceValues(String set) { - List list = new ArrayList<>(); - if (set != null) { - String[] setItems = set.split(","); - for (String item : setItems) { - if (item.indexOf(':') == -1) { - // simple item - if (isNumberValid(item)) { - list.add(item); - } - } else { - // range - list.addAll(getImapRangeValues(item)); - } - } - } - - return list; - } - - /** - * Expand the given number range into a list of individual numbers. - * - *
-     * sequence-number = nz-number / "*"
-     * sequence-range  = sequence-number ":" sequence-number
-     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
-     * 
- * - * @param range - * The range string as received by the server. - * - * @return The list of IDs as strings in this range. If the range is not valid, an empty list - * is returned. - */ - public static List getImapRangeValues(String range) { - List list = new ArrayList<>(); - try { - if (range != null) { - int colonPos = range.indexOf(':'); - if (colonPos > 0) { - long first = Long.parseLong(range.substring(0, colonPos)); - long second = Long.parseLong(range.substring(colonPos + 1)); - if (is32bitValue(first) && is32bitValue(second)) { - if (first < second) { - for (long i = first; i <= second; i++) { - list.add(Long.toString(i)); - } - } else { - for (long i = first; i >= second; i--) { - list.add(Long.toString(i)); - } - } - } else { - Log.d("Invalid range: %s", range); - } - } - } - } catch (NumberFormatException e) { - Log.d(e, "Invalid range value: %s", range); - } - - return list; - } - - private static boolean isNumberValid(String number) { - try { - long value = Long.parseLong(number); - if (is32bitValue(value)) { - return true; - } - } catch (NumberFormatException e) { - // do nothing - } - - Log.d("Invalid UID value: %s", number); - - return false; - } - - private static boolean is32bitValue(long value) { - return ((value & ~0xFFFFFFFFL) == 0L); - } - - /** - * 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 - * 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. - */ - //TODO use a literal string - public static String encodeString(String str) { - return "\"" + str.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; - } - - public static ImapResponse getLastResponse(List responses) { - int lastIndex = responses.size() - 1; - - return responses.get(lastIndex); - } - - public static String combineFlags(Iterable flags, boolean canCreateForwardedFlag) { - List flagNames = new ArrayList<>(); - 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"); - } else if (flag == Flag.FORWARDED && canCreateForwardedFlag) { - flagNames.add("$Forwarded"); - } else if (flag == Flag.DRAFT) { - flagNames.add("\\Draft"); - } - } - - return ImapUtility.join(" ", flagNames); - } - - public static String join(String delimiter, Collection tokens) { - if (tokens == null) { - return null; - } - StringBuilder sb = new StringBuilder(); - boolean firstTime = true; - for (Object token: tokens) { - if (firstTime) { - firstTime = false; - } else { - sb.append(delimiter); - } - sb.append(token); - } - return sb.toString(); - } -} diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapUtility.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapUtility.kt new file mode 100644 index 00000000000..032313ba7f4 --- /dev/null +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapUtility.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2012 The K-9 Dog Walkers + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fsck.k9.mail.store.imap + +import net.thunderbird.core.common.mail.Flag +import net.thunderbird.core.logging.legacy.Log + +/** + * Utility methods for use with IMAP. + */ +internal object ImapUtility { + + /** + * Gets all of the values in a sequence set per RFC 3501. + * + *

+ * Any ranges are expanded into a list of individual numbers. + *

+ * + *
+     * sequence-number = nz-number / "*"
+     * sequence-range = sequence-number ":" sequence-number
+     * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
+     * 
+ * + * @param set The sequence set string as received by the server. + * @return The list of IDs as strings in this sequence set. If the set is invalid, an empty + * list is returned. + */ + fun getImapSequenceValues(set: String?): List { + set ?: return emptyList() + return set.split(",").flatMap { item -> + if (':' !in item) { + if (isNumberValid(item)) listOf(item) else emptyList() + } else { + getImapRangeValues(item) + } + } + } + + /** + * Expand the given number range into a list of individual numbers. + * + *
+     * sequence-number = nz-number / "*"
+     * sequence-range = sequence-number ":" sequence-number
+     * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
+     * 
+ * + * @param range The range string as received by the server. + * @return The list of IDs as strings in this range. If the range is not valid, an empty list + * is returned. + */ + fun getImapRangeValues(range: String?): List { + val (first, second) = parseRangeBounds(range) ?: return emptyList() + return if (first <= second) { + (first..second).map { it.toString() } + } else { + (first downTo second).map { it.toString() } + } + } + private fun parseRangeBounds(range: String?): Pair? { + val colonPos = range?.indexOf(':')?.takeIf { it > 0 } ?: return null + + return try { + val first = range.substring(0, colonPos).toLong() + val second = range.substring(colonPos + 1).toLong() + if (is32bitValue(first) && is32bitValue(second)) { + first to second + } else { + Log.d("Invalid range: %s", range) + null + } + } catch (e: NumberFormatException) { + Log.d(e, "Invalid range value: %s", range) + null + } + } + + private fun isNumberValid(number: String): Boolean { + val value = number.toLongOrNull() + if (value != null && is32bitValue(value)) return true + Log.d("Invalid UID value: %s", number) + return false + } + + private fun is32bitValue(value: Long): Boolean { + return (value and 0xFFFFFFFFL.inv()) == 0L + } + + /** + * 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 + * 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. + */ + // TODO use a literal string + @JvmStatic + fun encodeString(str: String): String { + return "\"" + str.replace("\\", "\\\\").replace("\"", "\\\"") + "\"" + } + + internal fun combineFlags(flags: Iterable, canCreateForwardedFlag: Boolean): String { + return flags.mapNotNull { flag -> + when (flag) { + Flag.SEEN -> "\\Seen" + Flag.DELETED -> "\\Deleted" + Flag.ANSWERED -> "\\Answered" + Flag.FLAGGED -> "\\Flagged" + Flag.FORWARDED -> if (canCreateForwardedFlag) $$"$Forwarded" else null + Flag.DRAFT -> "\\Draft" + else -> null + } + }.joinToString(" ") + } +} diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt index cf2de06dde2..8a421e01834 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.kt @@ -321,6 +321,7 @@ internal class RealImapConnection( throw MissingCapabilityException(Capabilities.AUTH_OAUTHBEARER) } } + AuthType.CRAM_MD5 -> { if (hasCapability(Capabilities.AUTH_CRAM_MD5)) { authCramMD5() @@ -328,6 +329,7 @@ internal class RealImapConnection( throw MissingCapabilityException(Capabilities.AUTH_CRAM_MD5) } } + AuthType.PLAIN -> { if (hasCapability(Capabilities.AUTH_PLAIN)) { saslAuthPlainWithLoginFallback() @@ -337,6 +339,7 @@ internal class RealImapConnection( throw MissingCapabilityException(Capabilities.AUTH_PLAIN) } } + AuthType.EXTERNAL -> { if (hasCapability(Capabilities.AUTH_EXTERNAL)) { saslAuthExternal() @@ -344,6 +347,7 @@ internal class RealImapConnection( throw MissingCapabilityException(Capabilities.AUTH_EXTERNAL) } } + else -> { throw MessagingException("Unhandled authentication method found in the server settings (bug).") } @@ -409,7 +413,11 @@ internal class RealImapConnection( val authString = method.buildInitialClientResponse(settings.username, token) val tag = sendSaslIrCommand(method.command, authString, true) - return responseParser.readStatusResponse(tag, method.command, logId, ::handleOAuthUntaggedResponse) + return responseParser.readStatusResponse( + tag, + method.command, + logId, + ) { handleOAuthUntaggedResponse(it) } } private fun handleOAuthUntaggedResponse(response: ImapResponse) { diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt index 1c4cc41a88c..7f661361e70 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt @@ -153,7 +153,7 @@ internal class RealImapFolder( handlePermanentFlags(response) } - handleSelectOrExamineOkResponse(ImapUtility.getLastResponse(responses)) + handleSelectOrExamineOkResponse(responses.last()) exists = true @@ -609,7 +609,7 @@ internal class RealImapFolder( fetchFields.add("BODY.PEEK[]") } - val spaceSeparatedFetchFields = ImapUtility.join(" ", fetchFields) + val spaceSeparatedFetchFields = fetchFields.joinToString(" ") var windowStart = 0 val processedUids = mutableSetOf() while (windowStart < messages.size) { @@ -617,7 +617,7 @@ internal class RealImapFolder( val uidWindow = uids.subList(windowStart, windowEnd) try { - val commaSeparatedUids = ImapUtility.join(",", uidWindow) + val commaSeparatedUids = uidWindow.joinToString(",") val command = String.format("UID FETCH %s (%s)", commaSeparatedUids, spaceSeparatedFetchFields) connection!!.sendCommand(command, false) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/UntaggedHandler.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/UntaggedHandler.java deleted file mode 100644 index 7dc72449957..00000000000 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/UntaggedHandler.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.fsck.k9.mail.store.imap; - -import java.io.IOException; - -interface UntaggedHandler { - void handleAsyncUntaggedResponse(ImapResponse response) throws IOException; -} diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/UntaggedHandler.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/UntaggedHandler.kt new file mode 100644 index 00000000000..3e01f2aea11 --- /dev/null +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/UntaggedHandler.kt @@ -0,0 +1,8 @@ +package com.fsck.k9.mail.store.imap + +import java.io.IOException + +internal fun interface UntaggedHandler { + @Throws(IOException::class) + fun handleAsyncUntaggedResponse(response: ImapResponse) +} diff --git a/mail/protocols/imap/src/main/kotlin/net/thunderbird/protocols/imap/folder/FolderTypeAttribute.kt b/mail/protocols/imap/src/main/java/net/thunderbird/protocols/imap/folder/FolderTypeAttribute.kt similarity index 100% rename from mail/protocols/imap/src/main/kotlin/net/thunderbird/protocols/imap/folder/FolderTypeAttribute.kt rename to mail/protocols/imap/src/main/java/net/thunderbird/protocols/imap/folder/FolderTypeAttribute.kt diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapCommandSplitterTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapCommandSplitterTest.kt index f1da44f5ace..bfded82d76a 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapCommandSplitterTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapCommandSplitterTest.kt @@ -61,7 +61,7 @@ class ImapCommandSplitterTest { val sortedIds: Set = TreeSet(ids) val expectedCommandBuilder = StringBuilder(COMMAND_PREFIX) .append(" ") - .append(ImapUtility.join(",", sortedIds)) + .append(sortedIds.joinToString(",")) if (idGroupString != null) { expectedCommandBuilder.append(',').append(idGroupString) } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapUtilityTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapUtilityTest.java deleted file mode 100644 index f401649403b..00000000000 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapUtilityTest.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (C) 2012 The K-9 Dog Walkers - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.fsck.k9.mail.store.imap; - - -import java.util.List; - -import net.thunderbird.core.logging.legacy.Log; -import net.thunderbird.core.logging.testing.TestLogger; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertArrayEquals; - - -public class ImapUtilityTest { - - @Before - public void setUp() { - Log.logger = new TestLogger(); - } - - @Test - public void testGetImapSequenceValues() { - String[] expected; - List actual; - - // Test valid sets - expected = new String[] {"1"}; - actual = ImapUtility.getImapSequenceValues("1"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[] {"2147483648"}; // Integer.MAX_VALUE + 1 - actual = ImapUtility.getImapSequenceValues("2147483648"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[] {"4294967295"}; // 2^32 - 1 - actual = ImapUtility.getImapSequenceValues("4294967295"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[] {"1", "3", "2"}; - actual = ImapUtility.getImapSequenceValues("1,3,2"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[] {"4", "5", "6"}; - actual = ImapUtility.getImapSequenceValues("4:6"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[] {"9", "8", "7"}; - actual = ImapUtility.getImapSequenceValues("9:7"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[] {"1", "2", "3", "4", "9", "8", "7"}; - actual = ImapUtility.getImapSequenceValues("1,2:4,9:7"); - assertArrayEquals(expected, actual.toArray()); - - // Test numbers larger than Integer.MAX_VALUE (2147483647) - expected = new String[] {"2147483646", "2147483647", "2147483648"}; - actual = ImapUtility.getImapSequenceValues("2147483646:2147483648"); - assertArrayEquals(expected, actual.toArray()); - - // Test partially invalid sets - expected = new String[] { "1", "5" }; - actual = ImapUtility.getImapSequenceValues("1,x,5"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[] { "1", "2", "3" }; - actual = ImapUtility.getImapSequenceValues("a:d,1:3"); - assertArrayEquals(expected, actual.toArray()); - - // Test invalid sets - expected = new String[0]; - actual = ImapUtility.getImapSequenceValues(""); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[0]; - actual = ImapUtility.getImapSequenceValues(null); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[0]; - actual = ImapUtility.getImapSequenceValues("a"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[0]; - actual = ImapUtility.getImapSequenceValues("1:x"); - assertArrayEquals(expected, actual.toArray()); - - // Test values larger than 2^32 - 1 - expected = new String[0]; - actual = ImapUtility.getImapSequenceValues("4294967296:4294967297"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[0]; - actual = ImapUtility.getImapSequenceValues("4294967296"); // 2^32 - assertArrayEquals(expected, actual.toArray()); - } - - @Test public void testGetImapRangeValues() { - String[] expected; - List actual; - - // Test valid ranges - expected = new String[] {"1", "2", "3"}; - actual = ImapUtility.getImapRangeValues("1:3"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[] {"16", "15", "14"}; - actual = ImapUtility.getImapRangeValues("16:14"); - assertArrayEquals(expected, actual.toArray()); - - // Test in-valid ranges - expected = new String[0]; - actual = ImapUtility.getImapRangeValues(""); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[0]; - actual = ImapUtility.getImapRangeValues(null); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[0]; - actual = ImapUtility.getImapRangeValues("a"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[0]; - actual = ImapUtility.getImapRangeValues("6"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[0]; - actual = ImapUtility.getImapRangeValues("1:3,6"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[0]; - actual = ImapUtility.getImapRangeValues("1:x"); - assertArrayEquals(expected, actual.toArray()); - - expected = new String[0]; - actual = ImapUtility.getImapRangeValues("1:*"); - assertArrayEquals(expected, actual.toArray()); - } -} diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapUtilityTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapUtilityTest.kt new file mode 100644 index 00000000000..d5672cf396d --- /dev/null +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapUtilityTest.kt @@ -0,0 +1,232 @@ +package com.fsck.k9.mail.store.imap + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import net.thunderbird.core.common.mail.Flag +import net.thunderbird.core.logging.legacy.Log +import net.thunderbird.core.logging.testing.TestLogger +import org.junit.Before +import org.junit.Test + +class ImapUtilityTest { + + @Before + fun setUp() { + Log.logger = TestLogger() + } + + @Test + fun `getImapSequenceValues with single value`() { + assertThat(ImapUtility.getImapSequenceValues("1")).containsExactly("1") + } + + @Test + fun `getImapSequenceValues with value above Integer MAX_VALUE`() { + assertThat(ImapUtility.getImapSequenceValues("2147483648")).containsExactly("2147483648") + } + + @Test + fun `getImapSequenceValues with max 32-bit value`() { + assertThat(ImapUtility.getImapSequenceValues("4294967295")).containsExactly("4294967295") + } + + @Test + fun `getImapSequenceValues with multiple values`() { + assertThat(ImapUtility.getImapSequenceValues("1,3,2")).containsExactly("1", "3", "2") + } + + @Test + fun `getImapSequenceValues with ascending range`() { + assertThat(ImapUtility.getImapSequenceValues("4:6")).containsExactly("4", "5", "6") + } + + @Test + fun `getImapSequenceValues with descending range`() { + assertThat(ImapUtility.getImapSequenceValues("9:7")).containsExactly("9", "8", "7") + } + + @Test + fun `getImapSequenceValues with mixed values and ranges`() { + assertThat(ImapUtility.getImapSequenceValues("1,2:4,9:7")) + .containsExactly("1", "2", "3", "4", "9", "8", "7") + } + + @Test + fun `getImapSequenceValues with range crossing Integer MAX_VALUE`() { + assertThat(ImapUtility.getImapSequenceValues("2147483646:2147483648")) + .containsExactly("2147483646", "2147483647", "2147483648") + } + + @Test + fun `getImapSequenceValues with partially invalid set`() { + assertThat(ImapUtility.getImapSequenceValues("1,x,5")).containsExactly("1", "5") + } + + @Test + fun `getImapSequenceValues with invalid range and valid range`() { + assertThat(ImapUtility.getImapSequenceValues("a:d,1:3")).containsExactly("1", "2", "3") + } + + @Test + fun `getImapSequenceValues with empty string`() { + assertThat(ImapUtility.getImapSequenceValues("")).isEmpty() + } + + @Test + fun `getImapSequenceValues with null`() { + assertThat(ImapUtility.getImapSequenceValues(null)).isEmpty() + } + + @Test + fun `getImapSequenceValues with non-numeric string`() { + assertThat(ImapUtility.getImapSequenceValues("a")).isEmpty() + } + + @Test + fun `getImapSequenceValues with invalid range`() { + assertThat(ImapUtility.getImapSequenceValues("1:x")).isEmpty() + } + + @Test + fun `getImapSequenceValues with value exceeding 32 bits`() { + assertThat(ImapUtility.getImapSequenceValues("4294967296:4294967297")).isEmpty() + } + + @Test + fun `getImapSequenceValues with single value exceeding 32 bits`() { + assertThat(ImapUtility.getImapSequenceValues("4294967296")).isEmpty() + } + + @Test + fun `getImapRangeValues with ascending range`() { + assertThat(ImapUtility.getImapRangeValues("1:3")).containsExactly("1", "2", "3") + } + + @Test + fun `getImapRangeValues with descending range`() { + assertThat(ImapUtility.getImapRangeValues("16:14")).containsExactly("16", "15", "14") + } + + @Test + fun `getImapRangeValues with empty string`() { + assertThat(ImapUtility.getImapRangeValues("")).isEmpty() + } + + @Test + fun `getImapRangeValues with null`() { + assertThat(ImapUtility.getImapRangeValues(null)).isEmpty() + } + + @Test + fun `getImapRangeValues with non-numeric string`() { + assertThat(ImapUtility.getImapRangeValues("a")).isEmpty() + } + + @Test + fun `getImapRangeValues with single number`() { + assertThat(ImapUtility.getImapRangeValues("6")).isEmpty() + } + + @Test + fun `getImapRangeValues with range and extra segment`() { + assertThat(ImapUtility.getImapRangeValues("1:3,6")).isEmpty() + } + + @Test + fun `getImapRangeValues with invalid upper bound`() { + assertThat(ImapUtility.getImapRangeValues("1:x")).isEmpty() + } + + @Test + fun `getImapRangeValues with wildcard upper bound`() { + assertThat(ImapUtility.getImapRangeValues("1:*")).isEmpty() + } + + @Test + fun `encodeString wraps string in double quotes`() { + assertThat(ImapUtility.encodeString("hello")).isEqualTo("\"hello\"") + } + + @Test + fun `encodeString escapes backslash`() { + assertThat(ImapUtility.encodeString("a\\b")).isEqualTo("\"a\\\\b\"") + } + + @Test + fun `encodeString escapes double quote`() { + assertThat(ImapUtility.encodeString("say \"hi\"")).isEqualTo("\"say \\\"hi\\\"\"") + } + + @Test + fun `encodeString escapes backslash before double quote`() { + assertThat(ImapUtility.encodeString("a\\\"b")).isEqualTo("\"a\\\\\\\"b\"") + } + + @Test + fun `encodeString with empty string`() { + assertThat(ImapUtility.encodeString("")).isEqualTo("\"\"") + } + + @Test + fun `combineFlags with no flags returns empty string`() { + assertThat(ImapUtility.combineFlags(emptyList(), false)).isEqualTo("") + } + + @Test + fun `combineFlags with seen flag`() { + assertThat(ImapUtility.combineFlags(listOf(Flag.SEEN), false)) + .isEqualTo("\\Seen") + } + + @Test + fun `combineFlags with deleted flag`() { + assertThat(ImapUtility.combineFlags(listOf(Flag.DELETED), false)) + .isEqualTo("\\Deleted") + } + + @Test + fun `combineFlags with answered flag`() { + assertThat(ImapUtility.combineFlags(listOf(Flag.ANSWERED), false)) + .isEqualTo("\\Answered") + } + + @Test + fun `combineFlags with flagged flag`() { + assertThat(ImapUtility.combineFlags(listOf(Flag.FLAGGED), false)) + .isEqualTo("\\Flagged") + } + + @Test + fun `combineFlags with draft flag`() { + assertThat(ImapUtility.combineFlags(listOf(Flag.DRAFT), false)) + .isEqualTo("\\Draft") + } + + @Test + fun `combineFlags with forwarded flag when canCreateForwardedFlag is true`() { + assertThat(ImapUtility.combineFlags(listOf(Flag.FORWARDED), true)) + .isEqualTo("\$Forwarded") + } + + @Test + fun `combineFlags with forwarded flag when canCreateForwardedFlag is false`() { + assertThat(ImapUtility.combineFlags(listOf(Flag.FORWARDED), false)) + .isEqualTo("") + } + + @Test + fun `combineFlags ignores unknown flags`() { + assertThat(ImapUtility.combineFlags(listOf(Flag.RECENT), false)) + .isEqualTo("") + } + + @Test + fun `combineFlags with multiple flags`() { + assertThat( + ImapUtility.combineFlags(listOf(Flag.SEEN, Flag.FLAGGED, Flag.DELETED), false), + ) + .isEqualTo("\\Seen \\Flagged \\Deleted") + } +} diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt index 9862033f8ed..ef8b0616461 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt @@ -1771,7 +1771,7 @@ class RealImapFolderTest { val commandUids = commandUidsCaptor.allValues for (i in commandPrefixes.indices) { - val command = commandPrefixes[i] + " " + ImapUtility.join(",", commandUids[i]) + + val command = commandPrefixes[i] + " " + commandUids[i].joinToString(",") + if (commandSuffixes[i].isEmpty()) "" else " " + commandSuffixes[i] if (command == expectedCommand) {